@bug-on/md3-react 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/index.css.d.ts +2 -0
  4. package/dist/index.d.mts +6127 -0
  5. package/dist/index.d.ts +6127 -71
  6. package/dist/index.js +1653 -614
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1566 -547
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/material-symbols-cdn.css.d.ts +2 -0
  11. package/dist/material-symbols-self-hosted.css.d.ts +2 -0
  12. package/dist/typography.css.d.ts +2 -0
  13. package/package.json +22 -19
  14. package/scripts/copy-assets.js +82 -0
  15. package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
  16. package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  17. package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  18. package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
  19. package/src/assets/loading-indicator.svg +19 -0
  20. package/src/assets/material-symbols-cdn.css +65 -0
  21. package/src/assets/material-symbols-self-hosted.css +90 -0
  22. package/src/css.d.ts +20 -0
  23. package/src/hooks/useClickOutside.ts +37 -0
  24. package/src/hooks/useMediaQuery.ts +28 -0
  25. package/src/hooks/useRipple.ts +88 -0
  26. package/src/index.css +23 -0
  27. package/src/index.ts +349 -0
  28. package/src/lib/material-symbols-preconnect.tsx +82 -0
  29. package/src/lib/theme-utils.ts +180 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/test/button.test.tsx +59 -0
  32. package/src/test/icon.test.tsx +91 -0
  33. package/src/test/loading-indicator.test.tsx +128 -0
  34. package/src/test/progress-indicator.test.tsx +306 -0
  35. package/src/test/setup.ts +80 -0
  36. package/src/test/typography.test.tsx +206 -0
  37. package/src/types/index.ts +7 -0
  38. package/src/types/md3.ts +31 -0
  39. package/src/ui/Text.tsx +60 -0
  40. package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
  41. package/src/ui/app-bar/app-bar-column.tsx +99 -0
  42. package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
  43. package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
  44. package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
  45. package/src/ui/app-bar/app-bar-row.tsx +104 -0
  46. package/src/ui/app-bar/app-bar.test.tsx +87 -0
  47. package/src/ui/app-bar/app-bar.tokens.ts +223 -0
  48. package/src/ui/app-bar/app-bar.types.ts +441 -0
  49. package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
  50. package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
  51. package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
  52. package/src/ui/app-bar/docked-toolbar.tsx +54 -0
  53. package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
  54. package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
  55. package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
  56. package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
  57. package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
  58. package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
  59. package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
  60. package/src/ui/app-bar/search-app-bar.tsx +176 -0
  61. package/src/ui/app-bar/search-view.tsx +227 -0
  62. package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
  63. package/src/ui/app-bar/small-app-bar.tsx +203 -0
  64. package/src/ui/badge.test.tsx +345 -0
  65. package/src/ui/badge.tsx +282 -0
  66. package/src/ui/button-group.test.tsx +71 -0
  67. package/src/ui/button-group.tsx +350 -0
  68. package/src/ui/button.test.tsx +297 -0
  69. package/src/ui/button.tsx +669 -0
  70. package/src/ui/card.test.tsx +187 -0
  71. package/src/ui/card.tsx +259 -0
  72. package/src/ui/checkbox.test.tsx +423 -0
  73. package/src/ui/checkbox.tsx +525 -0
  74. package/src/ui/chip.test.tsx +292 -0
  75. package/src/ui/chip.tsx +548 -0
  76. package/src/ui/code-block.tsx +219 -0
  77. package/src/ui/dialog.test.tsx +300 -0
  78. package/src/ui/dialog.tsx +384 -0
  79. package/src/ui/divider.test.tsx +314 -0
  80. package/src/ui/divider.tsx +412 -0
  81. package/src/ui/drawer.tsx +240 -0
  82. package/src/ui/fab-menu.test.tsx +494 -0
  83. package/src/ui/fab-menu.tsx +739 -0
  84. package/src/ui/fab.test.tsx +232 -0
  85. package/src/ui/fab.tsx +505 -0
  86. package/src/ui/icon-button.test.tsx +515 -0
  87. package/src/ui/icon-button.tsx +525 -0
  88. package/src/ui/icon.test.tsx +197 -0
  89. package/src/ui/icon.tsx +179 -0
  90. package/src/ui/loading-indicator.test.tsx +73 -0
  91. package/src/ui/loading-indicator.tsx +312 -0
  92. package/src/ui/menu/context-menu.tsx +275 -0
  93. package/src/ui/menu/index.ts +77 -0
  94. package/src/ui/menu/menu-animations.ts +102 -0
  95. package/src/ui/menu/menu-context.tsx +99 -0
  96. package/src/ui/menu/menu-divider.tsx +47 -0
  97. package/src/ui/menu/menu-group.tsx +200 -0
  98. package/src/ui/menu/menu-item.tsx +294 -0
  99. package/src/ui/menu/menu-tokens.ts +208 -0
  100. package/src/ui/menu/menu-types.ts +313 -0
  101. package/src/ui/menu/menu.test.tsx +624 -0
  102. package/src/ui/menu/menu.tsx +289 -0
  103. package/src/ui/menu/sub-menu.tsx +223 -0
  104. package/src/ui/menu/vertical-menu.tsx +382 -0
  105. package/src/ui/navigation-rail.test.tsx +404 -0
  106. package/src/ui/navigation-rail.tsx +604 -0
  107. package/src/ui/progress-indicator/circular.tsx +248 -0
  108. package/src/ui/progress-indicator/hooks.ts +51 -0
  109. package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
  110. package/src/ui/progress-indicator/linear-flat.tsx +83 -0
  111. package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
  112. package/src/ui/progress-indicator/linear.tsx +143 -0
  113. package/src/ui/progress-indicator/types.ts +158 -0
  114. package/src/ui/progress-indicator/utils.ts +73 -0
  115. package/src/ui/radio-button.test.tsx +407 -0
  116. package/src/ui/radio-button.tsx +551 -0
  117. package/src/ui/ripple.test.tsx +72 -0
  118. package/src/ui/ripple.tsx +234 -0
  119. package/src/ui/scroll-area.test.tsx +58 -0
  120. package/src/ui/scroll-area.tsx +139 -0
  121. package/src/ui/search/animated-placeholder.tsx +145 -0
  122. package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
  123. package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
  124. package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
  125. package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
  126. package/src/ui/search/index.ts +44 -0
  127. package/src/ui/search/search-bar.tsx +220 -0
  128. package/src/ui/search/search-context.tsx +42 -0
  129. package/src/ui/search/search-view-docked.tsx +194 -0
  130. package/src/ui/search/search-view-fullscreen.tsx +247 -0
  131. package/src/ui/search/search.test.tsx +233 -0
  132. package/src/ui/search/search.tokens.ts +134 -0
  133. package/src/ui/search/search.tsx +131 -0
  134. package/src/ui/search/search.types.ts +154 -0
  135. package/src/ui/search/trailing-action.tsx +49 -0
  136. package/src/ui/shared/constants.ts +122 -0
  137. package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
  138. package/src/ui/slider/hooks/useSliderMath.ts +195 -0
  139. package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
  140. package/src/ui/slider/range-slider.tsx +561 -0
  141. package/src/ui/slider/slider-thumb.tsx +379 -0
  142. package/src/ui/slider/slider-track.tsx +912 -0
  143. package/src/ui/slider/slider.tokens.ts +189 -0
  144. package/src/ui/slider/slider.tsx +259 -0
  145. package/src/ui/slider/slider.types.ts +288 -0
  146. package/src/ui/snackbar/index.ts +20 -0
  147. package/src/ui/snackbar/snackbar.test.tsx +338 -0
  148. package/src/ui/snackbar/snackbar.tsx +476 -0
  149. package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
  150. package/src/ui/switch/switch.stories.tsx +309 -0
  151. package/src/ui/switch/switch.test.tsx +243 -0
  152. package/src/ui/switch/switch.tokens.ts +89 -0
  153. package/src/ui/switch/switch.tsx +504 -0
  154. package/src/ui/switch/switch.types.ts +62 -0
  155. package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
  156. package/src/ui/tabs/tab.tsx +407 -0
  157. package/src/ui/tabs/tabs-content.tsx +89 -0
  158. package/src/ui/tabs/tabs-list.tsx +146 -0
  159. package/src/ui/tabs/tabs.test.tsx +290 -0
  160. package/src/ui/tabs/tabs.tokens.ts +121 -0
  161. package/src/ui/tabs/tabs.tsx +229 -0
  162. package/src/ui/tabs/tabs.types.ts +185 -0
  163. package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
  164. package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
  165. package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
  166. package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
  167. package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
  168. package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
  169. package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
  170. package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
  171. package/src/ui/text-field/text-field.test.tsx +454 -0
  172. package/src/ui/text-field/text-field.tokens.ts +104 -0
  173. package/src/ui/text-field/text-field.tsx +548 -0
  174. package/src/ui/text-field/text-field.types.ts +180 -0
  175. package/src/ui/theme-provider/index.tsx +190 -0
  176. package/src/ui/toc.test.tsx +108 -0
  177. package/src/ui/toc.tsx +172 -0
  178. package/src/ui/tooltip/plain-tooltip.tsx +63 -0
  179. package/src/ui/tooltip/rich-tooltip.tsx +94 -0
  180. package/src/ui/tooltip/tooltip-box.tsx +266 -0
  181. package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
  182. package/src/ui/tooltip/tooltip.tokens.ts +26 -0
  183. package/src/ui/tooltip/tooltip.types.ts +70 -0
  184. package/src/ui/tooltip/use-tooltip-position.ts +208 -0
  185. package/src/ui/tooltip/use-tooltip-state.ts +41 -0
  186. package/src/ui/typography/__tests__/typography.test.tsx +170 -0
  187. package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
  188. package/src/ui/typography/type-scale-tokens.ts +205 -0
  189. package/src/ui/typography/typography-key-tokens.ts +43 -0
  190. package/src/ui/typography/typography-tokens.ts +360 -0
  191. package/src/ui/typography/typography.css +22 -0
  192. package/src/ui/typography/typography.tsx +559 -0
  193. package/test-render.tsx +4 -0
  194. package/test-shadow.html +26 -0
  195. package/test_output.txt +164 -0
  196. package/test_output_v2.txt +5 -0
  197. package/tsconfig.build.json +10 -0
  198. package/tsconfig.json +18 -0
  199. package/tsup.config.ts +20 -0
  200. package/vitest.config.ts +11 -0
  201. package/dist/hooks/useClickOutside.d.ts +0 -8
  202. package/dist/hooks/useMediaQuery.d.ts +0 -11
  203. package/dist/hooks/useRipple.d.ts +0 -26
  204. package/dist/lib/material-symbols-preconnect.d.ts +0 -42
  205. package/dist/lib/theme-utils.d.ts +0 -63
  206. package/dist/lib/utils.d.ts +0 -2
  207. package/dist/types/index.d.ts +0 -1
  208. package/dist/types/md3.d.ts +0 -14
  209. package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
  210. package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
  211. package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
  212. package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
  213. package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
  214. package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
  215. package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
  216. package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
  217. package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
  218. package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
  219. package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
  220. package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
  221. package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
  222. package/dist/ui/app-bar/search-view.d.ts +0 -54
  223. package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
  224. package/dist/ui/badge.d.ts +0 -125
  225. package/dist/ui/button-group.d.ts +0 -59
  226. package/dist/ui/button.d.ts +0 -148
  227. package/dist/ui/card.d.ts +0 -62
  228. package/dist/ui/checkbox.d.ts +0 -82
  229. package/dist/ui/chip.d.ts +0 -110
  230. package/dist/ui/code-block.d.ts +0 -14
  231. package/dist/ui/dialog.d.ts +0 -111
  232. package/dist/ui/divider.d.ts +0 -164
  233. package/dist/ui/drawer.d.ts +0 -39
  234. package/dist/ui/dropdown.d.ts +0 -29
  235. package/dist/ui/fab-menu.d.ts +0 -204
  236. package/dist/ui/fab.d.ts +0 -162
  237. package/dist/ui/icon-button.d.ts +0 -131
  238. package/dist/ui/icon.d.ts +0 -88
  239. package/dist/ui/loading-indicator.d.ts +0 -42
  240. package/dist/ui/navigation-rail.d.ts +0 -29
  241. package/dist/ui/progress-indicator/circular.d.ts +0 -3
  242. package/dist/ui/progress-indicator/hooks.d.ts +0 -3
  243. package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
  244. package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
  245. package/dist/ui/progress-indicator/linear.d.ts +0 -3
  246. package/dist/ui/progress-indicator/types.d.ts +0 -151
  247. package/dist/ui/progress-indicator/utils.d.ts +0 -3
  248. package/dist/ui/radio-button.d.ts +0 -106
  249. package/dist/ui/ripple.d.ts +0 -126
  250. package/dist/ui/scroll-area.d.ts +0 -27
  251. package/dist/ui/search/animated-placeholder.d.ts +0 -54
  252. package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
  253. package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
  254. package/dist/ui/search/index.d.ts +0 -27
  255. package/dist/ui/search/search-bar.d.ts +0 -32
  256. package/dist/ui/search/search-context.d.ts +0 -24
  257. package/dist/ui/search/search-view-docked.d.ts +0 -25
  258. package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
  259. package/dist/ui/search/search.d.ts +0 -50
  260. package/dist/ui/search/search.tokens.d.ts +0 -112
  261. package/dist/ui/search/search.types.d.ts +0 -131
  262. package/dist/ui/search/trailing-action.d.ts +0 -9
  263. package/dist/ui/shared/constants.d.ts +0 -86
  264. package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
  265. package/dist/ui/slider/range-slider.d.ts +0 -47
  266. package/dist/ui/slider/slider-thumb.d.ts +0 -33
  267. package/dist/ui/slider/slider-track.d.ts +0 -25
  268. package/dist/ui/slider/slider.d.ts +0 -60
  269. package/dist/ui/slider/slider.tokens.d.ts +0 -151
  270. package/dist/ui/slider/slider.types.d.ts +0 -259
  271. package/dist/ui/snackbar/index.d.ts +0 -6
  272. package/dist/ui/snackbar/snackbar.d.ts +0 -197
  273. package/dist/ui/switch/switch.d.ts +0 -30
  274. package/dist/ui/switch/switch.stories.d.ts +0 -48
  275. package/dist/ui/switch/switch.tokens.d.ts +0 -67
  276. package/dist/ui/switch/switch.types.d.ts +0 -59
  277. package/dist/ui/tabs/tab.d.ts +0 -43
  278. package/dist/ui/tabs/tabs-content.d.ts +0 -36
  279. package/dist/ui/tabs/tabs-list.d.ts +0 -40
  280. package/dist/ui/tabs/tabs.d.ts +0 -60
  281. package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
  282. package/dist/ui/tabs/tabs.types.d.ts +0 -172
  283. package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
  284. package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
  285. package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
  286. package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
  287. package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
  288. package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
  289. package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
  290. package/dist/ui/text-field/text-field.d.ts +0 -49
  291. package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
  292. package/dist/ui/text-field/text-field.types.d.ts +0 -126
  293. package/dist/ui/theme-provider/index.d.ts +0 -48
  294. package/dist/ui/toc.d.ts +0 -80
  295. package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
  296. package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
  297. package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
  298. package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
  299. package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
  300. package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
  301. package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
  302. package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
  303. package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
  304. package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
  305. package/dist/ui/typography/typography-tokens.d.ts +0 -220
  306. package/dist/ui/typography/typography.d.ts +0 -265
  307. /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
  308. /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * @file chip.tsx
3
+ *
4
+ * MD3 Expressive Chip component — 4 variants.
5
+ *
6
+ * - `assist` → Triggered actions spanning multiple apps. Flat (bordered) or Elevated.
7
+ * - `filter` → Toggleable selections. Animated checkmark on select.
8
+ * - `input` → Entities/tags with optional avatar and a dedicated remove button.
9
+ * - `suggestion` → Contextual dynamic recommendations. Flat (bordered) or Elevated.
10
+ *
11
+ * @remarks
12
+ * Token references (Kotlin source):
13
+ * AssistChipTokens, FilterChipTokens, InputChipTokens, SuggestionChipTokens
14
+ *
15
+ * Architecture:
16
+ * - Styling: `cva` + `cn` (clsx/tailwind-merge)
17
+ * - Animation: Framer Motion (`LazyMotion` + `domMax`) for animated checkmark
18
+ * - Ripple: `Ripple` + `useRippleState` from `./ripple.tsx`
19
+ * - A11y: `role="checkbox"` (filter), `role="button"` (others); full keyboard support
20
+ *
21
+ * @see https://m3.material.io/components/chips/overview
22
+ */
23
+
24
+ import { cva, type VariantProps } from "class-variance-authority";
25
+ import { AnimatePresence, domMax, LazyMotion, m } from "motion/react";
26
+ import * as React from "react";
27
+ import { cn } from "../lib/utils";
28
+ import { Ripple, useRippleState } from "./ripple";
29
+
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // Internal Icons
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Animated checkmark icon for selected Filter chips.
36
+ * @internal
37
+ */
38
+ function CheckIcon({ className }: { className?: string }) {
39
+ return (
40
+ <svg
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ viewBox="0 0 24 24"
43
+ width={18}
44
+ height={18}
45
+ fill="none"
46
+ stroke="currentColor"
47
+ strokeWidth={2.5}
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ aria-hidden="true"
51
+ className={className}
52
+ >
53
+ <polyline points="20 6 9 17 4 12" />
54
+ </svg>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Close (×) icon for the trailing remove button on Input chips.
60
+ * @internal
61
+ */
62
+ function CloseIcon({ className }: { className?: string }) {
63
+ return (
64
+ <svg
65
+ xmlns="http://www.w3.org/2000/svg"
66
+ viewBox="0 0 24 24"
67
+ width={18}
68
+ height={18}
69
+ fill="none"
70
+ stroke="currentColor"
71
+ strokeWidth={2.5}
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ aria-hidden="true"
75
+ className={className}
76
+ >
77
+ <line x1="18" y1="6" x2="6" y2="18" />
78
+ <line x1="6" y1="6" x2="18" y2="18" />
79
+ </svg>
80
+ );
81
+ }
82
+
83
+ // CVA Variants
84
+ // Token mapping references (Kotlin source):
85
+ // - FlatOutlineColor (all variants): border-m3-outline-variant
86
+ // - FlatSelectedContainerColor: bg-m3-secondary-container
87
+ // - SelectedLabelTextColor: text-m3-on-secondary-container
88
+ // - ElevatedContainerColor: bg-m3-surface-container-low
89
+
90
+ const chipVariants = cva(
91
+ [
92
+ // Base layout
93
+ "inline-flex items-center h-8 rounded-lg",
94
+ // Typography: LabelLarge
95
+ "text-sm font-medium leading-none",
96
+ // Interaction
97
+ "relative overflow-hidden cursor-pointer select-none",
98
+ "transition-all duration-200 ease-in-out",
99
+ // Remove browser default focus; use MD3 focus ring
100
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1",
101
+ "focus-visible:ring-m3-secondary",
102
+ // Expressive scale feedback (MD3 "bouncy" press)
103
+ "active:scale-[0.98]",
104
+ // MD3 State Layer (pseudo-element overlay)
105
+ // Use inset-[-1px] to ensure the state layer covers the 1px border perfectly
106
+ "before:absolute before:inset-[-1px] before:pointer-events-none before:rounded-lg",
107
+ "before:transition-opacity before:duration-200 before:opacity-0",
108
+ "hover:before:opacity-[0.08] focus-visible:before:opacity-[0.10] active:before:opacity-[0.10]",
109
+ ].join(" "),
110
+ {
111
+ variants: {
112
+ /**
113
+ * Chip variant controlling default colors and border behavior.
114
+ * Selected/elevated state overrides are applied via `cn()` at runtime.
115
+ */
116
+ variant: {
117
+ /**
118
+ * Assist chip – FlatOutlineColor: outline-variant, LabelTextColor: on-surface
119
+ * Leading icon color: primary (AssistChipTokens.IconColor)
120
+ */
121
+ assist:
122
+ "border border-m3-outline-variant text-m3-on-surface before:bg-m3-on-surface",
123
+ /**
124
+ * Filter chip (unselected) – FlatUnselectedOutlineColor: outline-variant
125
+ * UnselectedLabelTextColor: on-surface-variant
126
+ */
127
+ filter:
128
+ "border border-m3-outline-variant text-m3-on-surface-variant before:bg-m3-on-surface-variant",
129
+ /**
130
+ * Input chip (unselected) – UnselectedOutlineColor: outline-variant
131
+ * UnselectedLabelTextColor: on-surface-variant
132
+ */
133
+ input:
134
+ "border border-m3-outline-variant text-m3-on-surface-variant before:bg-m3-on-surface-variant",
135
+ /**
136
+ * Suggestion chip – FlatOutlineColor: outline-variant
137
+ * LabelTextColor: on-surface-variant (SuggestionChipTokens)
138
+ */
139
+ suggestion:
140
+ "border border-m3-outline-variant text-m3-on-surface-variant before:bg-m3-on-surface-variant",
141
+ },
142
+ },
143
+ defaultVariants: { variant: "assist" },
144
+ },
145
+ );
146
+
147
+ export interface ChipProps
148
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
149
+ /**
150
+ * Chip variant.
151
+ * - `assist` → Smart/automated actions. Flat by default, can be elevated.
152
+ * - `filter` → Toggleable tag/filter. Shows animated checkmark when selected.
153
+ * - `input` → Entity representation (tag, contact). Has optional avatar + remove button.
154
+ * - `suggestion` → Contextual suggestions. Like assist, flat by default, can be elevated.
155
+ * @default 'assist'
156
+ */
157
+ variant?: VariantProps<typeof chipVariants>["variant"];
158
+
159
+ /**
160
+ * Renders with elevation shadow (Level 1) and fills background with `surface-container-low`.
161
+ * Applicable to `assist`, `filter` (unselected), and `suggestion` variants.
162
+ * Source: AssistChipTokens.ElevatedContainerColor / SuggestionChipTokens.ElevatedContainerColor
163
+ */
164
+ elevated?: boolean;
165
+
166
+ /**
167
+ * Toggle/selection state.
168
+ * - `filter`: selected → bg `secondary-container`, animated checkmark appears.
169
+ * - `input`: selected → bg `secondary-container`.
170
+ * Used for `role="checkbox"` (filter) / `aria-pressed` (input).
171
+ */
172
+ selected?: boolean;
173
+
174
+ /**
175
+ * Disables the chip. Applies:
176
+ * - `pointer-events-none` – no mouse/touch interaction
177
+ * - `opacity-[0.38]` – DisabledLabelTextOpacity (0.38) per MD3 tokens
178
+ * - `aria-disabled="true"`
179
+ * - `tabIndex={-1}`
180
+ */
181
+ disabled?: boolean;
182
+
183
+ /**
184
+ * Visible label. Required. Can be a string or ReactNode.
185
+ */
186
+ label: React.ReactNode;
187
+
188
+ /**
189
+ * Optional leading icon element (18×18px recommended).
190
+ * For `filter` chips with `selected=true`, this is replaced by an animated checkmark.
191
+ * For `assist`/`suggestion`: icon color → `primary`
192
+ * For `input` (unselected): icon color → `on-surface-variant`
193
+ */
194
+ leadingIcon?: React.ReactNode;
195
+
196
+ /**
197
+ * Optional trailing icon element (18×18px recommended).
198
+ * Color: `on-surface-variant` (unselected) / `on-secondary-container` (selected).
199
+ */
200
+ trailingIcon?: React.ReactNode;
201
+
202
+ /**
203
+ * Avatar element for `input` chips. Takes priority over `leadingIcon`.
204
+ * Rendered as a 24×24px circle (InputChipTokens: AvatarSize = 24.dp, AvatarShape = CornerFull).
205
+ */
206
+ avatar?: React.ReactNode;
207
+
208
+ /**
209
+ * Callback when the trailing remove (×) button is activated on `input` chips.
210
+ * When provided, a dedicated tabbable close button with `aria-label="Remove {label}"` is rendered.
211
+ */
212
+ onRemove?: (e: React.MouseEvent<HTMLButtonElement>) => void;
213
+ }
214
+
215
+ const ChipImpl = React.forwardRef<HTMLButtonElement, ChipProps>(
216
+ (
217
+ {
218
+ variant = "assist",
219
+ elevated = false,
220
+ selected = false,
221
+ disabled = false,
222
+ label,
223
+ leadingIcon,
224
+ trailingIcon,
225
+ avatar,
226
+ onRemove,
227
+ className,
228
+ onClick,
229
+ ...props
230
+ },
231
+ ref,
232
+ ) => {
233
+ const { ripples, onPointerDown, removeRipple } = useRippleState({
234
+ disabled,
235
+ });
236
+
237
+ const isFilter = variant === "filter";
238
+ const isInput = variant === "input";
239
+
240
+ /**
241
+ * For input chips, avatar takes priority over leadingIcon.
242
+ * Source: leadingContent() in Chip.kt – "An avatar takes precedence"
243
+ */
244
+ const resolvedLeadingIcon = isInput && avatar ? avatar : leadingIcon;
245
+
246
+ /**
247
+ * Filter chip: when selected, show animated checkmark.
248
+ * If a leadingIcon is provided, the checkmark replaces it when selected.
249
+ * Source: Chip.kt – AnimatedVisibility with expandHorizontally + fadeIn
250
+ */
251
+ const showCheckmark = isFilter && selected;
252
+
253
+ /** Trailing slot: custom trailingIcon OR the remove button for input chips */
254
+ const hasTrailingContent = !!trailingIcon || !!onRemove;
255
+
256
+ /**
257
+ * Leading slot: filter chips always have a potential leading slot
258
+ * (checkmark or leadingIcon). Otherwise depends on resolvedLeadingIcon.
259
+ */
260
+ const hasLeadingContent = isFilter || !!resolvedLeadingIcon;
261
+
262
+ // Source: AssistChipDefaults.ContentPadding / inputChipPadding in Chip.kt
263
+ // No icons: px-4 | Input: px-3
264
+ // Leading only: pl-2 pr-4 | Input: pl-1 pr-3
265
+ // Trailing only: pl-4 pr-2 | Input: pl-3 pr-2
266
+ // Both: px-2 | Input: pl-1 pr-2
267
+ const paddingClass = React.useMemo(
268
+ () =>
269
+ cn(
270
+ !isInput && !hasLeadingContent && !hasTrailingContent && "px-4",
271
+ !isInput && hasLeadingContent && !hasTrailingContent && "pl-2 pr-4",
272
+ !isInput && !hasLeadingContent && hasTrailingContent && "pl-4 pr-2",
273
+ !isInput && hasLeadingContent && hasTrailingContent && "px-2",
274
+ isInput && !hasLeadingContent && !hasTrailingContent && "px-3",
275
+ isInput && hasLeadingContent && !hasTrailingContent && "pl-1 pr-3",
276
+ isInput && !hasLeadingContent && hasTrailingContent && "pl-3 pr-2",
277
+ isInput && hasLeadingContent && hasTrailingContent && "pl-1 pr-2",
278
+ ),
279
+ [isInput, hasLeadingContent, hasTrailingContent],
280
+ );
281
+
282
+ // Selected / elevated / disabled state overrides.
283
+ // Source: MD3 tokens — DisabledLabelTextOpacity=0.38, FlatDisabledOutlineOpacity=0.12
284
+ const stateClass = React.useMemo(
285
+ () =>
286
+ cn(
287
+ (isFilter || isInput) &&
288
+ selected &&
289
+ "bg-m3-secondary-container text-m3-on-secondary-container border-none before:bg-m3-on-secondary-container",
290
+ elevated &&
291
+ !selected &&
292
+ "bg-m3-surface-container-low border-none elevation-1",
293
+ elevated && isFilter && selected && "elevation-1",
294
+ disabled && "opacity-[0.38] pointer-events-none cursor-not-allowed",
295
+ disabled && !selected && "border-m3-outline-variant/[.12]",
296
+ disabled &&
297
+ selected &&
298
+ "bg-m3-on-surface/[.12] text-m3-on-surface border-none",
299
+ ),
300
+ [isFilter, isInput, selected, elevated, disabled],
301
+ );
302
+
303
+ // Leading icon color tokens:
304
+ // assist/suggestion → Primary | filter unselected → Primary
305
+ // filter selected → OnSecondaryContainer | input unselected → OnSurfaceVariant | input selected → Primary
306
+ const leadingIconColorClass = React.useMemo(
307
+ () =>
308
+ cn(
309
+ (variant === "assist" || variant === "suggestion") &&
310
+ "text-m3-primary",
311
+ isFilter && !selected && "text-m3-primary",
312
+ isFilter && selected && "text-m3-on-secondary-container",
313
+ isInput && !selected && "text-m3-on-surface-variant",
314
+ isInput && selected && "text-m3-primary",
315
+ ),
316
+ [variant, isFilter, isInput, selected],
317
+ );
318
+
319
+ const isCompound = !!onRemove;
320
+ const Root = (isCompound ? "div" : "button") as React.ElementType;
321
+
322
+ // Composed class for the root container
323
+ const containerClass = cn(
324
+ chipVariants({ variant }),
325
+ !isCompound && paddingClass,
326
+ !isCompound && "gap-2",
327
+ stateClass,
328
+ // When compound, the root div handles the overall shape but not the main interaction
329
+ // We disable the root's state layer (hover/focus) to avoid the 1px gap issue
330
+ isCompound &&
331
+ "items-stretch cursor-default active:scale-100 before:opacity-0 gap-0",
332
+ className,
333
+ );
334
+
335
+ /** Main hit area content (Leading Icon + Label) */
336
+ const mainContent = (
337
+ <>
338
+ <AnimatePresence initial={false} mode="wait">
339
+ {isFilter ? (
340
+ showCheckmark ? (
341
+ <m.span
342
+ key="checkmark"
343
+ initial={{ width: 0, opacity: 0 }}
344
+ animate={{ width: 18, opacity: 1 }}
345
+ exit={{ width: 0, opacity: 0 }}
346
+ transition={{
347
+ width: { duration: 0.2, ease: [0.2, 0, 0, 1] },
348
+ opacity: { duration: 0.15, ease: "easeOut" },
349
+ }}
350
+ className="flex items-center justify-center shrink-0 overflow-hidden"
351
+ aria-hidden="true"
352
+ >
353
+ <CheckIcon />
354
+ </m.span>
355
+ ) : resolvedLeadingIcon ? (
356
+ <m.span
357
+ key="leading-icon"
358
+ initial={{ width: 0, opacity: 0 }}
359
+ animate={{ width: 18, opacity: 1 }}
360
+ exit={{ width: 0, opacity: 0 }}
361
+ transition={{
362
+ width: { duration: 0.2, ease: [0.2, 0, 0, 1] },
363
+ opacity: { duration: 0.15, ease: "easeOut" },
364
+ }}
365
+ className={cn(
366
+ "flex items-center justify-center shrink-0 overflow-hidden [&_.md-icon]:text-[length:inherit]!",
367
+ leadingIconColorClass,
368
+ )}
369
+ style={{ fontSize: 18 }}
370
+ aria-hidden="true"
371
+ >
372
+ {resolvedLeadingIcon}
373
+ </m.span>
374
+ ) : null
375
+ ) : resolvedLeadingIcon ? (
376
+ isInput && avatar ? (
377
+ <span
378
+ key="avatar"
379
+ className="flex items-center justify-center shrink-0 w-6 h-6 rounded-full overflow-hidden"
380
+ aria-hidden="true"
381
+ >
382
+ {resolvedLeadingIcon}
383
+ </span>
384
+ ) : (
385
+ <span
386
+ key="leading-icon"
387
+ className={cn(
388
+ "flex items-center justify-center shrink-0 w-4.5 h-4.5 [&_.md-icon]:text-[length:inherit]!",
389
+ leadingIconColorClass,
390
+ )}
391
+ style={{ fontSize: 18 }}
392
+ aria-hidden="true"
393
+ >
394
+ {resolvedLeadingIcon}
395
+ </span>
396
+ )
397
+ ) : null}
398
+ </AnimatePresence>
399
+
400
+ <span className="whitespace-nowrap">{label}</span>
401
+ </>
402
+ );
403
+
404
+ return (
405
+ <LazyMotion features={domMax} strict>
406
+ <Root
407
+ ref={(!isCompound ? ref : undefined) as React.Ref<HTMLButtonElement>}
408
+ type={!isCompound ? "button" : undefined}
409
+ // Filter chips act as checkboxes; Input chips can be buttons/toggleable; others buttons
410
+ // Source: Material Design 3 Accessibility
411
+ {...(isFilter
412
+ ? {
413
+ role: "checkbox" as React.AriaRole,
414
+ "aria-checked": selected,
415
+ }
416
+ : isInput && !isCompound && selected
417
+ ? { role: "button", "aria-pressed": true }
418
+ : isCompound
419
+ ? { role: "group" }
420
+ : { role: "button" })}
421
+ aria-disabled={disabled || undefined}
422
+ tabIndex={isCompound ? -1 : disabled ? -1 : 0}
423
+ disabled={!isCompound ? disabled : undefined}
424
+ onClick={!isCompound ? onClick : undefined}
425
+ onPointerDown={!isCompound ? onPointerDown : undefined}
426
+ className={containerClass}
427
+ // Filter out props that shouldn't be on a div if isCompound
428
+ {...(isCompound ? {} : props)}
429
+ >
430
+ {/* State Ripple layer */}
431
+ {!isCompound && (
432
+ <Ripple ripples={ripples} onRippleDone={removeRipple} />
433
+ )}
434
+
435
+ {/* Main action area: if compound, this is a nested button for a11y & avoids nesting buttons */}
436
+ {isCompound ? (
437
+ <button
438
+ ref={ref}
439
+ type="button"
440
+ tabIndex={disabled ? -1 : 0}
441
+ disabled={disabled}
442
+ onClick={onClick}
443
+ onPointerDown={onPointerDown}
444
+ className={cn(
445
+ "flex items-center h-full grow focus:outline-none appearance-none bg-transparent border-none",
446
+ "text-inherit font-inherit cursor-pointer",
447
+ // Move padding here; keep horizontal padding but remove trailing to fit next to X
448
+ paddingClass,
449
+ "gap-2",
450
+ "pr-0",
451
+ // Re-apply focus ring to internal button instead of root div
452
+ "focus-visible:ring-2 focus-visible:ring-m3-secondary focus-visible:ring-inset",
453
+ "rounded-l-[7px] rounded-r-none",
454
+ // State layer for main area
455
+ "relative overflow-hidden",
456
+ "before:absolute before:-inset-px before:pointer-events-none before:bg-current",
457
+ "before:transition-opacity before:duration-200 before:opacity-0 before:rounded-l-[7px]",
458
+ "hover:before:opacity-[0.08] focus-visible:before:opacity-[0.10] active:before:opacity-[0.10]",
459
+ )}
460
+ >
461
+ <Ripple ripples={ripples} onRippleDone={removeRipple} />
462
+ {mainContent}
463
+ </button>
464
+ ) : (
465
+ mainContent
466
+ )}
467
+
468
+ {/* Trailing slot */}
469
+ {hasTrailingContent && (
470
+ <span className="flex items-center justify-center shrink-0">
471
+ {onRemove ? (
472
+ <button
473
+ type="button"
474
+ tabIndex={disabled ? -1 : 0}
475
+ aria-label={
476
+ typeof label === "string" ? `Remove ${label}` : "Remove"
477
+ }
478
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
479
+ e.stopPropagation();
480
+ onRemove(e);
481
+ }}
482
+ onPointerDown={(e: React.PointerEvent<HTMLButtonElement>) =>
483
+ e.stopPropagation()
484
+ }
485
+ className={cn(
486
+ "flex items-center justify-center w-8.5 h-full", // 18px icon + 8px left + 8px right padding
487
+ "cursor-pointer focus-visible:outline-none",
488
+ "transition-all duration-150",
489
+ "relative overflow-hidden rounded-r-[7px] rounded-l-none",
490
+ "before:absolute before:-inset-px before:pointer-events-none before:bg-current",
491
+ "before:transition-opacity before:duration-200 before:opacity-0 before:rounded-r-[7px]",
492
+ "hover:before:opacity-[0.08] active:before:opacity-[0.12]",
493
+ // TrailingIcon color tokens
494
+ selected
495
+ ? "text-m3-on-secondary-container"
496
+ : "text-m3-on-surface-variant",
497
+ )}
498
+ >
499
+ <CloseIcon />
500
+ </button>
501
+ ) : trailingIcon ? (
502
+ <span
503
+ className={cn(
504
+ "flex items-center justify-center w-4.5 h-4.5 [&_.md-icon]:text-[length:inherit]!",
505
+ selected
506
+ ? "text-m3-on-secondary-container"
507
+ : "text-m3-on-surface-variant",
508
+ )}
509
+ style={{ fontSize: 18 }}
510
+ aria-hidden="true"
511
+ >
512
+ {trailingIcon}
513
+ </span>
514
+ ) : null}
515
+ </span>
516
+ )}
517
+ </Root>
518
+ </LazyMotion>
519
+ );
520
+ },
521
+ );
522
+
523
+ ChipImpl.displayName = "Chip";
524
+
525
+ /**
526
+ * MD3 Expressive Chip — 4-variant interactive tag component.
527
+ *
528
+ * @remarks
529
+ * - `filter` chips accept `selected` and render an animated checkmark.
530
+ * - `input` chips accept `onRemove` to render a compound close button.
531
+ * - `elevated` is supported on `assist`, `filter` (unselected), and `suggestion`.
532
+ * - Fully accessible: `role="checkbox"` for filter, `role="group"` for compound chips.
533
+ *
534
+ * @example
535
+ * ```tsx
536
+ * // Assist chip
537
+ * <Chip variant="assist" label="Share" onClick={share} />
538
+ *
539
+ * // Filter chip
540
+ * <Chip variant="filter" label="Unread" selected={showUnread} onClick={toggle} />
541
+ *
542
+ * // Input chip with remove
543
+ * <Chip variant="input" label="React" onRemove={() => removeTag("React")} />
544
+ * ```
545
+ *
546
+ * @see https://m3.material.io/components/chips/overview
547
+ */
548
+ export const Chip = React.memo(ChipImpl);