@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,525 @@
1
+ /**
2
+ * @file checkbox.tsx
3
+ * MD3 Expressive Checkbox — 2-state and tri-state support.
4
+ * Spec: https://m3.material.io/components/checkbox/overview
5
+ */
6
+
7
+ import {
8
+ AnimatePresence,
9
+ domMax,
10
+ LazyMotion,
11
+ m,
12
+ useReducedMotion,
13
+ } from "motion/react";
14
+ import * as React from "react";
15
+ import { cn } from "../lib/utils";
16
+ import { Ripple, type RippleOrigin } from "./ripple";
17
+
18
+ // ─── Constants ────────────────────────────────────────────────────────────────
19
+
20
+ /** MD3 Standard easing: checkmark draw and path morph. */
21
+ const MD3_STANDARD = [0.2, 0, 0, 1] as const;
22
+
23
+ /** MD3 FastEffects easing: container fill (emphasizedAccelerate). */
24
+ const MD3_FAST_EFFECTS = [0.3, 0, 1, 1] as const;
25
+
26
+ /**
27
+ * SVG paths share the same command count (M→L→L) for Framer Motion d-morph.
28
+ * viewBox: 18×18
29
+ */
30
+ const CHECKMARK_PATH = "M 4.5 9.5 L 7.5 12.5 L 13.5 5.5";
31
+ const DASH_PATH = "M 4.5 9 L 9 9 L 13.5 9";
32
+
33
+ // ─── Types ────────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Tri-state value: `"unchecked"` | `"checked"` | `"indeterminate"`.
37
+ */
38
+ export type CheckboxState = "unchecked" | "checked" | "indeterminate";
39
+
40
+ /**
41
+ * Props for `Checkbox`. Supports boolean (`checked`/`onCheckedChange`)
42
+ * and tri-state (`state`/`onStateChange`) modes.
43
+ */
44
+ export interface CheckboxProps {
45
+ /** Controlled checked value (2-state mode). */
46
+ checked?: boolean;
47
+ /** Initial value for uncontrolled mode. @default false */
48
+ defaultChecked?: boolean;
49
+ /** Forces indeterminate rendering regardless of `checked`. */
50
+ indeterminate?: boolean;
51
+ /** Fired on checked change (simple mode). Not called when disabled. */
52
+ onCheckedChange?: (checked: boolean) => void;
53
+
54
+ /** Controlled tri-state value. Takes priority over `checked`/`indeterminate`. */
55
+ state?: CheckboxState;
56
+ /** Fired on tri-state change. Cycles: unchecked → checked → indeterminate. */
57
+ onStateChange?: (state: CheckboxState) => void;
58
+
59
+ /** Disables interaction and applies 0.38 opacity. */
60
+ disabled?: boolean;
61
+ /** Error state — changes colors to `m3-error` and sets `aria-invalid`. */
62
+ error?: boolean;
63
+
64
+ /** Adjacent label text. Wraps checkbox + span in `<label>`. */
65
+ label?: string;
66
+ "aria-label"?: string;
67
+ "aria-labelledby"?: string;
68
+ "aria-describedby"?: string;
69
+ "aria-required"?: boolean;
70
+
71
+ /** Passed to the hidden `<input>` for form submission. */
72
+ name?: string;
73
+ /** Passed to the hidden `<input>` for form submission. */
74
+ value?: string;
75
+ /** ID for the hidden `<input>`. Auto-generated when `label` is set. */
76
+ id?: string;
77
+
78
+ /** Extra class names on the outermost wrapper. */
79
+ className?: string;
80
+ /** Ref pointing to the hidden `<input type="checkbox">`. */
81
+ ref?: React.Ref<HTMLInputElement>;
82
+ }
83
+
84
+ /**
85
+ * `TriStateCheckbox` props — requires `state` + `onStateChange`.
86
+ */
87
+ export interface TriStateCheckboxProps
88
+ extends Omit<
89
+ CheckboxProps,
90
+ "checked" | "defaultChecked" | "onCheckedChange"
91
+ > {
92
+ state: CheckboxState;
93
+ onStateChange: (state: CheckboxState) => void;
94
+ }
95
+
96
+ // ─── State helpers ────────────────────────────────────────────────────────────
97
+
98
+ /** Priority: `state` prop > `indeterminate` > `checked`. @internal */
99
+ function resolveState(
100
+ checked?: boolean,
101
+ indeterminate?: boolean,
102
+ state?: CheckboxState,
103
+ ): CheckboxState {
104
+ if (state !== undefined) return state;
105
+ if (indeterminate) return "indeterminate";
106
+ return checked ? "checked" : "unchecked";
107
+ }
108
+
109
+ /** Tri-state cycle: unchecked → checked → indeterminate → unchecked. */
110
+ const NEXT_STATE: Record<CheckboxState, CheckboxState> = {
111
+ unchecked: "checked",
112
+ checked: "indeterminate",
113
+ indeterminate: "unchecked",
114
+ };
115
+
116
+ // ─── Internal subcomponents ───────────────────────────────────────────────────
117
+
118
+ interface CheckboxVisualProps {
119
+ isSelected: boolean;
120
+ isIndeterminate: boolean;
121
+ containerBg: string;
122
+ containerBorderColor: string;
123
+ containerBorderWidth: number;
124
+ iconColor: string;
125
+ svgPath: string;
126
+ pathLength: number;
127
+ fillDuration: number;
128
+ drawDuration: number;
129
+ morphDuration: number;
130
+ prefersReduced: boolean;
131
+ }
132
+
133
+ /** Animated 18×18dp checkbox box (container + SVG icon). @internal */
134
+ const CheckboxVisual = React.memo(function CheckboxVisual({
135
+ isSelected,
136
+ isIndeterminate,
137
+ containerBg,
138
+ containerBorderColor,
139
+ containerBorderWidth,
140
+ iconColor,
141
+ svgPath,
142
+ pathLength,
143
+ fillDuration,
144
+ drawDuration,
145
+ morphDuration,
146
+ prefersReduced,
147
+ }: CheckboxVisualProps) {
148
+ return (
149
+ <m.div
150
+ aria-hidden="true"
151
+ className="relative flex items-center justify-center w-4.5 h-4.5 rounded-sm overflow-hidden"
152
+ animate={{
153
+ backgroundColor: containerBg,
154
+ borderColor: containerBorderColor,
155
+ borderWidth: containerBorderWidth,
156
+ }}
157
+ transition={{
158
+ backgroundColor: {
159
+ duration: fillDuration,
160
+ ease: isSelected ? MD3_FAST_EFFECTS : "easeOut",
161
+ },
162
+ borderColor: { duration: fillDuration, ease: "easeOut" },
163
+ borderWidth: { duration: fillDuration, ease: "easeOut" },
164
+ }}
165
+ style={{ borderStyle: "solid" }}
166
+ >
167
+ <AnimatePresence>
168
+ {isSelected && (
169
+ <m.svg
170
+ key="icon"
171
+ viewBox="0 0 18 18"
172
+ fill="none"
173
+ strokeLinecap="round"
174
+ strokeLinejoin="round"
175
+ width={18}
176
+ height={18}
177
+ initial={{ opacity: 0 }}
178
+ animate={{ opacity: 1 }}
179
+ exit={{ opacity: 0 }}
180
+ transition={{ duration: prefersReduced ? 0 : 0.1 }}
181
+ aria-hidden="true"
182
+ >
183
+ <m.path
184
+ d={svgPath}
185
+ stroke={iconColor}
186
+ strokeWidth={2}
187
+ animate={{
188
+ d: svgPath,
189
+ pathLength: isIndeterminate ? 1 : pathLength,
190
+ }}
191
+ initial={{ pathLength: 0 }}
192
+ transition={{
193
+ d: { duration: morphDuration, ease: MD3_STANDARD },
194
+ pathLength: { duration: drawDuration, ease: MD3_STANDARD },
195
+ }}
196
+ />
197
+ </m.svg>
198
+ )}
199
+ </AnimatePresence>
200
+ </m.div>
201
+ );
202
+ });
203
+
204
+ // ─── useMergedRef ─────────────────────────────────────────────────────────────
205
+
206
+ /** Merges external + internal refs into a single callback ref. @internal */
207
+ function useMergedRef<T>(
208
+ externalRef: React.Ref<T> | undefined,
209
+ internalRef: React.RefObject<T | null>,
210
+ ): React.RefCallback<T> {
211
+ return React.useCallback(
212
+ (node: T | null) => {
213
+ (internalRef as React.MutableRefObject<T | null>).current = node;
214
+ if (!externalRef) return;
215
+ if (typeof externalRef === "function") {
216
+ externalRef(node);
217
+ } else {
218
+ (externalRef as React.MutableRefObject<T | null>).current = node;
219
+ }
220
+ },
221
+ [externalRef, internalRef],
222
+ );
223
+ }
224
+
225
+ // ─── Component ────────────────────────────────────────────────────────────────
226
+
227
+ const CheckboxComponent = React.forwardRef<HTMLInputElement, CheckboxProps>(
228
+ (
229
+ {
230
+ checked,
231
+ defaultChecked = false,
232
+ indeterminate = false,
233
+ onCheckedChange,
234
+ state: stateProp,
235
+ onStateChange,
236
+ disabled = false,
237
+ error = false,
238
+ label,
239
+ "aria-label": ariaLabel,
240
+ "aria-labelledby": ariaLabelledby,
241
+ "aria-describedby": ariaDescribedby,
242
+ "aria-required": ariaRequired,
243
+ name,
244
+ value,
245
+ id: idProp,
246
+ className,
247
+ },
248
+ ref,
249
+ ) => {
250
+ const prefersReduced = useReducedMotion() ?? false;
251
+
252
+ const generatedId = React.useId();
253
+ const inputId = idProp ?? (label ? `checkbox-${generatedId}` : undefined);
254
+
255
+ const [internalState, setInternalState] = React.useState<CheckboxState>(
256
+ () => (defaultChecked ? "checked" : "unchecked"),
257
+ );
258
+
259
+ // `state` and `checked` determine controlled vs uncontrolled.
260
+ // `indeterminate` is visual-only and always overrides.
261
+ const isControlled = stateProp !== undefined || checked !== undefined;
262
+ const baseState = isControlled
263
+ ? resolveState(checked, false, stateProp)
264
+ : internalState;
265
+ const effectiveState: CheckboxState = indeterminate
266
+ ? "indeterminate"
267
+ : baseState;
268
+
269
+ // ── Ripple ──────────────────────────────────────────────────────────
270
+ const [ripples, setRipples] = React.useState<RippleOrigin[]>([]);
271
+ const removeRipple = React.useCallback(
272
+ (id: number) => setRipples((prev) => prev.filter((r) => r.id !== id)),
273
+ [],
274
+ );
275
+
276
+ const onPointerDown = React.useCallback(
277
+ (e: React.PointerEvent<HTMLElement>) => {
278
+ if (disabled) return;
279
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
280
+ // Ripple origin offset by 4px to simulate a 40×40 state layer inside 48×48 touch target.
281
+ const x = e.clientX - rect.left - 4;
282
+ const y = e.clientY - rect.top - 4;
283
+ const rippleSize = Math.hypot(40, 40) * 2;
284
+ setRipples((prev) => [
285
+ ...prev,
286
+ { id: Date.now(), x, y, size: rippleSize },
287
+ ]);
288
+ },
289
+ [disabled],
290
+ );
291
+
292
+ // ── Change handler ───────────────────────────────────────────────────
293
+ const handleChange = React.useCallback(
294
+ (e: React.ChangeEvent<HTMLInputElement>) => {
295
+ if (disabled) return;
296
+
297
+ if (stateProp !== undefined) {
298
+ onStateChange?.(NEXT_STATE[effectiveState]);
299
+ } else if (checked !== undefined) {
300
+ onCheckedChange?.(e.target.checked);
301
+ } else {
302
+ const next = NEXT_STATE[effectiveState];
303
+ setInternalState(next);
304
+ if (next === "indeterminate") {
305
+ onStateChange?.(next);
306
+ } else {
307
+ onCheckedChange?.(next === "checked");
308
+ }
309
+ }
310
+ },
311
+ [
312
+ disabled,
313
+ stateProp,
314
+ checked,
315
+ effectiveState,
316
+ onStateChange,
317
+ onCheckedChange,
318
+ ],
319
+ );
320
+
321
+ // ── Sync indeterminate DOM property ──────────────────────────────────
322
+ const inputRef = React.useRef<HTMLInputElement>(null);
323
+ const mergedRef = useMergedRef(ref, inputRef);
324
+
325
+ React.useEffect(() => {
326
+ if (inputRef.current) {
327
+ inputRef.current.indeterminate = effectiveState === "indeterminate";
328
+ }
329
+ }, [effectiveState]);
330
+
331
+ // ── Derived visual state ─────────────────────────────────────────────
332
+ const isChecked = effectiveState === "checked";
333
+ const isIndeterminate = effectiveState === "indeterminate";
334
+ const isSelected = isChecked || isIndeterminate;
335
+ const ariaChecked = isIndeterminate ? ("mixed" as const) : isChecked;
336
+
337
+ // ── Animation values ─────────────────────────────────────────────────
338
+ const accentColor = error
339
+ ? "var(--color-m3-error)"
340
+ : "var(--color-m3-primary)";
341
+ const onAccentColor = error
342
+ ? "var(--color-m3-on-error)"
343
+ : "var(--color-m3-on-primary)";
344
+
345
+ // MD3 Outline color (on-surface with 38% opacity)
346
+ // We use a CSS variable or a static RGBA to ensure Framer Motion can animate it.
347
+ // Since color-mix is not animatable, we'll use the variable directly if it's a plain color,
348
+ // or use a fallback. Better yet, we can use the style prop for static colors
349
+ // and only animate opacity if needed, but here we want to animate the color itself.
350
+ const outlineColor = error
351
+ ? "var(--color-m3-error)"
352
+ : "rgba(0, 0, 0, 0.38)"; // Standard fallback for on-surface 38%
353
+
354
+ const containerBg = isSelected ? accentColor : "rgba(0, 0, 0, 0)";
355
+ const containerBorderColor = isSelected ? "rgba(0, 0, 0, 0)" : outlineColor;
356
+ const containerBorderWidth = isSelected ? 0 : 2;
357
+ const iconColor = isSelected ? onAccentColor : "rgba(0, 0, 0, 0)";
358
+
359
+ const svgPath = isIndeterminate ? DASH_PATH : CHECKMARK_PATH;
360
+ const pathLength = isSelected ? 1 : 0;
361
+
362
+ const fillDuration = prefersReduced ? 0 : isSelected ? 0.15 : 0.1;
363
+ const drawDuration = prefersReduced ? 0 : isSelected ? 0.2 : 0.1;
364
+ const morphDuration = prefersReduced ? 0 : 0.2;
365
+
366
+ // ── State layer classes ──────────────────────────────────────────────
367
+ const stateLayerBg = isSelected
368
+ ? error
369
+ ? "before:bg-m3-error"
370
+ : "before:bg-m3-primary"
371
+ : error
372
+ ? "before:bg-m3-error"
373
+ : "before:bg-m3-on-surface";
374
+
375
+ const stateLayerClass = cn(
376
+ "before:absolute before:inset-0 before:rounded-full before:pointer-events-none",
377
+ "before:transition-opacity before:duration-150 before:opacity-0",
378
+ "group-hover/cbx:before:opacity-[0.08]",
379
+ "group-focus-within/cbx:before:opacity-[0.10]",
380
+ "group-active/cbx:before:opacity-[0.10]",
381
+ stateLayerBg,
382
+ );
383
+
384
+ // ── Shared visual props ──────────────────────────────────────────────
385
+ const visualProps: CheckboxVisualProps = {
386
+ isSelected,
387
+ isIndeterminate,
388
+ containerBg,
389
+ containerBorderColor,
390
+ containerBorderWidth,
391
+ iconColor,
392
+ svgPath,
393
+ pathLength,
394
+ fillDuration,
395
+ drawDuration,
396
+ morphDuration,
397
+ prefersReduced,
398
+ };
399
+
400
+ const touchTargetClass = cn(
401
+ "relative inline-flex items-center justify-center outline-none shrink-0",
402
+ "w-12 h-12 group/cbx",
403
+ disabled && "pointer-events-none",
404
+ );
405
+
406
+ // ── Render ───────────────────────────────────────────────────────────
407
+ const stateLayerAndRipple = (
408
+ <div
409
+ className={cn(
410
+ "absolute flex items-center justify-center w-10 h-10 m-auto inset-0 rounded-full overflow-hidden pointer-events-none",
411
+ stateLayerClass,
412
+ )}
413
+ aria-hidden="true"
414
+ >
415
+ <Ripple
416
+ ripples={ripples}
417
+ onRippleDone={removeRipple}
418
+ disabled={disabled}
419
+ />
420
+ </div>
421
+ );
422
+
423
+ const hiddenInput = (
424
+ <input
425
+ ref={mergedRef}
426
+ type="checkbox"
427
+ id={inputId}
428
+ name={name}
429
+ value={value}
430
+ checked={isChecked}
431
+ disabled={disabled}
432
+ aria-checked={ariaChecked}
433
+ aria-disabled={disabled || undefined}
434
+ aria-invalid={error || undefined}
435
+ aria-label={ariaLabel}
436
+ aria-labelledby={ariaLabelledby}
437
+ aria-describedby={ariaDescribedby}
438
+ aria-required={ariaRequired}
439
+ onChange={handleChange}
440
+ className="sr-only"
441
+ />
442
+ );
443
+
444
+ if (label) {
445
+ return (
446
+ <LazyMotion features={domMax} strict>
447
+ <label
448
+ htmlFor={inputId}
449
+ className={cn(
450
+ "inline-flex items-center gap-2 cursor-pointer select-none",
451
+ disabled &&
452
+ "cursor-not-allowed opacity-[0.38] pointer-events-none",
453
+ className,
454
+ )}
455
+ >
456
+ <div className={touchTargetClass} onPointerDown={onPointerDown}>
457
+ {stateLayerAndRipple}
458
+ {hiddenInput}
459
+ <CheckboxVisual {...visualProps} />
460
+ </div>
461
+ <span className="text-sm leading-none text-m3-on-surface">
462
+ {label}
463
+ </span>
464
+ </label>
465
+ </LazyMotion>
466
+ );
467
+ }
468
+
469
+ return (
470
+ <LazyMotion features={domMax} strict>
471
+ <label
472
+ htmlFor={inputId}
473
+ className={cn(touchTargetClass, "cursor-pointer", className)}
474
+ onPointerDown={onPointerDown}
475
+ >
476
+ {stateLayerAndRipple}
477
+ {hiddenInput}
478
+ <CheckboxVisual {...visualProps} />
479
+ </label>
480
+ </LazyMotion>
481
+ );
482
+ },
483
+ );
484
+
485
+ CheckboxComponent.displayName = "Checkbox";
486
+
487
+ /**
488
+ * MD3 Expressive Checkbox component.
489
+ *
490
+ * Supports 2-state and tri-state patterns. Fully animated per MD3 spec:
491
+ * checkmark draw, indeterminate dash morph, container fill, state layer, and ripple.
492
+ *
493
+ * @example
494
+ * ```tsx
495
+ * <Checkbox checked={isChecked} onCheckedChange={setIsChecked} label="Accept terms" />
496
+ * <Checkbox state={parentState} onStateChange={setParentState} label="Select all" />
497
+ * <Checkbox error label="Required field" aria-describedby="err-msg" />
498
+ * ```
499
+ * @see https://m3.material.io/components/checkbox/overview
500
+ */
501
+ export const Checkbox = React.memo(CheckboxComponent);
502
+
503
+ // ─── TriStateCheckbox ─────────────────────────────────────────────────────────
504
+
505
+ const TriStateCheckboxComponent = React.forwardRef<
506
+ HTMLInputElement,
507
+ TriStateCheckboxProps
508
+ >(({ state, onStateChange, ...rest }, ref) => (
509
+ <Checkbox ref={ref} state={state} onStateChange={onStateChange} {...rest} />
510
+ ));
511
+
512
+ TriStateCheckboxComponent.displayName = "TriStateCheckbox";
513
+
514
+ /**
515
+ * MD3 Expressive Tri-State Checkbox.
516
+ *
517
+ * Convenience wrapper around `Checkbox` that enforces `state` + `onStateChange`.
518
+ * Ideal for parent-child selection patterns.
519
+ *
520
+ * @example
521
+ * ```tsx
522
+ * <TriStateCheckbox state={parentState} onStateChange={setParentState} label="Select all" />
523
+ * ```
524
+ */
525
+ export const TriStateCheckbox = React.memo(TriStateCheckboxComponent);