@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,541 @@
1
+ /*
2
+ * Vendored from Fluid Functionalism — `registry/default/hooks/use-fluid-hover.ts` at
3
+ * github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
4
+ * (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
5
+ * LICENSE.fluid-functionalism in this package.
6
+ *
7
+ * Local modifications:
8
+ * - `"use client"` directive dropped (no RSC here).
9
+ * - React 18 types: `sessionRef` typed `MutableRefObject<number>` (upstream
10
+ * targets React 19, where `RefObject.current` is mutable and non-null).
11
+ */
12
+
13
+ import {
14
+ type Dispatch,
15
+ type MutableRefObject,
16
+ type RefObject,
17
+ type SetStateAction,
18
+ useCallback,
19
+ useEffect,
20
+ useRef,
21
+ useState,
22
+ } from 'react'
23
+
24
+ export interface ItemRect {
25
+ top: number
26
+ height: number
27
+ left: number
28
+ width: number
29
+ }
30
+
31
+ export interface UseFluidHoverOptions {
32
+ /**
33
+ * Which direction to resolve the nearest item along.
34
+ * "y" — vertical lists (default): closest by top/height
35
+ * "x" — horizontal strips: closest by left/width
36
+ * "xy" — 2-D grids: closest card across both rows AND columns,
37
+ * measured by Euclidean distance to each item's center
38
+ */
39
+ axis?: 'x' | 'y' | 'xy'
40
+ /**
41
+ * Makes an item invisible to hit-testing without unregistering it — for
42
+ * rows that stay mounted while clipped away (a collapsed sub-tree).
43
+ * Unregistering would invalidate every measurement; a skipped item keeps
44
+ * the set stable. Consulted per mouse move, so keep it cheap.
45
+ */
46
+ isItemDisabled?: (element: HTMLElement) => boolean
47
+ /**
48
+ * Whether a click that lands between items (a gap, the padding, past the
49
+ * last row) is routed to the highlighted item, so what is lit is what a
50
+ * click hits. On by default: in a menu or a list the highlight is a
51
+ * promise about the click. Pass `false` where empty space should stay
52
+ * inert (rows with destructive actions, generous whitespace), or
53
+ * `{ maxDistance }` to route only clicks within that many pixels of the
54
+ * highlighted item's edge.
55
+ */
56
+ gapClick?: boolean | { maxDistance?: number }
57
+ }
58
+
59
+ export interface UseFluidHoverReturn {
60
+ activeIndex: number | null
61
+ setActiveIndex: Dispatch<SetStateAction<number | null>>
62
+ itemRects: ItemRect[]
63
+ /**
64
+ * True once every registered item has been measured and no remeasure is
65
+ * pending, i.e. `itemRects` describes the current item set. Gate absolutely
66
+ * positioned overlays on it: an overlay that mounts against a rect a later
67
+ * pass still corrects animates from the wrong place to the right one, which
68
+ * reads as the highlight sliding in from another row.
69
+ */
70
+ isMeasured: boolean
71
+ sessionRef: MutableRefObject<number>
72
+ handlers: {
73
+ onMouseMove: (e: React.MouseEvent) => void
74
+ onMouseEnter: () => void
75
+ onMouseLeave: () => void
76
+ /**
77
+ * Routes a click that lands between items (a gap, the padding, past the
78
+ * last row) to the highlighted item, so the highlight and the click agree:
79
+ * what is lit is what a click hits. A click inside an item is left to the
80
+ * item. Disabled items (`isItemDisabled`) are never activated.
81
+ */
82
+ onClick: (e: React.MouseEvent) => void
83
+ }
84
+ registerItem: (index: number, element: HTMLElement | null) => void
85
+ /**
86
+ * Invalidates the published rects and runs the hook's coalesced measurement
87
+ * pass again, holding `isMeasured` false until it settles. Reach for it when
88
+ * the rects may be wrong and showing an overlay against them would misplace
89
+ * it: a popup that stays mounted between opens keeps its items registered,
90
+ * so nothing else would notice that its rects were taken while it was
91
+ * hidden. Registration, item resize, and container resize already trigger
92
+ * a pass; do not call this on `children` changes.
93
+ */
94
+ remeasure: () => void
95
+ /**
96
+ * Re-reads the rects synchronously, keeping `isMeasured` as it is. Only for
97
+ * layout that moves the rows under a visible overlay frame by frame (the
98
+ * accordion re-measures inside its height animation). Everything else
99
+ * wants `remeasure`, or nothing.
100
+ */
101
+ measureItems: () => void
102
+ }
103
+
104
+ export interface PickNearestInput {
105
+ axis: 'x' | 'y' | 'xy'
106
+ /** The pointer, in viewport coordinates. */
107
+ point: { x: number; y: number }
108
+ /** Item rects in the container's layout space (sparse: unregistered slots
109
+ * are undefined). */
110
+ rects: readonly (ItemRect | undefined)[]
111
+ /** The container's bounding rect and live scroll / border offsets, which
112
+ * map layout rects into the pointer's viewport space. */
113
+ containerRect: { left: number; top: number; width: number; height: number }
114
+ scroll: { x: number; y: number }
115
+ border: { x: number; y: number }
116
+ /** Layout size of the container, so a cumulative ancestor `transform:
117
+ * scale` (a popup mid scale-in) can be factored out per axis. */
118
+ layoutSize: { width: number; height: number }
119
+ /** Skips an item without unregistering it. */
120
+ isDisabled?: (index: number) => boolean
121
+ }
122
+
123
+ /**
124
+ * The rule, as one pure function: an item the pointer is inside wins;
125
+ * otherwise the item whose center is nearest does, so a pointer in a gap, in
126
+ * the padding, or past the last row still lands. `y` and `x` measure one
127
+ * coordinate; `xy` measures the straight line to each center. Ties keep the
128
+ * first item. The hook calls this once per animation frame; the docs page
129
+ * times it.
130
+ */
131
+ export function pickNearest({
132
+ axis,
133
+ point,
134
+ rects,
135
+ containerRect,
136
+ scroll,
137
+ border,
138
+ layoutSize,
139
+ isDisabled,
140
+ }: PickNearestInput): number | null {
141
+ const scaleX = layoutSize.width > 0 ? containerRect.width / layoutSize.width : 1
142
+ const scaleY = layoutSize.height > 0 ? containerRect.height / layoutSize.height : 1
143
+ let closestIndex: number | null = null
144
+ let closestDistance = Infinity
145
+ let containingIndex: number | null = null
146
+
147
+ for (let index = 0; index < rects.length; index++) {
148
+ const r = rects[index]
149
+ if (!r) continue
150
+ if (isDisabled?.(index)) continue
151
+
152
+ if (axis === 'xy') {
153
+ const left = containerRect.left + (border.x + r.left - scroll.x) * scaleX
154
+ const top = containerRect.top + (border.y + r.top - scroll.y) * scaleY
155
+ const width = r.width * scaleX
156
+ const height = r.height * scaleY
157
+ if (point.x >= left && point.x <= left + width && point.y >= top && point.y <= top + height) {
158
+ containingIndex = index
159
+ }
160
+ const distance = Math.hypot(point.x - (left + width / 2), point.y - (top + height / 2))
161
+ if (distance < closestDistance) {
162
+ closestDistance = distance
163
+ closestIndex = index
164
+ }
165
+ continue
166
+ }
167
+
168
+ const horizontal = axis === 'x'
169
+ const mousePos = horizontal ? point.x : point.y
170
+ const scale = horizontal ? scaleX : scaleY
171
+ const itemStart =
172
+ (horizontal ? containerRect.left : containerRect.top) +
173
+ ((horizontal ? border.x : border.y) + (horizontal ? r.left : r.top) - (horizontal ? scroll.x : scroll.y)) * scale
174
+ const itemSize = (horizontal ? r.width : r.height) * scale
175
+ if (mousePos >= itemStart && mousePos <= itemStart + itemSize) {
176
+ containingIndex = index
177
+ }
178
+ const distance = Math.abs(mousePos - (itemStart + itemSize / 2))
179
+ if (distance < closestDistance) {
180
+ closestDistance = distance
181
+ closestIndex = index
182
+ }
183
+ }
184
+
185
+ return containingIndex ?? closestIndex
186
+ }
187
+
188
+ /** Set on the highlighted item (boolean attribute). */
189
+ export const ACTIVE_ATTR = 'data-fluid-hover-active'
190
+ /** Set on the container: the highlighted index, or absent. */
191
+ export const ACTIVE_INDEX_ATTR = 'data-fluid-hover-active-index'
192
+
193
+ const ACTIVATOR_SELECTOR =
194
+ "a[href], button, [role='menuitem'], [role='menuitemradio'], [role='menuitemcheckbox'], [role='option'], [role='radio'], [role='checkbox'], [role='tab'], [role='link'], [role='button']"
195
+
196
+ /**
197
+ * The element a routed click should land on. A registered item is usually
198
+ * the interactive row itself; when it is only a box around one (a sidebar
199
+ * row around its button, a card around its link), the first control inside
200
+ * is what a real click on the row would have reached.
201
+ */
202
+ function resolveActivator(element: HTMLElement): HTMLElement {
203
+ if (element.matches(ACTIVATOR_SELECTOR) || element.hasAttribute('tabindex')) {
204
+ return element
205
+ }
206
+ return element.querySelector<HTMLElement>(ACTIVATOR_SELECTOR) ?? element
207
+ }
208
+
209
+ /**
210
+ * How many frames the coalesced remeasure retries while the registered items
211
+ * still have no layout box. A popup can be in the DOM one frame before it is
212
+ * laid out; retrying beats publishing zeroed rects, and the cap keeps a list
213
+ * that stays hidden for good from spinning frames forever.
214
+ */
215
+ const measurementAttempts = 3
216
+
217
+ export function useFluidHover<T extends HTMLElement>(
218
+ containerRef: RefObject<T | null>,
219
+ options: UseFluidHoverOptions = {},
220
+ ): UseFluidHoverReturn {
221
+ const { axis = 'y', isItemDisabled, gapClick = true } = options
222
+ const gapClickMaxDistance = typeof gapClick === 'object' ? (gapClick.maxDistance ?? Infinity) : Infinity
223
+ const itemsRef = useRef(new Map<number, HTMLElement>())
224
+ const [activeIndex, setActiveIndex] = useState<number | null>(null)
225
+ // Mirrored for handlers that read it outside a render (the gap click).
226
+ const activeIndexRef = useRef<number | null>(null)
227
+ activeIndexRef.current = activeIndex
228
+
229
+ // The state, in the DOM: `data-fluid-hover-active` on the highlighted item
230
+ // and `data-fluid-hover-active-index` on the container. Devtools shows it
231
+ // and a test asserts on it without waiting for a frame. React does not
232
+ // manage these attributes, so it never clobbers them.
233
+ useEffect(() => {
234
+ const container = containerRef.current
235
+ if (activeIndex === null) container?.removeAttribute(ACTIVE_INDEX_ATTR)
236
+ else container?.setAttribute(ACTIVE_INDEX_ATTR, String(activeIndex))
237
+ const active = activeIndex === null ? undefined : itemsRef.current.get(activeIndex)
238
+ active?.setAttribute(ACTIVE_ATTR, '')
239
+ return () => {
240
+ active?.removeAttribute(ACTIVE_ATTR)
241
+ // A row that re-registered under this index while it was highlighted
242
+ // (a remount under a new key) was marked by registerItem, not by this
243
+ // effect: drop the mark from whatever element holds the index now.
244
+ if (activeIndex !== null) itemsRef.current.get(activeIndex)?.removeAttribute(ACTIVE_ATTR)
245
+ }
246
+ }, [activeIndex, containerRef])
247
+ const [itemRects, setItemRects] = useState<ItemRect[]>([])
248
+ const [isMeasured, setIsMeasured] = useState(false)
249
+ const itemRectsRef = useRef<ItemRect[]>([])
250
+ const sessionRef = useRef(0)
251
+ const rafIdRef = useRef<number | null>(null)
252
+ const remeasureRafIdRef = useRef<number | null>(null)
253
+
254
+ /**
255
+ * Publishes a rect for every registered item. Returns false when the
256
+ * measurement could not be completed (no container, or an item without a
257
+ * layout box) — nothing is published in that case, so the last complete
258
+ * measurement stands instead of being overwritten with zeroes.
259
+ */
260
+ const runMeasurement = useCallback(() => {
261
+ const container = containerRef.current
262
+ if (!container) return false
263
+ const rects: ItemRect[] = []
264
+ let everyItemHasLayout = true
265
+ itemsRef.current.forEach((element, index) => {
266
+ // An element inside a display:none / not-yet-laid-out popup has no
267
+ // offsetParent and reports every offset as 0. Publishing that would pin
268
+ // overlays to the top of the list, so treat the whole pass as
269
+ // incomplete. A boxless element is the only case: `position: fixed`
270
+ // items also have no offsetParent but do have a size.
271
+ const hasLayoutBox = element.offsetParent !== null || element.offsetWidth > 0 || element.offsetHeight > 0
272
+ if (!hasLayoutBox) {
273
+ everyItemHasLayout = false
274
+ return
275
+ }
276
+ // Use offset* instead of getBoundingClientRect so measurements are
277
+ // unaffected by CSS transforms (e.g. scaleY animation on the parent
278
+ // motion.div). offsetTop/offsetLeft are layout values relative to the
279
+ // offsetParent (the scroll container), matching the coordinate space
280
+ // used by `position: absolute` children. Items nested inside positioned
281
+ // descendants of the container (a sidebar sub-menu's rows live inside a
282
+ // positioned row) accumulate those ancestors' offsets, so every rect
283
+ // lands in the container's own coordinate space; for a flat list the
284
+ // loop never runs and this is exactly the plain offsetTop/offsetLeft.
285
+ let top = element.offsetTop
286
+ let left = element.offsetLeft
287
+ let ancestor = element.offsetParent as HTMLElement | null
288
+ while (ancestor && ancestor !== container && container.contains(ancestor)) {
289
+ top += ancestor.offsetTop + ancestor.clientTop
290
+ left += ancestor.offsetLeft + ancestor.clientLeft
291
+ ancestor = ancestor.offsetParent as HTMLElement | null
292
+ }
293
+ rects[index] = {
294
+ top,
295
+ height: element.offsetHeight,
296
+ left,
297
+ width: element.offsetWidth,
298
+ }
299
+ })
300
+ if (!everyItemHasLayout) return false
301
+ // Skip the state update when nothing moved (a cheap top/left/width/height
302
+ // compare) so redundant remeasures don't churn re-renders.
303
+ const prev = itemRectsRef.current
304
+ let changed = prev.length !== rects.length
305
+ for (let i = 0; !changed && i < rects.length; i++) {
306
+ const p = prev[i]
307
+ const r = rects[i]
308
+ if (p === r) continue // both undefined (sparse slot)
309
+ changed = !p || !r || p.top !== r.top || p.left !== r.left || p.width !== r.width || p.height !== r.height
310
+ }
311
+ if (changed) {
312
+ itemRectsRef.current = rects
313
+ setItemRects(rects)
314
+ }
315
+ return true
316
+ }, [containerRef])
317
+
318
+ const measureItems = useCallback(() => {
319
+ runMeasurement()
320
+ }, [runMeasurement])
321
+
322
+ /**
323
+ * The hook's single measurement pass: coalesces every trigger (item
324
+ * registration, container resize) into one remeasure on the next frame and
325
+ * is the only place readiness is reported, so `isMeasured` can never turn
326
+ * true while another pass is still queued.
327
+ */
328
+ const scheduleMeasurement = useCallback(
329
+ (attemptsLeft: number) => {
330
+ if (remeasureRafIdRef.current !== null) {
331
+ cancelAnimationFrame(remeasureRafIdRef.current)
332
+ }
333
+ remeasureRafIdRef.current = requestAnimationFrame(() => {
334
+ remeasureRafIdRef.current = null
335
+ if (runMeasurement()) {
336
+ setIsMeasured(true)
337
+ } else if (attemptsLeft > 1) {
338
+ scheduleMeasurement(attemptsLeft - 1)
339
+ }
340
+ })
341
+ },
342
+ [runMeasurement],
343
+ )
344
+
345
+ const remeasure = useCallback(() => {
346
+ // Readiness drops first: until the pass below settles, the published rects
347
+ // may not describe what is on screen, and an overlay positioned from them
348
+ // would be corrected after mounting — which animates as a slide.
349
+ setIsMeasured(false)
350
+ scheduleMeasurement(measurementAttempts)
351
+ }, [scheduleMeasurement])
352
+
353
+ // Observes the registered items themselves (not just the container): rows
354
+ // that change size in place — e.g. the site-wide size step flipping while a
355
+ // selection background is up — must invalidate the published rects even when
356
+ // the container the effect below captured has since been remounted and the
357
+ // ref points at a different element than the one being observed.
358
+ const itemRoRef = useRef<ResizeObserver | null>(null)
359
+ const getItemRo = useCallback(() => {
360
+ if (itemRoRef.current === null && typeof ResizeObserver !== 'undefined') {
361
+ itemRoRef.current = new ResizeObserver(() => scheduleMeasurement(measurementAttempts))
362
+ }
363
+ return itemRoRef.current
364
+ }, [scheduleMeasurement])
365
+
366
+ const registerItem = useCallback(
367
+ (index: number, element: HTMLElement | null) => {
368
+ if (element) {
369
+ itemsRef.current.set(index, element)
370
+ getItemRo()?.observe(element)
371
+ if (index === activeIndexRef.current) element.setAttribute(ACTIVE_ATTR, '')
372
+ } else {
373
+ const previous = itemsRef.current.get(index)
374
+ if (previous) itemRoRef.current?.unobserve(previous)
375
+ // The mark leaves with the element: a row that only moved to another
376
+ // index (a filtering list re-ordering) must not carry it there.
377
+ previous?.removeAttribute(ACTIVE_ATTR)
378
+ itemsRef.current.delete(index)
379
+ // The highlighted row is gone: nothing should stay lit or receive a
380
+ // routed click until the pointer picks again. Decided when the
381
+ // update applies, after this commit's registrations, so a row that
382
+ // only moved index hands the highlight to the row now under it.
383
+ if (index === activeIndexRef.current) {
384
+ setActiveIndex(current => (current === index && !itemsRef.current.has(index) ? null : current))
385
+ }
386
+ }
387
+ // Coalesce rapid register/unregister calls (e.g. when an AnimatePresence
388
+ // remounts a list of rows) into a single remeasure on the next frame,
389
+ // so consumers don't have to manually call measureItems after the
390
+ // container's children swap.
391
+ remeasure()
392
+ },
393
+ [remeasure, getItemRo],
394
+ )
395
+
396
+ const handleMouseMove = useCallback(
397
+ (e: React.MouseEvent) => {
398
+ const mouseX = e.clientX
399
+ const mouseY = e.clientY
400
+
401
+ if (rafIdRef.current !== null) {
402
+ cancelAnimationFrame(rafIdRef.current)
403
+ }
404
+
405
+ rafIdRef.current = requestAnimationFrame(() => {
406
+ rafIdRef.current = null
407
+ const container = containerRef.current
408
+ if (!container) return
409
+ setActiveIndex(
410
+ pickNearest({
411
+ axis,
412
+ point: { x: mouseX, y: mouseY },
413
+ rects: itemRectsRef.current,
414
+ containerRect: container.getBoundingClientRect(),
415
+ scroll: { x: container.scrollLeft, y: container.scrollTop },
416
+ border: { x: container.clientLeft, y: container.clientTop },
417
+ layoutSize: { width: container.offsetWidth, height: container.offsetHeight },
418
+ isDisabled: isItemDisabled
419
+ ? index => {
420
+ const el = itemsRef.current.get(index)
421
+ return !!el && isItemDisabled(el)
422
+ }
423
+ : undefined,
424
+ }),
425
+ )
426
+ })
427
+ },
428
+ [axis, containerRef, isItemDisabled],
429
+ )
430
+
431
+ const handleMouseEnter = useCallback(() => {
432
+ sessionRef.current += 1
433
+ }, [])
434
+
435
+ const handleMouseLeave = useCallback(() => {
436
+ if (rafIdRef.current !== null) {
437
+ cancelAnimationFrame(rafIdRef.current)
438
+ rafIdRef.current = null
439
+ }
440
+ setActiveIndex(null)
441
+ }, [])
442
+
443
+ const handleClick = useCallback(
444
+ (e: React.MouseEvent) => {
445
+ const target = e.target as Node | null
446
+ if (!target) return
447
+ // Inside an item: the item owns the click.
448
+ for (const element of itemsRef.current.values()) {
449
+ if (element.contains(target)) return
450
+ }
451
+ // A row that unmounted while its own click was still bubbling (a pick
452
+ // whose primitive re-renders the list synchronously, like a "create"
453
+ // row that becomes a real item) already landed; it is not a gap.
454
+ if (!target.isConnected) return
455
+ // A control that sits between the rows (a search field at the top of
456
+ // a menu, a footer button) keeps its own click too.
457
+ const control = (target as Element).closest?.(
458
+ "input, textarea, select, button, a, summary, [contenteditable], [role='textbox'], [role='searchbox'], [role='button']",
459
+ )
460
+ if (control) return
461
+ if (gapClick === false) return
462
+ const index = activeIndexRef.current
463
+ if (index === null) return
464
+ const element = itemsRef.current.get(index)
465
+ if (!element || isItemDisabled?.(element)) return
466
+ if (gapClickMaxDistance !== Infinity) {
467
+ const r = element.getBoundingClientRect()
468
+ const dx = Math.max(r.left - e.clientX, 0, e.clientX - r.right)
469
+ const dy = Math.max(r.top - e.clientY, 0, e.clientY - r.bottom)
470
+ if (Math.hypot(dx, dy) > gapClickMaxDistance) return
471
+ }
472
+ // A real DOM click on the item, so its own handlers (and the primitive
473
+ // wrapping it, if any) run exactly as if the pointer had been inside.
474
+ resolveActivator(element).click()
475
+ },
476
+ [isItemDisabled, gapClick, gapClickMaxDistance],
477
+ )
478
+
479
+ // Remeasure when the container resizes — a reflow moves items even though
480
+ // the registered set is unchanged, which would otherwise leave itemRects
481
+ // stale. Coalesced through the same rAF as register/unregister. Readiness is
482
+ // deliberately not dropped: the item set is unchanged, so the published rects
483
+ // stay usable, and hiding overlays on every reflow would flicker them.
484
+ useEffect(() => {
485
+ const container = containerRef.current
486
+ if (!container || typeof ResizeObserver === 'undefined') return
487
+ const ro = new ResizeObserver(() => scheduleMeasurement(measurementAttempts))
488
+ ro.observe(container)
489
+ return () => ro.disconnect()
490
+ }, [containerRef, scheduleMeasurement])
491
+
492
+ // Clean up rAF and the item observer on unmount
493
+ useEffect(() => {
494
+ return () => {
495
+ if (rafIdRef.current !== null) {
496
+ cancelAnimationFrame(rafIdRef.current)
497
+ }
498
+ if (remeasureRafIdRef.current !== null) {
499
+ cancelAnimationFrame(remeasureRafIdRef.current)
500
+ }
501
+ itemRoRef.current?.disconnect()
502
+ itemRoRef.current = null
503
+ }
504
+ }, [])
505
+
506
+ return {
507
+ activeIndex,
508
+ setActiveIndex,
509
+ itemRects,
510
+ isMeasured,
511
+ sessionRef,
512
+ handlers: {
513
+ onMouseMove: handleMouseMove,
514
+ onMouseEnter: handleMouseEnter,
515
+ onMouseLeave: handleMouseLeave,
516
+ onClick: handleClick,
517
+ },
518
+ registerItem,
519
+ remeasure,
520
+ measureItems,
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Registers an item's element with its list for as long as it is mounted.
526
+ * The one way rows join a list: pass the hook's `registerItem` (or the copy
527
+ * a context hands down), the row's index, and its ref. Either may be
528
+ * missing for a row rendered outside a list (a standalone card, an accordion
529
+ * item that is not grouped); then nothing is registered.
530
+ */
531
+ export function useRegisterFluidHoverItem(
532
+ registerItem: ((index: number, element: HTMLElement | null) => void) | undefined,
533
+ index: number | undefined,
534
+ ref: RefObject<HTMLElement | null>,
535
+ ) {
536
+ useEffect(() => {
537
+ if (!registerItem || index === undefined) return
538
+ registerItem(index, ref.current)
539
+ return () => registerItem(index, null)
540
+ }, [index, registerItem, ref])
541
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Vendored from Fluid Functionalism — `registry/default/sidebar-core.tsx` at
3
+ * github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
4
+ * (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
5
+ * LICENSE.fluid-functionalism in this package.
6
+ *
7
+ * Local modifications:
8
+ * - Moved out of the sidebar so the date and time pickers share it; exported.
9
+ */
10
+
11
+ import { useEffect, useState } from 'react'
12
+
13
+ // Starts undefined so the server and first client render agree (both treat it
14
+ // as desktop); the media query corrects it in an effect before interaction.
15
+ export function useIsMobile(breakpoint: number): boolean {
16
+ const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
17
+ useEffect(() => {
18
+ const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
19
+ const onChange = () => setIsMobile(mql.matches)
20
+ onChange()
21
+ mql.addEventListener('change', onChange)
22
+ return () => mql.removeEventListener('change', onChange)
23
+ }, [breakpoint])
24
+ return !!isMobile
25
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Vendored from Fluid Functionalism — `registry/default/hooks/use-keyboard-nav-gate.ts` at
3
+ * github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
4
+ * (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
5
+ * LICENSE.fluid-functionalism in this package.
6
+ *
7
+ * Local modifications:
8
+ * - `"use client"` directive dropped (no RSC here).
9
+ * - `@/lib/*` and `@/hooks/*` imports rewritten to sibling `./*` paths.
10
+ */
11
+
12
+ import { type KeyboardEvent, useCallback, useEffect, useRef } from 'react'
13
+ import { POPUP_NAV_KEYS } from './popup'
14
+
15
+ /**
16
+ * Gates a popup's keyboard focus ring on keyboard use.
17
+ *
18
+ * Rows take focus as a popup opens (the primitive's own autofocus, or the
19
+ * popup's first-row focus), and Chrome grants `:focus-visible` to any
20
+ * script-driven focus — so without a gate every pointer-opened popup would
21
+ * open with a ring on its first row. The flag is seeded at open from the
22
+ * element that has focus then: a trigger reached by keyboard matches
23
+ * `:focus-visible`, a clicked one doesn't. Navigation keys inside the popup
24
+ * earn the ring afterwards. Attach `trackKeyboardNav` in the CAPTURE phase —
25
+ * the primitive moves focus during its own keydown handling, so the flag
26
+ * must be set before then.
27
+ */
28
+ export function useKeyboardNavGate(open: boolean) {
29
+ const keyboardNavRef = useRef(false)
30
+
31
+ useEffect(() => {
32
+ if (!open) return
33
+ // Script focus inherits :focus-visible from the previously focused
34
+ // element, so this reads the same whether the trigger still has focus
35
+ // or the primitive has already moved it into the popup.
36
+ const active = document.activeElement
37
+ keyboardNavRef.current = active instanceof HTMLElement && active.matches(':focus-visible')
38
+ }, [open])
39
+
40
+ const trackKeyboardNav = useCallback((e: KeyboardEvent) => {
41
+ if (POPUP_NAV_KEYS.includes(e.key)) keyboardNavRef.current = true
42
+ }, [])
43
+
44
+ return { keyboardNavRef, trackKeyboardNav }
45
+ }