@justanarthur/modo-ui 0.5.0 → 0.6.0-canary.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 (461) hide show
  1. package/DESIGN.md +608 -0
  2. package/README.md +61 -39
  3. package/blocks/sidebar-app/examples.mdx +33 -0
  4. package/blocks/sidebar-app/index.tsx +218 -0
  5. package/blocks/sidebar-app/inset-topbar.tsx +25 -0
  6. package/blocks/sidebar-app/menu-grid.ts +26 -0
  7. package/blocks/sidebar-app/nav-data.ts +36 -0
  8. package/blocks/sidebar-app/search-field.tsx +48 -0
  9. package/blocks/sidebar-app/user-footer.tsx +67 -0
  10. package/blocks/sidebar-app/workspace-header.tsx +129 -0
  11. package/components/AI/input-message/examples.mdx +124 -0
  12. package/components/AI/input-message/file-thumbnail.tsx +173 -0
  13. package/components/AI/input-message/index.tsx +1383 -0
  14. package/components/AI/thinking-indicator/examples.mdx +27 -0
  15. package/components/AI/thinking-indicator/index.tsx +210 -0
  16. package/components/accordion/examples.mdx +148 -0
  17. package/components/accordion/index.tsx +1034 -0
  18. package/components/avatar/examples.mdx +95 -0
  19. package/components/avatar/index.tsx +129 -0
  20. package/components/badge/change-demo.tsx +29 -0
  21. package/components/badge/examples.mdx +120 -0
  22. package/components/badge/index.tsx +225 -0
  23. package/components/badge/variants.ts +73 -0
  24. package/components/button/examples.mdx +164 -0
  25. package/components/button/index.tsx +416 -0
  26. package/components/button/loading-demo.tsx +25 -0
  27. package/components/button/variants.ts +166 -0
  28. package/components/card/examples.mdx +212 -0
  29. package/components/card/index.tsx +1037 -0
  30. package/components/copy/examples.mdx +87 -0
  31. package/components/copy/index.tsx +482 -0
  32. package/components/inputs/checkbox/examples.mdx +83 -0
  33. package/components/inputs/checkbox/index.tsx +611 -0
  34. package/components/inputs/checkbox/select-all-demo.tsx +48 -0
  35. package/components/inputs/color-picker/examples.mdx +72 -0
  36. package/components/inputs/color-picker/index.tsx +1955 -0
  37. package/components/inputs/color-picker/variants.ts +401 -0
  38. package/components/inputs/combobox/examples.mdx +273 -0
  39. package/components/inputs/combobox/index.tsx +1436 -0
  40. package/components/inputs/combobox/remote-demo.tsx +101 -0
  41. package/components/inputs/date-time/calendar/examples.mdx +34 -0
  42. package/components/inputs/date-time/calendar/index.tsx +351 -0
  43. package/components/inputs/date-time/calendar/use-month-window.ts +163 -0
  44. package/components/inputs/date-time/date-picker/examples.mdx +166 -0
  45. package/components/inputs/date-time/date-picker/index.tsx +385 -0
  46. package/components/inputs/date-time/date-picker/masked-date-input.tsx +336 -0
  47. package/components/inputs/date-time/date-range-picker/examples.mdx +29 -0
  48. package/components/inputs/date-time/date-range-picker/index.tsx +217 -0
  49. package/components/inputs/date-time/duration-picker/examples.mdx +16 -0
  50. package/components/inputs/date-time/duration-picker/index.tsx +189 -0
  51. package/components/inputs/date-time/month-picker/examples.mdx +16 -0
  52. package/components/inputs/date-time/month-picker/index.tsx +190 -0
  53. package/components/inputs/date-time/picker-shell.tsx +395 -0
  54. package/components/inputs/date-time/time-columns.tsx +91 -0
  55. package/components/inputs/date-time/time-picker/examples.mdx +26 -0
  56. package/components/inputs/date-time/time-picker/index.tsx +166 -0
  57. package/components/inputs/field/examples.mdx +113 -0
  58. package/components/inputs/field/index.tsx +267 -0
  59. package/components/inputs/file-dropzone/examples.mdx +73 -0
  60. package/components/inputs/file-dropzone/file-row.tsx +201 -0
  61. package/components/inputs/file-dropzone/index.tsx +296 -0
  62. package/components/inputs/file-dropzone/upload-demo.tsx +67 -0
  63. package/components/inputs/file-dropzone/variants.ts +94 -0
  64. package/components/inputs/input/examples.mdx +135 -0
  65. package/components/inputs/input/index.tsx +494 -0
  66. package/components/inputs/input/password-demo.tsx +30 -0
  67. package/components/inputs/label/examples.mdx +67 -0
  68. package/components/inputs/label/index.tsx +85 -0
  69. package/components/inputs/number-input/examples.mdx +56 -0
  70. package/components/inputs/number-input/index.tsx +214 -0
  71. package/components/inputs/radio/examples.mdx +77 -0
  72. package/components/inputs/radio/index.tsx +636 -0
  73. package/components/inputs/search/examples.mdx +74 -0
  74. package/components/inputs/search/index.tsx +333 -0
  75. package/components/inputs/select/examples.mdx +218 -0
  76. package/components/inputs/select/index.tsx +862 -0
  77. package/components/inputs/select/variants.ts +32 -0
  78. package/components/inputs/slider/examples.mdx +127 -0
  79. package/components/inputs/slider/index.tsx +1725 -0
  80. package/components/inputs/switch/examples.mdx +94 -0
  81. package/components/inputs/switch/index.tsx +504 -0
  82. package/components/inputs/textarea/examples.mdx +88 -0
  83. package/components/inputs/textarea/index.tsx +161 -0
  84. package/components/navigation/navigation-menu/examples.mdx +109 -0
  85. package/components/navigation/navigation-menu/index.tsx +613 -0
  86. package/components/navigation/pagination/examples.mdx +64 -0
  87. package/components/navigation/pagination/index.tsx +395 -0
  88. package/components/navigation/sidebar/examples.mdx +319 -0
  89. package/components/navigation/sidebar/index.tsx +453 -0
  90. package/components/navigation/sidebar/sidebar-core.tsx +1408 -0
  91. package/components/navigation/sidebar/sidebar-menu.tsx +1495 -0
  92. package/components/navigation/sidebar/sidebar.css +74 -0
  93. package/components/navigation/sidebar/variants.ts +55 -0
  94. package/components/navigation/tabs/examples.mdx +89 -0
  95. package/components/navigation/tabs/index.tsx +494 -0
  96. package/components/navigation/tabs-subtle/examples.mdx +49 -0
  97. package/components/navigation/tabs-subtle/index.tsx +512 -0
  98. package/components/overlays/alert-dialog/examples.mdx +123 -0
  99. package/components/overlays/alert-dialog/index.tsx +258 -0
  100. package/components/overlays/alert-dialog/pending-demo.tsx +36 -0
  101. package/components/overlays/command/examples.mdx +113 -0
  102. package/components/overlays/command/index.tsx +416 -0
  103. package/components/overlays/context-menu/examples.mdx +56 -0
  104. package/components/overlays/context-menu/index.tsx +279 -0
  105. package/components/overlays/dialog/examples.mdx +144 -0
  106. package/components/overlays/dialog/index.tsx +366 -0
  107. package/components/overlays/dropdown/dropdown-search.tsx +395 -0
  108. package/components/overlays/dropdown/examples.mdx +294 -0
  109. package/components/overlays/dropdown/index.tsx +1168 -0
  110. package/components/overlays/dropdown/menu-item.tsx +464 -0
  111. package/components/overlays/dropdown/menu-parts.tsx +660 -0
  112. package/components/overlays/popover/anchored-demo.tsx +59 -0
  113. package/components/overlays/popover/examples.mdx +180 -0
  114. package/components/overlays/popover/index.tsx +315 -0
  115. package/components/overlays/preview-card/examples.mdx +61 -0
  116. package/components/overlays/preview-card/index.tsx +204 -0
  117. package/components/overlays/sheet/examples.mdx +89 -0
  118. package/components/overlays/sheet/index.tsx +247 -0
  119. package/components/overlays/toast/examples.mdx +68 -0
  120. package/components/overlays/toast/global-demo.tsx +72 -0
  121. package/components/overlays/toast/index.tsx +474 -0
  122. package/components/overlays/toast/manager.ts +193 -0
  123. package/components/overlays/tooltip/examples.mdx +106 -0
  124. package/components/overlays/tooltip/index.tsx +519 -0
  125. package/components/progress/examples.mdx +59 -0
  126. package/components/progress/index.tsx +174 -0
  127. package/components/progress/progress.css +49 -0
  128. package/components/progress/step-demo.tsx +27 -0
  129. package/components/table/examples.mdx +184 -0
  130. package/components/table/index.tsx +510 -0
  131. package/components/table/sort-demo.tsx +61 -0
  132. package/dist/base.css +100 -0
  133. package/dist/blocks/sidebar-app/index.js +39 -26
  134. package/dist/components/AI/input-message/index.js +22 -16
  135. package/dist/components/AI/thinking-indicator/index.js +9 -6
  136. package/dist/components/accordion/index.js +16 -13
  137. package/dist/components/avatar/index.d.ts +46 -0
  138. package/dist/components/avatar/index.js +18 -0
  139. package/dist/components/badge/index.d.ts +18 -11
  140. package/dist/components/badge/index.js +11 -7
  141. package/dist/components/badge/variants.d.ts +33 -0
  142. package/dist/components/badge/variants.js +10 -0
  143. package/dist/components/button/index.d.ts +53 -17
  144. package/dist/components/button/index.js +10 -6
  145. package/dist/components/button/variants.d.ts +39 -0
  146. package/dist/components/button/variants.js +8 -0
  147. package/dist/components/card/index.d.ts +16 -3
  148. package/dist/components/card/index.js +16 -11
  149. package/dist/components/copy/index.js +13 -9
  150. package/dist/components/inputs/checkbox/index.d.ts +28 -9
  151. package/dist/components/inputs/checkbox/index.js +17 -13
  152. package/dist/components/inputs/color-picker/index.d.ts +13 -13
  153. package/dist/components/inputs/color-picker/index.js +25 -18
  154. package/dist/components/inputs/color-picker/variants.d.ts +61 -0
  155. package/dist/components/inputs/color-picker/variants.js +28 -0
  156. package/dist/components/inputs/combobox/index.d.ts +60 -17
  157. package/dist/components/inputs/combobox/index.js +26 -18
  158. package/dist/components/inputs/date-time/calendar/index.js +14 -11
  159. package/dist/components/inputs/date-time/date-picker/index.d.ts +53 -11
  160. package/dist/components/inputs/date-time/date-picker/index.js +33 -25
  161. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -3
  162. package/dist/components/inputs/date-time/date-range-picker/index.js +32 -24
  163. package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -3
  164. package/dist/components/inputs/date-time/duration-picker/index.js +32 -24
  165. package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -3
  166. package/dist/components/inputs/date-time/month-picker/index.js +31 -23
  167. package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -3
  168. package/dist/components/inputs/date-time/time-picker/index.js +32 -24
  169. package/dist/components/inputs/field/index.d.ts +116 -0
  170. package/dist/components/inputs/field/index.js +28 -0
  171. package/dist/components/inputs/file-dropzone/index.d.ts +165 -0
  172. package/dist/components/inputs/file-dropzone/index.js +32 -0
  173. package/dist/components/inputs/file-dropzone/variants.d.ts +35 -0
  174. package/dist/components/inputs/file-dropzone/variants.js +12 -0
  175. package/dist/components/inputs/input/index.d.ts +95 -13
  176. package/dist/components/inputs/input/index.js +17 -9
  177. package/dist/components/inputs/label/index.d.ts +32 -0
  178. package/dist/components/inputs/label/index.js +11 -0
  179. package/dist/components/inputs/number-input/index.d.ts +84 -0
  180. package/dist/components/inputs/number-input/index.js +20 -0
  181. package/dist/components/inputs/radio/index.d.ts +20 -5
  182. package/dist/components/inputs/radio/index.js +15 -12
  183. package/dist/components/inputs/search/index.js +25 -18
  184. package/dist/components/inputs/select/index.d.ts +41 -11
  185. package/dist/components/inputs/select/index.js +27 -14
  186. package/dist/components/inputs/select/variants.d.ts +5 -0
  187. package/dist/components/inputs/select/variants.js +6 -0
  188. package/dist/components/inputs/slider/index.js +7 -5
  189. package/dist/components/inputs/switch/index.d.ts +8 -2
  190. package/dist/components/inputs/switch/index.js +14 -12
  191. package/dist/components/inputs/textarea/index.d.ts +4 -0
  192. package/dist/components/inputs/textarea/index.js +8 -6
  193. package/dist/components/navigation/navigation-menu/index.d.ts +114 -0
  194. package/dist/components/navigation/navigation-menu/index.js +40 -0
  195. package/dist/components/navigation/pagination/index.d.ts +73 -0
  196. package/dist/components/navigation/pagination/index.js +39 -0
  197. package/dist/components/navigation/sidebar/index.d.ts +44 -21
  198. package/dist/components/navigation/sidebar/index.js +41 -31
  199. package/dist/components/navigation/sidebar/variants.d.ts +25 -0
  200. package/dist/components/navigation/sidebar/variants.js +28 -0
  201. package/dist/components/navigation/tabs/index.d.ts +8 -2
  202. package/dist/components/navigation/tabs/index.js +14 -10
  203. package/dist/components/navigation/tabs-subtle/index.js +12 -10
  204. package/dist/components/overlays/alert-dialog/index.d.ts +121 -15
  205. package/dist/components/overlays/alert-dialog/index.js +22 -13
  206. package/dist/components/overlays/command/index.js +24 -17
  207. package/dist/components/overlays/context-menu/index.d.ts +33 -17
  208. package/dist/components/overlays/context-menu/index.js +27 -18
  209. package/dist/components/overlays/dialog/index.js +17 -12
  210. package/dist/components/overlays/dropdown/index.d.ts +257 -31
  211. package/dist/components/overlays/dropdown/index.js +42 -17
  212. package/dist/components/overlays/popover/index.d.ts +31 -9
  213. package/dist/components/overlays/popover/index.js +12 -9
  214. package/dist/components/overlays/preview-card/index.js +11 -8
  215. package/dist/components/overlays/sheet/index.js +18 -13
  216. package/dist/components/overlays/toast/index.d.ts +123 -47
  217. package/dist/components/overlays/toast/index.js +18 -10
  218. package/dist/components/overlays/tooltip/index.d.ts +113 -21
  219. package/dist/components/overlays/tooltip/index.js +15 -6
  220. package/dist/components/progress/index.d.ts +56 -0
  221. package/dist/components/progress/index.js +15 -0
  222. package/dist/components/table/index.d.ts +63 -8
  223. package/dist/components/table/index.js +19 -9
  224. package/dist/index.d.ts +2201 -736
  225. package/dist/index.js +223 -82
  226. package/dist/lib/date-locale.d.ts +14 -0
  227. package/dist/lib/date-locale.js +16 -0
  228. package/dist/lib/field-classes.d.ts +144 -0
  229. package/dist/lib/field-classes.js +40 -0
  230. package/dist/lib/field-variants.d.ts +47 -0
  231. package/dist/lib/field-variants.js +26 -0
  232. package/dist/lib/fluid-hover-highlight.d.ts +112 -0
  233. package/dist/lib/fluid-hover-highlight.js +17 -0
  234. package/dist/lib/focus-ring.d.ts +18 -0
  235. package/dist/lib/focus-ring.js +10 -0
  236. package/dist/lib/goo-indicator.d.ts +46 -0
  237. package/dist/lib/goo-indicator.js +14 -0
  238. package/dist/lib/icon-context.d.ts +30 -0
  239. package/dist/lib/icon-context.js +12 -0
  240. package/dist/lib/inert.d.ts +9 -0
  241. package/dist/lib/inert.js +8 -0
  242. package/dist/lib/morph-layers.d.ts +93 -0
  243. package/dist/lib/morph-layers.js +13 -0
  244. package/dist/lib/morph-part.d.ts +21 -0
  245. package/dist/lib/morph-part.js +12 -0
  246. package/dist/lib/popup.d.ts +20 -0
  247. package/dist/lib/popup.js +12 -0
  248. package/dist/lib/reduced-motion.d.ts +9 -0
  249. package/dist/lib/reduced-motion.js +6 -0
  250. package/dist/lib/row-index.d.ts +10 -0
  251. package/dist/lib/row-index.js +8 -0
  252. package/dist/lib/row-order.d.ts +28 -0
  253. package/dist/lib/row-order.js +10 -0
  254. package/dist/lib/scroll-column.d.ts +27 -0
  255. package/dist/lib/scroll-column.js +18 -0
  256. package/dist/lib/shape-context.d.ts +30 -0
  257. package/dist/lib/shape-context.js +12 -0
  258. package/dist/lib/size-context.d.ts +94 -0
  259. package/dist/lib/size-context.js +20 -0
  260. package/dist/lib/size-scale.d.ts +79 -0
  261. package/dist/lib/size-scale.js +8 -0
  262. package/dist/lib/slot.d.ts +12 -0
  263. package/dist/lib/slot.js +6 -0
  264. package/dist/lib/springs.d.ts +44 -0
  265. package/dist/lib/springs.js +8 -0
  266. package/dist/lib/surface-classes.d.ts +19 -0
  267. package/dist/lib/surface-classes.js +10 -0
  268. package/dist/lib/surface-context.d.ts +9 -0
  269. package/dist/lib/surface-context.js +8 -0
  270. package/dist/lib/use-controllable-state.d.ts +2 -0
  271. package/dist/lib/use-controllable-state.js +6 -0
  272. package/dist/lib/use-fluid-hover.d.ts +138 -0
  273. package/dist/lib/use-fluid-hover.js +14 -0
  274. package/dist/lib/use-is-mobile.d.ts +2 -0
  275. package/dist/lib/use-is-mobile.js +6 -0
  276. package/dist/lib/use-keyboard-nav-gate.d.ts +20 -0
  277. package/dist/lib/use-keyboard-nav-gate.js +7 -0
  278. package/dist/lib/use-merge-split.d.ts +47 -0
  279. package/dist/lib/use-merge-split.js +15 -0
  280. package/dist/lib/use-morph.d.ts +101 -0
  281. package/dist/lib/use-morph.js +20 -0
  282. package/dist/lib/use-touch-primary.d.ts +2 -0
  283. package/dist/lib/use-touch-primary.js +6 -0
  284. package/dist/lib/utils.d.ts +3 -0
  285. package/dist/lib/utils.js +6 -0
  286. package/dist/preset.d.ts +17 -0
  287. package/dist/preset.js +22 -2
  288. package/dist/primitives/code/index.js +7 -5
  289. package/dist/primitives/craft/index.js +9 -6
  290. package/dist/primitives/fluid-hover/index.js +9 -8
  291. package/dist/primitives/morph/index.js +15 -10
  292. package/dist/primitives/motion/index.js +10 -8
  293. package/dist/primitives/scroll-area/index.js +4 -3
  294. package/dist/primitives/separator/index.d.ts +25 -0
  295. package/dist/primitives/separator/index.js +9 -0
  296. package/dist/primitives/sizes/index.js +6 -4
  297. package/dist/primitives/skeleton/index.d.ts +35 -0
  298. package/dist/primitives/skeleton/index.js +12 -0
  299. package/dist/primitives/surface/index.js +6 -4
  300. package/dist/shared/{components-3mkepka5.js → components-0jmqhx2k.js} +28 -25
  301. package/dist/shared/components-124epb7f.js +53 -0
  302. package/dist/shared/{components-gz8zf3ec.js → components-1k0wv53d.js} +15 -43
  303. package/dist/shared/components-26snnbs9.js +322 -0
  304. package/dist/shared/{components-2dxkv6zm.js → components-2c0qcfx4.js} +8 -7
  305. package/dist/shared/{components-473g814w.js → components-2mt9te6d.js} +243 -137
  306. package/dist/shared/{components-hawvy2qz.js → components-2z48g4fp.js} +9 -30
  307. package/dist/shared/components-34txk9rj.js +35 -0
  308. package/dist/shared/components-3h92d94m.js +23 -0
  309. package/dist/shared/{components-zfnmn5d4.js → components-3sp623ae.js} +36 -31
  310. package/dist/shared/{components-jwst3vtj.js → components-3wxkjgqn.js} +186 -76
  311. package/dist/shared/{components-s2bz1xpk.js → components-49bsynbw.js} +8 -5
  312. package/dist/shared/components-4bwsd46w.js +71 -0
  313. package/dist/shared/components-4n4f5m3e.js +297 -0
  314. package/dist/shared/{components-0q0sqfh4.js → components-4y816hwp.js} +27 -49
  315. package/dist/shared/components-52ngsgca.js +399 -0
  316. package/dist/shared/{components-y9y0g8er.js → components-52r5763g.js} +22 -19
  317. package/dist/shared/components-559tf7yb.js +28 -0
  318. package/dist/shared/components-5mkga9hj.js +440 -0
  319. package/dist/shared/{components-kwa6mrf9.js → components-5s0nfqvw.js} +14 -9
  320. package/dist/shared/{components-dk0gjr5a.js → components-60atskhp.js} +19 -18
  321. package/dist/shared/{components-rr7a6ja9.js → components-658b26nb.js} +21 -18
  322. package/dist/shared/{components-xxvdw7k1.js → components-65vwfjs6.js} +37 -34
  323. package/dist/shared/{components-frw34b65.js → components-6j2fh74k.js} +114 -123
  324. package/dist/shared/components-6zg1yft7.js +167 -0
  325. package/dist/shared/components-6zng858q.js +53 -0
  326. package/dist/shared/components-72w4ge68.js +64 -0
  327. package/dist/shared/{components-cv16mryv.js → components-78x40gee.js} +13 -7
  328. package/dist/shared/{components-b2wyq2cq.js → components-7d4r7hh1.js} +39 -57
  329. package/dist/shared/components-7ntvvegr.js +316 -0
  330. package/dist/shared/components-8nwsqjn8.js +34 -0
  331. package/dist/shared/{components-5xc2j8jz.js → components-8tt3xbb5.js} +19 -18
  332. package/dist/shared/{components-vc7vq5nz.js → components-9as3rf68.js} +6 -3
  333. package/dist/shared/components-9mys9jna.js +255 -0
  334. package/dist/shared/components-adkarj8n.js +35 -0
  335. package/dist/shared/{components-b3c5et5b.js → components-ae9fncz4.js} +32 -31
  336. package/dist/shared/{components-hqeq3w9q.js → components-awra5sff.js} +24 -16
  337. package/dist/shared/components-b1e239g3.js +28 -0
  338. package/dist/shared/{components-q5xj5c16.js → components-b1f2bjf1.js} +9 -8
  339. package/dist/shared/{components-qmmjx1qw.js → components-bn42vq6d.js} +71 -72
  340. package/dist/shared/components-c96g82th.js +1444 -0
  341. package/dist/shared/components-c9yr4ed1.js +44 -0
  342. package/dist/shared/components-d3shbwqq.js +95 -0
  343. package/dist/shared/{components-jjw69c4c.js → components-dcc02hs2.js} +7 -4
  344. package/dist/shared/{components-men63rwj.js → components-de99ykt7.js} +3 -2
  345. package/dist/shared/components-dhgk6mbt.js +16 -0
  346. package/dist/shared/components-drxrtnp2.js +25 -0
  347. package/dist/shared/{components-6p8tfcd6.js → components-ds9fqat9.js} +21 -18
  348. package/dist/shared/{components-mjthm1kf.js → components-egy4tyjt.js} +11 -8
  349. package/dist/shared/components-f0xfpkhh.js +82 -0
  350. package/dist/shared/components-f7h57m6t.js +59 -0
  351. package/dist/shared/{components-d6xtjht3.js → components-fjkpbg6n.js} +33 -30
  352. package/dist/shared/{components-gc5vtq2f.js → components-gh0qeey3.js} +63 -29
  353. package/dist/shared/{components-39jq9va1.js → components-gvbapfen.js} +29 -24
  354. package/dist/shared/{components-5vgpnxgy.js → components-gzesg7hy.js} +19 -16
  355. package/dist/shared/components-h6p86be3.js +19 -0
  356. package/dist/shared/components-h83az0bz.js +33 -0
  357. package/dist/shared/components-jjrj6ymn.js +104 -0
  358. package/dist/shared/{components-wvp1m52g.js → components-jp5v0m0f.js} +15 -14
  359. package/dist/shared/{components-zrkzcayz.js → components-jqpefjsn.js} +54 -6
  360. package/dist/shared/{components-1z6tdadk.js → components-k74j4hwh.js} +20 -17
  361. package/dist/shared/components-kswxnm3h.js +24 -0
  362. package/dist/shared/{components-jbbmdr74.js → components-mkkbbgb7.js} +36 -17
  363. package/dist/shared/{components-pbmqctfa.js → components-mpw667rb.js} +169 -65
  364. package/dist/shared/{components-s5ngg32c.js → components-paa08427.js} +121 -47
  365. package/dist/shared/{components-vfbhxf5r.js → components-pd42emw3.js} +6 -5
  366. package/dist/shared/{components-hw9yk32z.js → components-pmfszv7a.js} +35 -23
  367. package/dist/shared/{components-chacnk61.js → components-pw06rbxq.js} +19 -19
  368. package/dist/shared/components-q03cktx8.js +246 -0
  369. package/dist/shared/{components-kvr7f9hy.js → components-qdm3yh26.js} +19 -10
  370. package/dist/shared/{components-27nnvrwe.js → components-qs4rkqsj.js} +15 -14
  371. package/dist/shared/{components-s083f38z.js → components-r6gbmkew.js} +22 -21
  372. package/dist/shared/{components-ta7shw28.js → components-r8eqs7gz.js} +4 -4
  373. package/dist/shared/{components-sn1rxx1d.js → components-rm02h9nt.js} +12 -11
  374. package/dist/shared/{components-c6s6b43g.js → components-rrbpmfx4.js} +3 -2
  375. package/dist/shared/{components-n7mjr357.js → components-tz1fv35x.js} +3 -2
  376. package/dist/shared/components-tz4gaf1v.js +23 -0
  377. package/dist/shared/{components-zx6b6yby.js → components-vf45j12c.js} +4 -18
  378. package/dist/shared/{components-3ky44908.js → components-vfjc9jzz.js} +2 -3
  379. package/dist/shared/{components-39vmfq9s.js → components-vsa625vk.js} +21 -20
  380. package/dist/shared/components-wawnmkph.js +500 -0
  381. package/dist/shared/components-wktrrb4m.js +38 -0
  382. package/dist/shared/{components-8snj490h.js → components-wwd363me.js} +78 -32
  383. package/dist/shared/{components-5pnw8183.js → components-xcwvgcrd.js} +23 -22
  384. package/dist/shared/components-xdqmt6ed.js +20 -0
  385. package/dist/shared/{components-h2ajmhwn.js → components-y1w3a6wx.js} +107 -35
  386. package/dist/shared/{components-ar66gnt0.js → components-y6578b8h.js} +37 -34
  387. package/dist/shared/{components-59efxjd9.js → components-yhgq0qxx.js} +13 -12
  388. package/dist/shared/{components-jthe1pwb.js → components-yz84gpkq.js} +101 -404
  389. package/dist/shared/components-z3v0n0de.js +11 -0
  390. package/dist/shared/{components-7r3etddy.js → components-z9n19fyk.js} +19 -54
  391. package/dist/shared/components-zbvjzh1p.js +141 -0
  392. package/dist/styles.css +241 -16
  393. package/dist/tailwind.css +13 -0
  394. package/dist/tokens.css +34 -0
  395. package/index.ts +60 -0
  396. package/lib/date-locale.ts +50 -0
  397. package/lib/field-classes.ts +115 -0
  398. package/lib/field-variants.ts +117 -0
  399. package/lib/fluid-hover-highlight.tsx +139 -0
  400. package/lib/focus-ring.tsx +46 -0
  401. package/lib/goo-indicator.tsx +261 -0
  402. package/lib/icon-context.tsx +310 -0
  403. package/lib/inert.ts +22 -0
  404. package/lib/morph-layers.tsx +102 -0
  405. package/lib/morph-part.tsx +49 -0
  406. package/lib/popup.ts +52 -0
  407. package/lib/reduced-motion.ts +19 -0
  408. package/lib/row-index.tsx +29 -0
  409. package/lib/row-order.tsx +115 -0
  410. package/lib/scroll-column.tsx +244 -0
  411. package/lib/shape-context.tsx +124 -0
  412. package/lib/size-context.tsx +66 -0
  413. package/lib/size-scale.ts +115 -0
  414. package/lib/slot.ts +25 -0
  415. package/lib/springs.ts +62 -0
  416. package/lib/surface-classes.ts +58 -0
  417. package/lib/surface-context.tsx +21 -0
  418. package/lib/use-controllable-state.ts +44 -0
  419. package/lib/use-fluid-hover.ts +541 -0
  420. package/lib/use-is-mobile.ts +25 -0
  421. package/lib/use-keyboard-nav-gate.ts +45 -0
  422. package/lib/use-merge-split.tsx +209 -0
  423. package/lib/use-morph.ts +557 -0
  424. package/lib/use-touch-primary.tsx +43 -0
  425. package/lib/utils.ts +37 -0
  426. package/package.json +177 -4
  427. package/preset.ts +196 -0
  428. package/primitives/code/examples.mdx +27 -0
  429. package/primitives/code/index.tsx +80 -0
  430. package/primitives/craft/demos.tsx +173 -0
  431. package/primitives/craft/examples.mdx +138 -0
  432. package/primitives/craft/index.tsx +76 -0
  433. package/primitives/fluid-hover/examples.mdx +84 -0
  434. package/primitives/fluid-hover/index.tsx +316 -0
  435. package/primitives/morph/examples.mdx +135 -0
  436. package/primitives/morph/index.tsx +192 -0
  437. package/primitives/motion/examples.mdx +100 -0
  438. package/primitives/motion/index.tsx +160 -0
  439. package/primitives/scroll-area/examples.mdx +58 -0
  440. package/primitives/scroll-area/index.tsx +200 -0
  441. package/primitives/separator/examples.mdx +30 -0
  442. package/primitives/separator/index.tsx +53 -0
  443. package/primitives/sizes/examples.mdx +167 -0
  444. package/primitives/sizes/index.tsx +89 -0
  445. package/primitives/skeleton/examples.mdx +51 -0
  446. package/primitives/skeleton/index.tsx +72 -0
  447. package/primitives/skeleton/skeleton.css +15 -0
  448. package/primitives/surface/examples.mdx +173 -0
  449. package/primitives/surface/index.tsx +109 -0
  450. package/primitives/surface/surface.css +176 -0
  451. package/dist/shared/components-0sn7ypjw.js +0 -61
  452. package/dist/shared/components-1434p1mh.js +0 -123
  453. package/dist/shared/components-a86dytt0.js +0 -986
  454. package/dist/shared/components-gf5s6ap9.js +0 -42
  455. package/dist/shared/components-hyq23phv.js +0 -310
  456. package/dist/shared/components-ng72k97e.js +0 -128
  457. package/dist/shared/components-raknhppb.js +0 -64
  458. package/dist/shared/components-vynjtrk6.js +0 -19
  459. package/dist/shared/components-wy9jpqny.js +0 -146
  460. /package/dist/shared/{components-1v7rxytx.js → components-3ey0hzpb.js} +0 -0
  461. /package/dist/shared/{components-ezxhrmer.js → components-jcnjay5g.js} +0 -0
@@ -0,0 +1,1034 @@
1
+ // biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
2
+ /*
3
+ * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
4
+ * `registry/base/accordion.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
+ * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
+ * Local modifications:
7
+ * - `"use client"` dropped; `framer-motion` → `motion/react`; `@/lib/*`,
8
+ * `@/hooks/*` and `@/components/ui/fluid-hover-highlight` → `../../lib/*`,
9
+ * except `SizeProvider` → `../../primitives/sizes`.
10
+ * - Open state (Accordion and AccordionGroup) runs through
11
+ * `useControllableState`, the Base UI contract: `value` controls, otherwise
12
+ * the accordion keeps its own state from `defaultValue` and still reports
13
+ * every change to `onValueChange`. Upstream treated `onValueChange` alone
14
+ * as controlled, so an uncontrolled accordion with a listener never opened.
15
+ * - Standalone `Accordion` accepts `highlight` (the FF API table lists it on
16
+ * the root) and hands it to its items through context as their default;
17
+ * `AccordionItem`'s own `highlight` still wins.
18
+ * - `AccordionItem` hands its ref to Base UI's `Item` instead of overriding the
19
+ * render function's ref, which carries the list-item registration the arrow
20
+ * keys, Home and End read (without it they threw a TypeError).
21
+ * - Prop JSDoc rewritten from the FF docs API tables; modo TSDoc and
22
+ * examples on `Accordion`.
23
+ * - Compound statics `Accordion.Group/.Item/.Trigger/.Content` attached with
24
+ * `Object.assign`, typed through an `AccordionComponent` cast on the
25
+ * forwardRef. Upstream's named exports are kept.
26
+ * - Styling reads DS tokens (AGENTS.md styling): inline
27
+ * `fontVariationSettings` → `weight-*`; the hex focus-ring fallback →
28
+ * `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
29
+ * tier-length JS durations → `duration-<tier>` / `spring.*`.
30
+ * - The panel morphs out of its row instead of springing its height (local
31
+ * morph language, see `primitives/morph`): one progress value grows a shape
32
+ * from the press point in the row (its middle for a keyboard open) to the
33
+ * whole item, the content is revealed by a clip from the same rect, and the
34
+ * panel's layout height follows the shape's bottom edge, so the rows below
35
+ * flow with it. Under `highlight="item"` the open tint is that shape, melted
36
+ * into the row through the shared `GooFilter`; it lives in
37
+ * `AccordionContent` now (the group draws only the `"trigger"` tint), and
38
+ * the hover fills sit one layer above it. The group's hover highlight and
39
+ * focus ring snap with rows a morph pushes around (instead of springing
40
+ * after them over the content) until the pointer or focus moves. `effect`
41
+ * and `tier` on `AccordionContent`; reduced motion snaps, as before.
42
+ */
43
+
44
+ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
45
+ import { AnimatePresence, animate, motion, useMotionValue, useTransform } from 'motion/react'
46
+ import {
47
+ createContext,
48
+ type ForwardRefExoticComponent,
49
+ forwardRef,
50
+ type HTMLAttributes,
51
+ type ReactNode,
52
+ type RefAttributes,
53
+ useCallback,
54
+ useContext,
55
+ useEffect,
56
+ useId,
57
+ useImperativeHandle,
58
+ useLayoutEffect,
59
+ useMemo,
60
+ useRef,
61
+ useState,
62
+ } from 'react'
63
+
64
+ // SSR-safe layout effect (client components still server-render in Next).
65
+ const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
66
+
67
+ import { FluidHoverHighlight } from '../../lib/fluid-hover-highlight'
68
+ import { useIcon } from '../../lib/icon-context'
69
+ import { GooFilter } from '../../lib/morph-layers'
70
+ import { useReduceMotion } from '../../lib/reduced-motion'
71
+ import { useShape } from '../../lib/shape-context'
72
+ import { type SizeVariant, useSize } from '../../lib/size-context'
73
+ import { spring } from '../../lib/springs'
74
+ import { useControllableState } from '../../lib/use-controllable-state'
75
+ import { useFluidHover, useRegisterFluidHoverItem } from '../../lib/use-fluid-hover'
76
+ import { GOO_BLUR_RATIO } from '../../lib/use-morph'
77
+ import { cn } from '../../lib/utils'
78
+ import { SizeProvider } from '../../primitives/sizes'
79
+
80
+ // ─── Contexts ────────────────────────────────────────────────────────────────
81
+
82
+ interface ItemRect {
83
+ top: number
84
+ left: number
85
+ width: number
86
+ height: number
87
+ }
88
+
89
+ interface AccordionGroupContextValue {
90
+ registerItem: (index: number, element: HTMLElement | null) => void
91
+ registerFullItem: (index: number, element: HTMLElement | null) => void
92
+ activeIndex: number | null
93
+ grouped: true
94
+ remeasure: () => void
95
+ openValues: Set<string>
96
+ openItemRects: Map<number, ItemRect>
97
+ /** The group's choice; its items draw the `"item"` tint themselves. */
98
+ highlight: 'trigger' | 'item'
99
+ /** A closed row is hovered: open tints step back. */
100
+ dimOpen: boolean
101
+ }
102
+
103
+ const AccordionGroupContext = createContext<AccordionGroupContextValue | null>(null)
104
+
105
+ function useAccordionGroup() {
106
+ return useContext(AccordionGroupContext)
107
+ }
108
+
109
+ interface AccordionItemContextValue {
110
+ index?: number
111
+ value: string
112
+ isOpen: boolean
113
+ triggerRef: React.MutableRefObject<HTMLDivElement | null>
114
+ /** Standalone items carry the group's choice themselves. */
115
+ highlight: 'trigger' | 'item'
116
+ /** Where the row was last pressed, in px from its left edge; the panel grows from there. */
117
+ press: React.MutableRefObject<number | null>
118
+ }
119
+
120
+ const AccordionItemContext = createContext<AccordionItemContextValue | null>(null)
121
+
122
+ function useAccordionItemContext() {
123
+ const ctx = useContext(AccordionItemContext)
124
+ if (!ctx) throw new Error('AccordionTrigger/AccordionContent must be used within an AccordionItem')
125
+ return ctx
126
+ }
127
+
128
+ // ─── AccordionGroup ──────────────────────────────────────────────────────────
129
+
130
+ type AccordionGroupSingleProps = {
131
+ /** Whether one or multiple items can be expanded. Defaults to `"single"`. */
132
+ type?: 'single'
133
+ /** Controlled expanded value. */
134
+ value?: string
135
+ /** Initially expanded item value — uncontrolled. */
136
+ defaultValue?: string
137
+ /** Callback when expanded state changes. */
138
+ onValueChange?: (value: string) => void
139
+ /** Allow collapsing all items when type is single. Defaults to `true`. */
140
+ collapsible?: boolean
141
+ }
142
+
143
+ type AccordionGroupMultipleProps = {
144
+ /** Whether one or multiple items can be expanded. */
145
+ type: 'multiple'
146
+ /** Controlled expanded values. */
147
+ value?: string[]
148
+ /** Initially expanded item values — uncontrolled. */
149
+ defaultValue?: string[]
150
+ /** Callback when expanded state changes. */
151
+ onValueChange?: (value: string[]) => void
152
+ }
153
+
154
+ type AccordionGroupProps = HTMLAttributes<HTMLDivElement> & {
155
+ children: ReactNode
156
+ /** Pins the group's rows to one step of the size ladder (default 36px,
157
+ * compact 28px — see /docs/sizes). Omitted, they follow the surrounding
158
+ * SizeProvider. */
159
+ size?: SizeVariant
160
+ /** What an open item tints. "item" paints the row and its panel as one
161
+ * block, and holds while it stays open. "trigger" scopes the fill to the
162
+ * row and shows it on hover only, leaving the panel on the page's own
163
+ * surface — the way a sidebar row highlights without colouring its
164
+ * sub-tree. @default "item" */
165
+ highlight?: 'trigger' | 'item'
166
+ } & (AccordionGroupSingleProps | AccordionGroupMultipleProps)
167
+
168
+ const AccordionGroup = forwardRef<HTMLDivElement, AccordionGroupProps>((props, ref) => {
169
+ const { children, highlight = 'item', type = 'single', size, className, ...rest } = props
170
+
171
+ const containerRef = useRef<HTMLDivElement>(null)
172
+ const fullItemElementsRef = useRef<Map<number, HTMLElement>>(new Map())
173
+ const [openItemRects, setOpenItemRects] = useState<Map<number, ItemRect>>(new Map())
174
+ const openItemRectsRef = useRef(openItemRects)
175
+
176
+ const hover = useFluidHover(containerRef)
177
+ const { activeIndex, setActiveIndex, itemRects, handlers, registerItem, measureItems } = hover
178
+
179
+ const registerFullItem = useCallback((index: number, element: HTMLElement | null) => {
180
+ if (element) {
181
+ fullItemElementsRef.current.set(index, element)
182
+ } else {
183
+ fullItemElementsRef.current.delete(index)
184
+ }
185
+ }, [])
186
+
187
+ const measureFullItems = useCallback(() => {
188
+ if (!containerRef.current) return
189
+ const next = new Map<number, ItemRect>()
190
+ fullItemElementsRef.current.forEach((el, idx) => {
191
+ next.set(idx, {
192
+ top: el.offsetTop,
193
+ left: el.offsetLeft,
194
+ width: el.offsetWidth,
195
+ height: el.offsetHeight,
196
+ })
197
+ })
198
+ // Skip the state update when nothing moved (mirrors the fluid hover
199
+ // hook's measureItems guard) — this runs per animation frame via
200
+ // onUpdate, and an unconditional set would invalidate the group
201
+ // context and re-render every item even on no-op remeasures.
202
+ const prev = openItemRectsRef.current
203
+ let changed = prev.size !== next.size
204
+ if (!changed) {
205
+ for (const [idx, r] of next) {
206
+ const p = prev.get(idx)
207
+ if (!p || p.top !== r.top || p.left !== r.left || p.width !== r.width || p.height !== r.height) {
208
+ changed = true
209
+ break
210
+ }
211
+ }
212
+ }
213
+ if (!changed) return
214
+ openItemRectsRef.current = next
215
+ setOpenItemRects(next)
216
+ }, [])
217
+
218
+ const sp = props as AccordionGroupSingleProps
219
+ const mp = props as AccordionGroupMultipleProps
220
+ const [singleValue, setSingleValue] = useControllableState<string>(
221
+ type === 'single' ? sp.value : undefined,
222
+ type === 'single' ? (sp.defaultValue ?? '') : '',
223
+ type === 'single' ? sp.onValueChange : undefined,
224
+ )
225
+ const [multipleValue, setMultipleValue] = useControllableState<string[]>(
226
+ type === 'multiple' ? mp.value : undefined,
227
+ type === 'multiple' ? (mp.defaultValue ?? []) : [],
228
+ type === 'multiple' ? mp.onValueChange : undefined,
229
+ )
230
+
231
+ const openValuesList: string[] = type === 'multiple' ? multipleValue : singleValue ? [singleValue] : []
232
+
233
+ // Keyed on the joined values so the Set (and the group context value
234
+ // below) keeps a stable identity across re-renders where the open values
235
+ // haven't actually changed.
236
+ const openValuesKey = openValuesList.join(',')
237
+
238
+ const openValues = useMemo(
239
+ () => new Set(openValuesList),
240
+ // Deliberately keyed on the joined string, not the (fresh) array.
241
+ [openValuesKey],
242
+ )
243
+
244
+ useEffect(() => {
245
+ measureItems()
246
+ measureFullItems()
247
+ }, [measureItems, measureFullItems, children])
248
+
249
+ useEffect(() => {
250
+ measureItems()
251
+ measureFullItems()
252
+ }, [measureItems, measureFullItems, openValuesKey])
253
+
254
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null)
255
+
256
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null
257
+ // "trigger" tints the open row only while you're on it: the panel below
258
+ // already says the item is open, so the fill goes back to being a hover
259
+ // affordance rather than a persistent state. The trigger rects are the
260
+ // ones fluid hover already tracks. Local: the "item" block is drawn by each
261
+ // item's content, as the shape its panel morphs out of.
262
+ const expandedRects =
263
+ highlight === 'trigger'
264
+ ? new Map(
265
+ [...openItemRects.keys()].flatMap(idx => {
266
+ const rect = idx === activeIndex ? itemRects[idx] : null
267
+ return rect ? ([[idx, rect]] as [number, ItemRect][]) : []
268
+ }),
269
+ )
270
+ : new Map<number, ItemRect>()
271
+
272
+ const isHoveringNonOpen = activeIndex !== null && !openItemRects.has(activeIndex)
273
+ const shape = useShape()
274
+
275
+ const {
276
+ value: _value,
277
+ defaultValue: _defaultValue,
278
+ onValueChange: _onValueChange,
279
+ collapsible: _collapsible,
280
+ type: _type,
281
+ ...htmlProps
282
+ } = rest as Record<string, unknown>
283
+
284
+ // Translate FF API → Base UI Accordion API.
285
+ // Base UI always uses `value: string[]` and a `multiple: boolean`. In
286
+ // single mode we wrap the active value in a single-element array.
287
+ const baseValue: string[] = openValuesList
288
+
289
+ const baseOnValueChange = (next: string[]) => {
290
+ if (type === 'multiple') setMultipleValue(next)
291
+ else setSingleValue(next[0] ?? '')
292
+ }
293
+
294
+ // Local: rows a morphing panel pushes around carry the hover highlight and
295
+ // the focus ring with them (a snap, not a spring chasing them), until the
296
+ // pointer or the focus moves again.
297
+ const [reflowing, setReflowing] = useState(false)
298
+ const remeasure = useCallback(() => {
299
+ setReflowing(true)
300
+ measureItems()
301
+ measureFullItems()
302
+ }, [measureItems, measureFullItems])
303
+
304
+ // Memoized: the group re-renders on every fluid-hover mousemove; a
305
+ // fresh context object each time would re-render every item with it.
306
+ const groupContextValue = useMemo<AccordionGroupContextValue>(
307
+ () => ({
308
+ registerItem,
309
+ registerFullItem,
310
+ activeIndex,
311
+ grouped: true,
312
+ remeasure,
313
+ openValues,
314
+ openItemRects,
315
+ highlight,
316
+ dimOpen: isHoveringNonOpen,
317
+ }),
318
+ [registerItem, registerFullItem, activeIndex, remeasure, openValues, openItemRects, highlight, isHoveringNonOpen],
319
+ )
320
+
321
+ const group = (
322
+ <AccordionGroupContext.Provider value={groupContextValue}>
323
+ <AccordionPrimitive.Root
324
+ value={baseValue}
325
+ onValueChange={baseOnValueChange}
326
+ multiple={type === 'multiple'}
327
+ render={rootProps => {
328
+ const {
329
+ style: _baseStyle,
330
+ onDrag: _onDrag,
331
+ onDragStart: _onDragStart,
332
+ onDragEnd: _onDragEnd,
333
+ onAnimationStart: _onAnimationStart,
334
+ onAnimationEnd: _onAnimationEnd,
335
+ onAnimationIteration: _onAnimationIteration,
336
+ ...restRoot
337
+ } = rootProps as React.HTMLAttributes<HTMLDivElement>
338
+ return (
339
+ <div
340
+ {...restRoot}
341
+ ref={node => {
342
+ ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
343
+ if (typeof ref === 'function') ref(node)
344
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
345
+ }}
346
+ onMouseEnter={handlers.onMouseEnter}
347
+ onMouseMove={e => {
348
+ setReflowing(false)
349
+ const container = containerRef.current
350
+ if (container) {
351
+ const cRect = container.getBoundingClientRect()
352
+ const layoutH = container.offsetHeight
353
+ const visualH = cRect.height
354
+ const scale = layoutH > 0 ? visualH / layoutH : 1
355
+ const localY = (e.clientY - cRect.top) / scale + container.scrollTop
356
+ for (const [idx, full] of openItemRects) {
357
+ const trigger = itemRects[idx]
358
+ if (!trigger) continue
359
+ const contentTop = trigger.top + trigger.height
360
+ const contentBottom = full.top + full.height
361
+ if (localY >= contentTop && localY <= contentBottom) {
362
+ setActiveIndex(null)
363
+ return
364
+ }
365
+ }
366
+ }
367
+ handlers.onMouseMove(e)
368
+ }}
369
+ onMouseLeave={handlers.onMouseLeave}
370
+ onFocus={e => {
371
+ setReflowing(false)
372
+ const indexAttr = (e.target as HTMLElement)
373
+ .closest('[data-fluid-hover-index]')
374
+ ?.getAttribute('data-fluid-hover-index')
375
+ if (indexAttr != null) {
376
+ const idx = Number(indexAttr)
377
+ setActiveIndex(idx)
378
+ setFocusedIndex((e.target as HTMLElement).matches(':focus-visible') ? idx : null)
379
+ }
380
+ }}
381
+ onBlur={e => {
382
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return
383
+ setFocusedIndex(null)
384
+ setActiveIndex(null)
385
+ }}
386
+ className={cn('relative flex flex-col w-72 max-w-full', className)}
387
+ {...(htmlProps as HTMLAttributes<HTMLDivElement>)}
388
+ >
389
+ {/* Expanded item backgrounds */}
390
+ <AnimatePresence>
391
+ {[...expandedRects.entries()].map(([idx, rect]) => (
392
+ <motion.div
393
+ key={`expanded-${idx}`}
394
+ className={`absolute ${shape.bg} bg-accent/20 dark:bg-accent/12 pointer-events-none`}
395
+ // Fade in from the item's current rect: with initial={false}
396
+ // a newly-opened item's background would pop in at full
397
+ // opacity mid-layout-shift while the previous item's bg is
398
+ // still fading out — reads as a glitch when switching items
399
+ // (especially under /demo's scaled card). Geometry still
400
+ // snaps (duration 0) so the bg hugs the animating item.
401
+ initial={{
402
+ top: rect.top,
403
+ left: rect.left,
404
+ width: rect.width,
405
+ height: rect.height,
406
+ opacity: 0,
407
+ }}
408
+ animate={{
409
+ top: rect.top,
410
+ left: rect.left,
411
+ width: rect.width,
412
+ height: rect.height,
413
+ opacity: isHoveringNonOpen ? 0.7 : 1,
414
+ }}
415
+ exit={{ opacity: 0, transition: spring.moderate.exit }}
416
+ transition={{
417
+ top: { duration: 0 },
418
+ left: { duration: 0 },
419
+ width: { duration: 0 },
420
+ height: { duration: 0 },
421
+ opacity: { duration: spring.moderate.exit.duration },
422
+ }}
423
+ />
424
+ ))}
425
+ </AnimatePresence>
426
+
427
+ {/* Hover background, one layer above the items' open tints */}
428
+ <FluidHoverHighlight
429
+ hover={hover}
430
+ className={cn(shape.bg, 'z-1')}
431
+ transition={reflowing ? false : undefined}
432
+ />
433
+
434
+ {/* Focus ring */}
435
+ <AnimatePresence>
436
+ {focusRect && (
437
+ <motion.div
438
+ className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-focus-ring`}
439
+ initial={false}
440
+ animate={{
441
+ left: focusRect.left - 2,
442
+ top: focusRect.top - 2,
443
+ width: focusRect.width + 4,
444
+ height: focusRect.height + 4,
445
+ }}
446
+ exit={{ opacity: 0, transition: spring.fast.exit }}
447
+ transition={{
448
+ ...(reflowing ? { duration: 0 } : spring.fast),
449
+ opacity: { duration: spring.fast.duration },
450
+ }}
451
+ />
452
+ )}
453
+ </AnimatePresence>
454
+
455
+ {children}
456
+ </div>
457
+ )
458
+ }}
459
+ />
460
+ </AccordionGroupContext.Provider>
461
+ )
462
+
463
+ // A size prop pins every row in the group to one ladder step.
464
+ return size ? <SizeProvider size={size}>{group}</SizeProvider> : group
465
+ })
466
+
467
+ AccordionGroup.displayName = 'AccordionGroup'
468
+
469
+ // ─── Accordion (Standalone) ──────────────────────────────────────────────────
470
+
471
+ interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
472
+ /** The accordion's items (`Accordion.Item`). */
473
+ children: ReactNode
474
+ /** Whether one or multiple items can be expanded. Defaults to `"single"`. */
475
+ type?: 'single' | 'multiple'
476
+ /** Allow collapsing all items when type is single. Defaults to `true` (Base UI's single mode is always collapsible). */
477
+ collapsible?: boolean
478
+ /** Initially expanded item value(s) — uncontrolled. A string for `type="single"`, an array for `type="multiple"`. */
479
+ defaultValue?: string | string[]
480
+ /** Controlled expanded value(s). Pair with `onValueChange`. */
481
+ value?: string | string[]
482
+ /** Callback when expanded state changes. Receives a string in single mode, an array in multiple mode. */
483
+ onValueChange?: ((value: string) => void) | ((value: string[]) => void)
484
+ /** What an open item tints: `item` holds a block across the row and its panel, `trigger` scopes it to the row and shows it on hover. Same prop on `Accordion.Group` and on a standalone `Accordion.Item`. Defaults to `"item"`. */
485
+ highlight?: 'trigger' | 'item'
486
+ /** Pins the accordion's rows to one step of the size ladder (default 36px, compact 28px). Omitted, they follow the surrounding SizeProvider. */
487
+ size?: SizeVariant
488
+ }
489
+
490
+ type AccordionComponent = ForwardRefExoticComponent<AccordionProps & RefAttributes<HTMLDivElement>> & {
491
+ Group: typeof AccordionGroup
492
+ Item: typeof AccordionItem
493
+ Trigger: typeof AccordionTrigger
494
+ Content: typeof AccordionContent
495
+ }
496
+
497
+ /**
498
+ * Collapsible sections with animated expand/collapse and fluid hover in
499
+ * grouped mode.
500
+ *
501
+ * Accordion is the standalone form: every row carries its own hover fill.
502
+ * Accordion.Group takes the same props and adds one magnetic highlight that
503
+ * glides between rows, so give each item inside it an `index`. Built on Base
504
+ * UI Accordion (WAI-ARIA wiring, keyboard navigation, focus management).
505
+ * Uncontrolled through `defaultValue`, controlled through `value` +
506
+ * `onValueChange`.
507
+ *
508
+ * The panel grows out of its row (see Morph): a shape drips from where the
509
+ * row was pressed (its middle, from the keyboard), widens into the whole
510
+ * item, and the content is revealed by a clip from the same shape. Under the
511
+ * default `highlight="item"` that shape is the open tint, melted into the
512
+ * row through a liquid goo neck; closing folds it back into the row. The
513
+ * rows below ride the shape's bottom edge, so nothing jumps.
514
+ *
515
+ * Statics: Accordion.Group (the grouped root: type, collapsible,
516
+ * defaultValue, value, onValueChange, highlight, size), Accordion.Item (value,
517
+ * index, disabled), Accordion.Trigger (the row that toggles its item) and
518
+ * Accordion.Content (the collapsible panel: `effect` — `'goo'` or a plain
519
+ * `'morph'` — and the spring `tier`, `'slow'` or `'moderate'`).
520
+ *
521
+ * @example {@include ./examples.mdx}
522
+ */
523
+ const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
524
+ (
525
+ {
526
+ children,
527
+ type = 'single',
528
+ collapsible = true,
529
+ defaultValue,
530
+ value,
531
+ onValueChange,
532
+ highlight,
533
+ size,
534
+ className,
535
+ ...props
536
+ },
537
+ ref,
538
+ ) => {
539
+ void collapsible // Base UI's single-mode is always collapsible.
540
+
541
+ const [singleValue, setSingleValue] = useControllableState<string>(
542
+ type === 'single' ? (value as string | undefined) : undefined,
543
+ type === 'single' ? ((defaultValue as string | undefined) ?? '') : '',
544
+ type === 'single' ? (onValueChange as ((v: string) => void) | undefined) : undefined,
545
+ )
546
+ const [multipleValue, setMultipleValue] = useControllableState<string[]>(
547
+ type === 'multiple' ? (value as string[] | undefined) : undefined,
548
+ type === 'multiple' ? ((defaultValue as string[] | undefined) ?? []) : [],
549
+ type === 'multiple' ? (onValueChange as ((v: string[]) => void) | undefined) : undefined,
550
+ )
551
+
552
+ const openValues = new Set<string>(type === 'multiple' ? multipleValue : singleValue ? [singleValue] : [])
553
+
554
+ const baseValue: string[] = [...openValues]
555
+
556
+ const baseOnValueChange = (next: string[]) => {
557
+ if (type === 'multiple') setMultipleValue(next)
558
+ else setSingleValue(next[0] ?? '')
559
+ }
560
+
561
+ const root = (
562
+ <AccordionPrimitive.Root
563
+ value={baseValue}
564
+ onValueChange={baseOnValueChange}
565
+ multiple={type === 'multiple'}
566
+ render={rootProps => {
567
+ const { style: _s, ...restRoot } = rootProps as React.HTMLAttributes<HTMLDivElement>
568
+ return (
569
+ <div {...restRoot} ref={ref} className={cn('w-72 max-w-full flex flex-col', className)} {...props}>
570
+ <StandaloneOpenContext.Provider value={openValues}>
571
+ <StandaloneHighlightContext.Provider value={highlight}>{children}</StandaloneHighlightContext.Provider>
572
+ </StandaloneOpenContext.Provider>
573
+ </div>
574
+ )
575
+ }}
576
+ />
577
+ )
578
+
579
+ // A size prop pins every row to one ladder step.
580
+ return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
581
+ },
582
+ ) as AccordionComponent
583
+
584
+ Accordion.displayName = 'Accordion'
585
+
586
+ const StandaloneOpenContext = createContext<Set<string>>(new Set())
587
+
588
+ const StandaloneHighlightContext = createContext<'trigger' | 'item' | undefined>(undefined)
589
+
590
+ // ─── AccordionItem ───────────────────────────────────────────────────────────
591
+
592
+ interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
593
+ /** Unique identifier for this item. */
594
+ value: string
595
+ /** Position index for fluid hover. Required inside `Accordion.Group`, omit for standalone. */
596
+ index?: number
597
+ /** Whether this item is disabled. Defaults to `false`. */
598
+ disabled?: boolean
599
+ /** Standalone equivalent of the group's prop: what an open item tints. Ignored inside a group, which decides for all its rows. Defaults to the standalone root's `highlight`, else `"item"`. */
600
+ highlight?: 'trigger' | 'item'
601
+ /** The item's `Accordion.Trigger` and `Accordion.Content`. */
602
+ children: ReactNode
603
+ }
604
+
605
+ const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
606
+ ({ value, index, disabled, highlight: highlightProp, children, className, ...props }, ref) => {
607
+ const internalRef = useRef<HTMLDivElement>(null)
608
+ const groupCtx = useAccordionGroup()
609
+ const standaloneOpen = useContext(StandaloneOpenContext)
610
+ const standaloneHighlight = useContext(StandaloneHighlightContext)
611
+ const highlight = groupCtx ? groupCtx.highlight : (highlightProp ?? standaloneHighlight ?? 'item')
612
+
613
+ const isOpen = groupCtx?.grouped ? groupCtx.openValues.has(value) : standaloneOpen.has(value)
614
+
615
+ const triggerRef = useRef<HTMLDivElement>(null)
616
+ const press = useRef<number | null>(null)
617
+
618
+ useRegisterFluidHoverItem(groupCtx?.grouped ? groupCtx.registerItem : undefined, index, triggerRef)
619
+
620
+ useEffect(() => {
621
+ if (groupCtx?.grouped && index !== undefined) {
622
+ if (isOpen) {
623
+ groupCtx.registerFullItem(index, internalRef.current)
624
+ } else {
625
+ groupCtx.registerFullItem(index, null)
626
+ }
627
+ return () => groupCtx.registerFullItem(index, null)
628
+ }
629
+ }, [index, groupCtx, isOpen])
630
+
631
+ return (
632
+ <AccordionItemContext.Provider value={{ index, value, isOpen, triggerRef, highlight, press }}>
633
+ <AccordionPrimitive.Item
634
+ // Through Base UI, which merges it into the render function's ref
635
+ // with the list-item registration that arrow-key navigation reads.
636
+ ref={node => {
637
+ ;(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node
638
+ if (typeof ref === 'function') ref(node)
639
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
640
+ }}
641
+ value={value}
642
+ disabled={disabled}
643
+ render={itemProps => {
644
+ const { style: _s, ...restItem } = itemProps as React.HTMLAttributes<HTMLDivElement>
645
+ return (
646
+ <div
647
+ {...restItem}
648
+ data-fluid-hover-index={index}
649
+ // Positioned in a group too: the content's open tint covers
650
+ // the whole item (fluid hover adds positioned ancestors'
651
+ // offsets, so the row rects don't move).
652
+ className={cn('relative', className)}
653
+ {...props}
654
+ >
655
+ {children}
656
+ </div>
657
+ )
658
+ }}
659
+ />
660
+ </AccordionItemContext.Provider>
661
+ )
662
+ },
663
+ )
664
+
665
+ AccordionItem.displayName = 'AccordionItem'
666
+
667
+ // ─── AccordionTrigger ────────────────────────────────────────────────────────
668
+
669
+ interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {
670
+ /** Trigger label content. */
671
+ children: ReactNode
672
+ }
673
+
674
+ const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
675
+ ({ children, className, ...props }, ref) => {
676
+ const ChevronRight = useIcon('chevron-right')
677
+ const groupCtx = useAccordionGroup()
678
+ const { index, isOpen, triggerRef, highlight, press } = useAccordionItemContext()
679
+ const shape = useShape()
680
+ const sizeClasses = useSize()
681
+ const [isHovered, setIsHovered] = useState(false)
682
+
683
+ const isActive = groupCtx?.grouped ? groupCtx.activeIndex === index : isHovered
684
+
685
+ // In layout px, so a scaled ancestor doesn't move the source.
686
+ const capture = (event: React.PointerEvent<HTMLDivElement>) => {
687
+ const row = event.currentTarget
688
+ const box = row.getBoundingClientRect()
689
+ press.current = ((event.clientX - box.left) * row.offsetWidth) / box.width
690
+ }
691
+
692
+ const triggerContent = (
693
+ // Render Header as a <div>. Base UI's Header defaults to <h3>, which
694
+ // would be more semantic but breaks the ancestor selectors the styles
695
+ // rely on.
696
+ <AccordionPrimitive.Header render={<div />}>
697
+ <AccordionPrimitive.Trigger
698
+ ref={ref as React.Ref<HTMLElement>}
699
+ className={cn(
700
+ `relative z-10 flex items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.px} ${sizeClasses.variant === 'compact' ? 'py-1' : 'py-2'} w-full cursor-pointer outline-none select-none`,
701
+ !groupCtx?.grouped && 'focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:ring-offset-0',
702
+ className,
703
+ )}
704
+ {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
705
+ >
706
+ {/* Label with dual-layer text */}
707
+ <span className={cn('inline-grid flex-1 text-left', sizeClasses.text)}>
708
+ <span className="col-start-1 row-start-1 invisible weight-semibold" aria-hidden="true">
709
+ {children}
710
+ </span>
711
+ <span
712
+ className={cn(
713
+ 'col-start-1 row-start-1 transition-[color,font-variation-settings] duration-fast',
714
+ isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
715
+ isOpen ? 'weight-semibold' : 'weight-normal',
716
+ )}
717
+ >
718
+ {children}
719
+ </span>
720
+ </span>
721
+
722
+ {/* Chevron */}
723
+ <motion.span
724
+ className="shrink-0 inline-flex items-center justify-center"
725
+ animate={{ rotate: isOpen ? 90 : 0 }}
726
+ transition={spring.fast}
727
+ >
728
+ <ChevronRight
729
+ size={sizeClasses.icon}
730
+ strokeWidth={isOpen || isActive ? 2 : 1.5}
731
+ className={cn(
732
+ 'transition-[color,stroke-width] duration-fast',
733
+ isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
734
+ )}
735
+ />
736
+ </motion.span>
737
+ </AccordionPrimitive.Trigger>
738
+ </AccordionPrimitive.Header>
739
+ )
740
+
741
+ if (groupCtx?.grouped) {
742
+ return (
743
+ <div ref={triggerRef} onPointerDown={capture}>
744
+ {triggerContent}
745
+ </div>
746
+ )
747
+ }
748
+
749
+ return (
750
+ <div
751
+ className="relative"
752
+ onPointerDown={capture}
753
+ onMouseEnter={() => setIsHovered(true)}
754
+ onMouseLeave={() => setIsHovered(false)}
755
+ >
756
+ {/* Open tint, scoped to this row: the panel below keeps the page's
757
+ own surface, the way a sidebar row highlights without colouring
758
+ its sub-tree. */}
759
+ <AnimatePresence>
760
+ {isOpen && highlight === 'trigger' && isHovered && (
761
+ <motion.div
762
+ className={`absolute inset-0 ${shape.bg} bg-accent/20 dark:bg-accent/12 pointer-events-none`}
763
+ initial={{ opacity: 0 }}
764
+ animate={{ opacity: 1 }}
765
+ // The expanded tint rides the moderate tier, like the grouped
766
+ // one — it marks a state, where the hover fill below tracks the
767
+ // pointer and stays fast.
768
+ exit={{ opacity: 0, transition: spring.moderate.exit }}
769
+ transition={{ duration: spring.moderate.exit.duration }}
770
+ />
771
+ )}
772
+ </AnimatePresence>
773
+ <AnimatePresence>
774
+ {isHovered && (
775
+ <motion.div
776
+ className={`absolute inset-0 z-1 ${shape.bg} bg-hover pointer-events-none`}
777
+ initial={{ opacity: 0 }}
778
+ animate={{ opacity: 1 }}
779
+ exit={{ opacity: 0, transition: spring.fast.exit }}
780
+ transition={{ duration: spring.fast.duration }}
781
+ />
782
+ )}
783
+ </AnimatePresence>
784
+ {triggerContent}
785
+ </div>
786
+ )
787
+ },
788
+ )
789
+
790
+ AccordionTrigger.displayName = 'AccordionTrigger'
791
+
792
+ // ─── AccordionContent ────────────────────────────────────────────────────────
793
+
794
+ interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {
795
+ /** Collapsible content. */
796
+ children: ReactNode
797
+ /** How the panel grows out of its row (see Morph): `goo` melts the open tint out of the row through a liquid neck, `morph` grows it without one. Defaults to `'goo'`. */
798
+ effect?: 'goo' | 'morph'
799
+ /** Spring tier the panel opens on; it closes on the tier's faster exit tween. Defaults to `'slow'`. */
800
+ tier?: 'moderate' | 'slow'
801
+ }
802
+
803
+ interface Rect {
804
+ x: number
805
+ y: number
806
+ w: number
807
+ h: number
808
+ r: number
809
+ }
810
+
811
+ // The morph engine's rect interpolation (lib/use-morph.ts). `t` stops at 1:
812
+ // the slow tier's bounce would push the rows below past their place and back.
813
+ function grow(a: Rect, b: Rect, t: number): Rect {
814
+ const p = Math.min(1, t)
815
+ const at = (from: number, to: number) => from + (to - from) * p
816
+ const w = at(a.w, b.w)
817
+ const h = at(a.h, b.h)
818
+ return { x: at(a.x, b.x), y: at(a.y, b.y), w, h, r: Math.min(at(a.r, b.r), w / 2, h / 2) }
819
+ }
820
+
821
+ function place(el: HTMLElement | null, { x, y, w, h, r }: Rect) {
822
+ if (!el) return
823
+ el.style.left = `${x}px`
824
+ el.style.top = `${y}px`
825
+ el.style.width = `${w}px`
826
+ el.style.height = `${h}px`
827
+ el.style.borderRadius = `${r}px`
828
+ }
829
+
830
+ const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
831
+ ({ children, className, effect = 'goo', tier = 'slow', ...props }, ref) => {
832
+ const groupCtx = useAccordionGroup()
833
+ const { isOpen, highlight, press } = useAccordionItemContext()
834
+ const shape = useShape()
835
+ const sizeClasses = useSize()
836
+ const reduceMotion = useReduceMotion()
837
+ const gooId = `accordion-goo-${useId().replace(/:/g, '')}`
838
+ const progress = useMotionValue(isOpen ? 1 : 0)
839
+ // The tint is there for most of the morph and fades only as the shape
840
+ // gets back into the row, so the shape, not a fade, carries the motion.
841
+ const tint = useTransform(progress, [0, 0.3], [0, 1])
842
+ const tinted = highlight === 'item'
843
+ const remeasure = useRef(groupCtx?.remeasure)
844
+ remeasure.current = groupCtx?.remeasure
845
+
846
+ const refs = {
847
+ panel: useRef<HTMLDivElement>(null),
848
+ content: useRef<HTMLDivElement>(null),
849
+ rest: useRef<HTMLDivElement>(null),
850
+ goo: useRef<HTMLDivElement>(null),
851
+ row: useRef<HTMLDivElement>(null),
852
+ shape: useRef<HTMLDivElement>(null),
853
+ }
854
+ useImperativeHandle(ref, () => refs.panel.current as HTMLDivElement, [])
855
+
856
+ // In the item's own coordinates: `x` is the source's left edge, `row`
857
+ // the trigger row's height (the panel's offsetTop), `body` the content's.
858
+ const geometry = useRef({ x: 0, width: 0, row: 0, body: 0 })
859
+
860
+ // One frame of the morph. The shape grows from a point in the middle of
861
+ // the row to the whole item; the panel's layout height is the shape's
862
+ // reach below the row, so the rows underneath move with its bottom edge,
863
+ // and the content shows only inside the shape.
864
+ const render = (p: number) => {
865
+ const { x, width, row, body } = geometry.current
866
+ const rect = grow(
867
+ { x, y: row / 2, w: 0, h: 0, r: width },
868
+ { x: 0, y: 0, w: width, h: row + body, r: shape.bgRadius },
869
+ p,
870
+ )
871
+ const reach = rect.y + rect.h - row
872
+ const { panel, content } = refs
873
+ if (panel.current) panel.current.style.height = `${Math.max(0, reach)}px`
874
+ if (content.current) {
875
+ content.current.style.clipPath = `inset(${rect.y - row}px ${width - rect.x - rect.w}px ${body - reach}px ${rect.x}px round ${rect.r}px)`
876
+ }
877
+ place(refs.shape.current, rect)
878
+ remeasure.current?.()
879
+ }
880
+
881
+ // Base UI's Panel would apply `hidden` the moment a controlled item
882
+ // closes (no CSS animation for it to wait on) — `display: none` would
883
+ // cut the morph off mid-flight. So the `hidden` attribute is ours, and
884
+ // only lands once the close has finished.
885
+ const [exitComplete, setExitComplete] = useState(!isOpen)
886
+ if (isOpen && exitComplete) {
887
+ // Reset during render so the panel is laid out before the opening
888
+ // morph's first frame.
889
+ setExitComplete(false)
890
+ }
891
+
892
+ // Back to rest: open, the panel's height and the tint are their classes
893
+ // again (auto height, one block over the item), so they follow content
894
+ // that resizes; closed, the panel goes `hidden`.
895
+ const settle = (open: boolean) => {
896
+ const { panel, content, goo, rest } = refs
897
+ if (panel.current) panel.current.style.height = open ? '' : '0px'
898
+ // Closed, the content stays clipped shut: `hidden` lands with React's
899
+ // next commit, a task later, and unclipped content would paint below
900
+ // the collapsed row for a frame in between.
901
+ if (content.current) content.current.style.clipPath = open ? '' : 'inset(50%)'
902
+ if (goo.current) goo.current.style.display = ''
903
+ if (rest.current) rest.current.style.display = ''
904
+ remeasure.current?.()
905
+ if (!open) setExitComplete(true)
906
+ }
907
+
908
+ const wasOpen = useRef(isOpen)
909
+ useIsoLayoutEffect(() => {
910
+ if (wasOpen.current === isOpen) return
911
+ wasOpen.current = isOpen
912
+ const { panel, content, goo, rest, row } = refs
913
+ if (!panel.current || !content.current) return
914
+ const g = geometry.current
915
+ g.width = panel.current.offsetWidth
916
+ g.row = panel.current.offsetTop
917
+ g.body = content.current.offsetHeight
918
+ // A reopen mid-close keeps its source, so the shape never jumps.
919
+ if (isOpen && progress.get() === 0) g.x = press.current ?? g.width / 2
920
+ press.current = null
921
+
922
+ if (reduceMotion) {
923
+ progress.set(isOpen ? 1 : 0)
924
+ settle(isOpen)
925
+ return
926
+ }
927
+ if (goo.current) goo.current.style.display = 'block'
928
+ if (rest.current) rest.current.style.display = 'none'
929
+ if (row.current) row.current.style.height = `${g.row}px`
930
+ render(progress.get())
931
+ const { exit, ...enter } = spring[tier]
932
+ const controls = animate(progress, isOpen ? 1 : 0, {
933
+ ...(isOpen ? enter : exit),
934
+ onUpdate: render,
935
+ onComplete: () => settle(isOpen),
936
+ })
937
+ return () => controls.stop()
938
+ }, [isOpen])
939
+
940
+ // The content keeps its natural size while the panel morphs, so this
941
+ // never feeds back: it retargets a running morph and, at rest, lets the
942
+ // group re-measure rows a nested accordion pushed around.
943
+ useIsoLayoutEffect(() => {
944
+ const content = refs.content.current
945
+ if (!content) return
946
+ const observer = new ResizeObserver(() => {
947
+ if (content.offsetHeight > 0) geometry.current.body = content.offsetHeight
948
+ remeasure.current?.()
949
+ })
950
+ observer.observe(content)
951
+ return () => observer.disconnect()
952
+ }, [])
953
+
954
+ // Render through `<AccordionPrimitive.Panel keepMounted>` so the panel
955
+ // element persists through the close and the trigger ↔ panel ARIA
956
+ // contract stays intact: the panel carries `role="region"`,
957
+ // `aria-labelledby` and the id that the Trigger's `aria-controls` points
958
+ // to.
959
+ return (
960
+ <AccordionPrimitive.Panel
961
+ keepMounted
962
+ render={panelProps => {
963
+ const {
964
+ hidden: _baseHidden,
965
+ // Only carries the --accordion-panel-height/width vars, which
966
+ // stay 'auto' since Base UI never measures JS-driven animations.
967
+ style: _baseStyle,
968
+ ...restPanel
969
+ } = panelProps as React.HTMLAttributes<HTMLDivElement> & {
970
+ hidden?: boolean
971
+ }
972
+ return (
973
+ <div {...restPanel} hidden={!isOpen && exitComplete}>
974
+ {/* Neither this nor the panel is positioned, so the tint
975
+ below covers the whole item (row + panel). */}
976
+ <div ref={refs.panel} className={className} {...props}>
977
+ {tinted && (
978
+ <motion.div
979
+ aria-hidden
980
+ className="pointer-events-none absolute inset-0"
981
+ initial={false}
982
+ animate={{ opacity: groupCtx?.dimOpen ? 0.7 : 1 }}
983
+ transition={{ duration: spring.moderate.exit.duration }}
984
+ >
985
+ <motion.div className="absolute inset-0" style={{ opacity: tint }}>
986
+ {/* The tint's strength: solid shapes, so the goo
987
+ threshold sees full alpha, faded as one layer. */}
988
+ <div className="absolute inset-0 opacity-20 dark:opacity-12">
989
+ <div ref={refs.rest} className={cn('absolute inset-0 bg-accent', shape.bg)} />
990
+ {effect === 'goo' && <GooFilter id={gooId} blur={shape.bgRadius * GOO_BLUR_RATIO} />}
991
+ {/* The filter is the effect itself: it melts the row and the growing shape into one. */}
992
+ <div
993
+ ref={refs.goo}
994
+ className="absolute inset-0 hidden"
995
+ style={effect === 'goo' ? { filter: `url(#${gooId})` } : undefined}
996
+ >
997
+ <div ref={refs.row} className={cn('absolute inset-x-0 top-0 bg-accent', shape.bg)} />
998
+ <div ref={refs.shape} className="absolute bg-accent" />
999
+ </div>
1000
+ </div>
1001
+ </motion.div>
1002
+ </motion.div>
1003
+ )}
1004
+ <div
1005
+ ref={refs.content}
1006
+ className={cn(
1007
+ 'relative pt-1 text-muted-foreground',
1008
+ sizeClasses.px,
1009
+ sizeClasses.text,
1010
+ sizeClasses.variant === 'compact' ? 'pb-2.5' : 'pb-3',
1011
+ )}
1012
+ >
1013
+ {children}
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ )
1018
+ }}
1019
+ />
1020
+ )
1021
+ },
1022
+ )
1023
+
1024
+ AccordionContent.displayName = 'AccordionContent'
1025
+
1026
+ Object.assign(Accordion, {
1027
+ Group: AccordionGroup,
1028
+ Item: AccordionItem,
1029
+ Trigger: AccordionTrigger,
1030
+ Content: AccordionContent,
1031
+ })
1032
+
1033
+ export { Accordion, AccordionContent, AccordionGroup, AccordionItem, AccordionTrigger }
1034
+ export default Accordion