@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,1168 @@
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/dropdown.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/*` and
8
+ * `@/hooks/*` → `../../../lib/*` (`SizeProvider` → `../../../primitives/sizes`);
9
+ * `@/lib/elevated` → `../../../primitives/surface`; `@/components/ui/scroll-area`
10
+ * → `../../../primitives/scroll-area`; `@/components/ui/fluid-hover-highlight` →
11
+ * `../../../lib/fluid-hover-highlight`; `@/components/ui/menu-item` and
12
+ * `@/components/ui/dropdown-search` → `./menu-item`, `./dropdown-search`
13
+ * (both vendored into this folder).
14
+ * - Uncontrolled selection (Base UI's contract) on `Dropdown` and
15
+ * `Dropdown.Content`: `defaultCheckedIndex` / `defaultCheckedIndices` plus
16
+ * `onCheckedIndexChange` / `onCheckedIndicesChange`, through
17
+ * `useControllableState`. With a `default*` the panel owns the selection,
18
+ * so rows need no `checked` or `onSelect` of their own. `checkedIndex` /
19
+ * `checkedIndices` keep working exactly as upstream.
20
+ * - Uncontrolled search: `Dropdown.Search` with `filter` hands the query to
21
+ * the panel, which drops the `Dropdown.Item` rows whose `label` doesn't
22
+ * match, re-indexes the survivors for fluid hover, and shows
23
+ * `Dropdown.Empty` only when nothing matched — the work the controlled
24
+ * form asks the consumer to do. Selection stays keyed on the authored
25
+ * index (`sourceIndex` on the row), so it survives re-indexing.
26
+ * - `DropdownProps` members re-declared one per line with the FF docs
27
+ * API-table descriptions (modo's props table lists only members declared in
28
+ * the interface body).
29
+ * - `items[next].focus()` guarded for `noUncheckedIndexedAccess`.
30
+ * - Compound statics `Dropdown.Menu/.Trigger/.Content/.Item/.Label/
31
+ * .Separator/.Search/.Empty` attached with `Object.assign`, typed through a
32
+ * `DropdownComponent` cast on the forwardRef. Upstream's named exports are
33
+ * kept.
34
+ * - modo item: TSDoc + examples from the FF "Dropdown" docs page (its
35
+ * "Create from the query" section is skipped — adding a row to the list is
36
+ * real consumer state, not something an uncontrolled panel can stand in
37
+ * for), and a default export.
38
+ * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
39
+ * `text-<role>[-compact]`; the hex focus-ring fallback →
40
+ * `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
41
+ * tier-length JS durations → `duration-<tier>` / `spring.*`.
42
+ * - The popup menu morphs out of its trigger through the shared morph layer
43
+ * (`lib/use-morph.ts` + `MorphSurface`, goo by default on
44
+ * `spring.goo`; `Dropdown.Content` takes `from` / `effect` /
45
+ * `hideSource` / `tier`), the origin captured from `Menu.Root`'s
46
+ * `onOpenChange`. The `scaleY` motion wrapper and the `actionsRef` deferred
47
+ * unmount are gone: Menu ignores `actionsRef`, and the wrapper animated
48
+ * outside `Menu.Popup`, so Base UI unmounted the menu before its exit
49
+ * played. The popup paints its level through the morph's surface layers
50
+ * instead of `render={<Elevated/>}`, re-providing it with `SurfaceProvider`.
51
+ * - `renderMenuItem` passes the row's activation to the Menu item primitive's
52
+ * `onClick` (see menu-item.tsx), so Enter and Space pick a row instead of
53
+ * only closing the menu.
54
+ * - `effect="morph"` with `hideSource` opens the menu in place over its
55
+ * trigger: the positioner's `sideOffset` becomes the engine's `inPlaceOffset`.
56
+ * - shadcn DropdownMenu parity (local parts in `menu-parts.tsx`, numbering in
57
+ * `row-order.tsx`): children-based rows with an optional `index` (both
58
+ * panels provide `RowOrderContext`), `Dropdown.Group`, `Dropdown.Shortcut`,
59
+ * value-keyed `Dropdown.CheckboxItem` and `Dropdown.RadioGroup` /
60
+ * `Dropdown.RadioItem` (their checked rows join the merged selection
61
+ * background), and submenus (`Dropdown.Sub` / `.SubTrigger` /
62
+ * `.SubContent`). `Dropdown.Label` takes `inset` and names a surrounding
63
+ * group (`Menu.GroupLabel`); labels and separators take a place in the row
64
+ * order so selection runs never melt across them.
65
+ * - `Dropdown.Trigger` also takes Radix-style `asChild` (`lib/slot.ts`).
66
+ * - The popup panel shares `useMenuPanel` with `Dropdown.SubContent`: a
67
+ * keyboard focus ring gated on keyboard use (as in Select), the highlight
68
+ * pinned on a submenu trigger while its submenu is open, and handlers that
69
+ * ignore events bubbling up (through the portal) from a submenu. Its
70
+ * `renderMenuItem` is the shared `renderPopupItem`.
71
+ */
72
+
73
+ import type { MenuTriggerProps } from '@base-ui/react/menu'
74
+ import { Menu } from '@base-ui/react/menu'
75
+ import { AnimatePresence, motion } from 'motion/react'
76
+ import {
77
+ Children,
78
+ type ComponentProps,
79
+ cloneElement,
80
+ createContext,
81
+ type ForwardRefExoticComponent,
82
+ forwardRef,
83
+ type HTMLAttributes,
84
+ isValidElement,
85
+ type ReactElement,
86
+ type ReactNode,
87
+ type RefAttributes,
88
+ type RefObject,
89
+ useCallback,
90
+ useContext,
91
+ useEffect,
92
+ useImperativeHandle,
93
+ useMemo,
94
+ useRef,
95
+ useState,
96
+ } from 'react'
97
+ import { FluidHoverHighlight } from '../../../lib/fluid-hover-highlight'
98
+ import { FocusRing } from '../../../lib/focus-ring'
99
+ import { MorphSurface } from '../../../lib/morph-layers'
100
+ import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../../lib/popup'
101
+ import { RowOrderContext, useRowOrder, useRowSlot } from '../../../lib/row-order'
102
+ import { shapeMap } from '../../../lib/shape-context'
103
+ import { type SizeVariant, useSize } from '../../../lib/size-context'
104
+ import { spring } from '../../../lib/springs'
105
+ import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
106
+ import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
107
+ import { useControllableState } from '../../../lib/use-controllable-state'
108
+ import { type ItemRect, useFluidHover } from '../../../lib/use-fluid-hover'
109
+ import { SelectionBackgrounds, useMergeSplitBlocks, useSelectionRuns } from '../../../lib/use-merge-split'
110
+ import { inPlaceOffset, type MorphOrigin, opensInPlace, useMorph, useMorphOrigin } from '../../../lib/use-morph'
111
+ import { cn } from '../../../lib/utils'
112
+ import { ScrollArea } from '../../../primitives/scroll-area'
113
+ import { SizeProvider } from '../../../primitives/sizes'
114
+ import { Elevated } from '../../../primitives/surface'
115
+ import {
116
+ DropdownEmpty,
117
+ DropdownFilterContext,
118
+ type DropdownFilterContextValue,
119
+ DropdownSearch,
120
+ DropdownSearchHostContext,
121
+ type DropdownSearchProps,
122
+ useDropdownSearchHost,
123
+ } from './dropdown-search'
124
+ import {
125
+ DropdownContext,
126
+ type DropdownContextValue,
127
+ DropdownShortcut,
128
+ type DropdownShortcutProps,
129
+ INSET_PX,
130
+ MenuItem,
131
+ type MenuItemProps,
132
+ type MenuItemRenderOptions,
133
+ useDropdown,
134
+ useDropdownMaybe,
135
+ } from './menu-item'
136
+ import {
137
+ DropdownCheckboxItem,
138
+ type DropdownCheckboxItemProps,
139
+ DropdownGroup,
140
+ DropdownGroupContext,
141
+ type DropdownGroupProps,
142
+ DropdownRadioGroup,
143
+ type DropdownRadioGroupProps,
144
+ DropdownRadioItem,
145
+ type DropdownRadioItemProps,
146
+ DropdownSub,
147
+ DropdownSubContent,
148
+ type DropdownSubContentProps,
149
+ type DropdownSubProps,
150
+ DropdownSubTrigger,
151
+ type DropdownSubTriggerProps,
152
+ mergeRefs,
153
+ ownEvent,
154
+ renderPopupItem,
155
+ useMenuPanel,
156
+ useValueChecked,
157
+ } from './menu-parts'
158
+
159
+ // Dropdown opts out of the global pill/rounded shape context — popover surfaces
160
+ // look cleaner with the smaller "rounded" radii regardless of how the rest of
161
+ // the UI is shaped (the heavy pill bubbling distorts perceived padding at this
162
+ // scale and produces the corner-shadow asymmetry).
163
+ const shape = shapeMap.rounded
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Panel context — shared by the inline Dropdown and the popup DropdownContent.
167
+ //
168
+ // The context object itself lives in menu-item.tsx so MenuItem resolves
169
+ // whichever dropdown provider actually wraps it, even when dropdowns built
170
+ // on different primitives render side by side. Re-exported here so the
171
+ // public dropdown API is unchanged.
172
+ // ---------------------------------------------------------------------------
173
+
174
+ export type { DropdownContextValue, MenuItemRenderOptions }
175
+ export { useDropdown, useDropdownMaybe }
176
+
177
+ /* "Nothing checked", so useControllableState always sees a defined value
178
+ while the caller drives the selection (`undefined` is what it reads as
179
+ "uncontrolled"). */
180
+ const NONE = -1
181
+ const NO_INDICES: number[] = []
182
+ /* Stable empty rect list, so a closed popup's merge/split pass gets the same
183
+ array every render instead of a fresh literal. */
184
+ const NO_RECTS: ItemRect[] = []
185
+
186
+ interface PanelSelectionProps {
187
+ checkedIndex?: number
188
+ defaultCheckedIndex?: number
189
+ onCheckedIndexChange?: (index: number | undefined) => void
190
+ checkedIndices?: number[]
191
+ defaultCheckedIndices?: number[]
192
+ onCheckedIndicesChange?: (indices: number[]) => void
193
+ }
194
+
195
+ /*
196
+ * The panel's selection, in the two index spaces it lives in: every prop and
197
+ * callback speaks the AUTHORED index (what a row was written with), while the
198
+ * overlays and the row context speak the RENDERED index (what a filtering
199
+ * panel re-indexed the visible rows to). They are the same list unless a
200
+ * `Dropdown.Search filter` is dropping rows.
201
+ */
202
+ function usePanelSelection(
203
+ {
204
+ checkedIndex,
205
+ defaultCheckedIndex,
206
+ onCheckedIndexChange,
207
+ checkedIndices,
208
+ defaultCheckedIndices,
209
+ onCheckedIndicesChange,
210
+ }: PanelSelectionProps,
211
+ sourceOf: number[],
212
+ filtering: boolean,
213
+ ) {
214
+ const [single, setSingle] = useControllableState<number>(
215
+ defaultCheckedIndex === undefined ? (checkedIndex ?? NONE) : undefined,
216
+ defaultCheckedIndex ?? NONE,
217
+ index => onCheckedIndexChange?.(index === NONE ? undefined : index),
218
+ )
219
+ const [many, setMany] = useControllableState<number[]>(
220
+ defaultCheckedIndices === undefined ? (checkedIndices ?? NO_INDICES) : undefined,
221
+ defaultCheckedIndices ?? NO_INDICES,
222
+ indices => onCheckedIndicesChange?.(indices),
223
+ )
224
+
225
+ const multiple = checkedIndices != null || defaultCheckedIndices != null
226
+ // Only a `default*` makes the panel answer for rows that brought no
227
+ // `checked` of their own.
228
+ const selfManaged = defaultCheckedIndex !== undefined || defaultCheckedIndices !== undefined
229
+
230
+ const toggleIndex = useCallback(
231
+ (source: number) => {
232
+ if (multiple) {
233
+ setMany(prev =>
234
+ prev.includes(source) ? prev.filter(i => i !== source) : [...prev, source].sort((a, b) => a - b),
235
+ )
236
+ return
237
+ }
238
+ setSingle(source)
239
+ },
240
+ [multiple, setMany, setSingle],
241
+ )
242
+
243
+ const renderedIndex = useMemo(() => {
244
+ if (single === NONE) return undefined
245
+ const i = filtering ? sourceOf.indexOf(single) : single
246
+ return i === -1 ? undefined : i
247
+ }, [single, filtering, sourceOf])
248
+
249
+ const renderedIndices = useMemo(() => {
250
+ if (!multiple) return undefined
251
+ if (!filtering) return many
252
+ return many.map(s => sourceOf.indexOf(s)).filter(i => i !== -1)
253
+ }, [multiple, many, filtering, sourceOf])
254
+
255
+ return {
256
+ checkedIndex: renderedIndex,
257
+ checkedIndices: renderedIndices,
258
+ multiple,
259
+ selfManaged,
260
+ toggleIndex,
261
+ }
262
+ }
263
+
264
+ interface PanelRows {
265
+ /** What to render: the rows re-indexed and `Dropdown.Empty` dropped while
266
+ * anything matched, or the children untouched when nothing filters. */
267
+ content: ReactNode
268
+ /** Rendered index → authored index. Empty while nothing filters. */
269
+ sourceOf: number[]
270
+ filtering: boolean
271
+ /** The query to publish to a `Dropdown.Search filter`, or null. */
272
+ filterCtx: DropdownFilterContextValue | null
273
+ }
274
+
275
+ /*
276
+ * Uncontrolled search. A `Dropdown.Search` with `filter` among the panel's
277
+ * direct children hands the panel the query; the panel then keeps only the
278
+ * rows (anything with a string `label` and a numeric `index` — MenuItem, or a
279
+ * wrapper that forwards both) whose label contains it, re-indexes them from 0
280
+ * for fluid hover, and stamps each with the index it was authored with so the
281
+ * selection survives. `Dropdown.Empty` children are held back and rendered
282
+ * only when no row matched.
283
+ */
284
+ function usePanelRows(children: ReactNode): PanelRows {
285
+ const [query, setQuery] = useState('')
286
+
287
+ const filtering = useMemo(
288
+ () =>
289
+ Children.toArray(children).some(
290
+ child => isValidElement(child) && child.type === DropdownSearch && (child.props as { filter?: boolean }).filter,
291
+ ),
292
+ [children],
293
+ )
294
+
295
+ const filterCtx = useMemo(() => ({ query, setQuery }), [query])
296
+
297
+ const { content, sourceOf } = useMemo(() => {
298
+ if (!filtering) return { content: children, sourceOf: NO_INDICES }
299
+ const needle = query.trim().toLowerCase()
300
+ const rendered: ReactNode[] = []
301
+ const empties: ReactNode[] = []
302
+ const sources: number[] = []
303
+ for (const child of Children.toArray(children)) {
304
+ if (!isValidElement(child)) {
305
+ rendered.push(child)
306
+ continue
307
+ }
308
+
309
+ if (child.type === DropdownEmpty) {
310
+ empties.push(child)
311
+ continue
312
+ }
313
+
314
+ const rowProps = child.props as { label?: unknown; index?: unknown }
315
+ if (typeof rowProps.label !== 'string' || typeof rowProps.index !== 'number') {
316
+ rendered.push(child)
317
+ continue
318
+ }
319
+
320
+ if (needle && !rowProps.label.toLowerCase().includes(needle)) continue
321
+
322
+ rendered.push(
323
+ cloneElement(child as ReactElement<Record<string, unknown>>, {
324
+ index: sources.length,
325
+ sourceIndex: rowProps.index,
326
+ }),
327
+ )
328
+ sources.push(rowProps.index)
329
+ }
330
+ if (sources.length === 0) rendered.push(...empties)
331
+ return { content: rendered as ReactNode, sourceOf: sources }
332
+ }, [children, filtering, query])
333
+
334
+ return { content, sourceOf, filtering, filterCtx: filtering ? filterCtx : null }
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Dropdown (inline panel)
339
+ //
340
+ // An always-rendered panel — no trigger, positioning, or dismissal. Because it
341
+ // sits statically in the page it does NOT claim popup menu semantics: the
342
+ // container is a plain role="group" (pass `aria-label` to name it). The real
343
+ // role="menu" lives on the popup DropdownContent below, which Base UI wires to
344
+ // a trigger. Consumers who hand-roll a trigger around the inline panel get
345
+ // grouping semantics rather than a falsely-announced popup menu.
346
+ // ---------------------------------------------------------------------------
347
+
348
+ interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
349
+ /** `Dropdown.Item` rows, plus `Dropdown.Label` and `Dropdown.Separator`. */
350
+ children: ReactNode
351
+ /** The checked row: drives the animated selected background and the rows' radio semantics. */
352
+ checkedIndex?: number
353
+ /** Uncontrolled twin of `checkedIndex`: the panel owns the selection, so rows need no `checked` or `onSelect` of their own. */
354
+ defaultCheckedIndex?: number
355
+ /** Called with the newly checked row, or `undefined` when nothing is checked. */
356
+ onCheckedIndexChange?: (index: number | undefined) => void
357
+ /** Multiple selection: the checked rows. Rows become checkbox items and contiguous runs share one merged background (see Checkbox.Group). */
358
+ checkedIndices?: number[]
359
+ /** Uncontrolled twin of `checkedIndices`: the panel toggles rows itself. */
360
+ defaultCheckedIndices?: number[]
361
+ /** Called with every checked row after a toggle. */
362
+ onCheckedIndicesChange?: (indices: number[]) => void
363
+ /** Pins the panel's rows to one step of the size ladder (default 36px, compact 28px — see Sizes). Omitted, they follow the surrounding SizeProvider. */
364
+ size?: SizeVariant
365
+ /** Extra classes for the panel surface — it is 18rem wide by default. */
366
+ className?: string
367
+ }
368
+
369
+ type DropdownComponent = ForwardRefExoticComponent<DropdownProps & RefAttributes<HTMLDivElement>> & {
370
+ Menu: typeof DropdownMenu
371
+ Trigger: typeof DropdownTrigger
372
+ Content: typeof DropdownContent
373
+ Item: typeof MenuItem
374
+ Label: typeof DropdownLabel
375
+ Separator: typeof DropdownSeparator
376
+ Search: typeof DropdownSearch
377
+ Empty: typeof DropdownEmpty
378
+ Group: typeof DropdownGroup
379
+ Shortcut: typeof DropdownShortcut
380
+ CheckboxItem: typeof DropdownCheckboxItem
381
+ RadioGroup: typeof DropdownRadioGroup
382
+ RadioItem: typeof DropdownRadioItem
383
+ Sub: typeof DropdownSub
384
+ SubTrigger: typeof DropdownSubTrigger
385
+ SubContent: typeof DropdownSubContent
386
+ }
387
+
388
+ /**
389
+ * A menu in two forms: an inline panel that sits in the page, or a popup on
390
+ * any trigger you like.
391
+ *
392
+ * `Dropdown` is the inline panel — an elevated surface with the fluid hover
393
+ * background gliding between rows, a selected background that springs to the
394
+ * picked one, contiguous picks merging into a single block in multiple mode,
395
+ * and an animated focus ring. `Dropdown.Menu` + `Dropdown.Trigger` +
396
+ * `Dropdown.Content` is the same panel as a popup on Base UI's Menu, which
397
+ * brings the positioning, dismissal, roving highlight, typeahead and
398
+ * close-on-select; the popup oozes out of its trigger and back through the
399
+ * shared morph (see Morph), goo by default. Both take `checkedIndex` / `checkedIndices` to be driven
400
+ * from outside, or `defaultCheckedIndex` / `defaultCheckedIndices` to run
401
+ * themselves — and a `Dropdown.Search` marked `filter` makes the panel own
402
+ * the query too, keeping only the rows whose `label` matches and showing
403
+ * `Dropdown.Empty` when none do.
404
+ *
405
+ * Rows also come shadcn-style: children instead of a `label`, no `index` (a
406
+ * row takes its place in document order, so the hover, the keyboard ring and
407
+ * the selection backgrounds still find it, inside groups and behind
408
+ * conditions alike), `inset` to line up with rows that have an icon and a
409
+ * trailing `shortcut`. `Dropdown.CheckboxItem` and `Dropdown.RadioGroup` /
410
+ * `Dropdown.RadioItem` select by their own state and value instead of by
411
+ * index, their checked rows melting into one background like a multiple
412
+ * selection. A `Dropdown.Sub` opens a submenu from its trigger row (ArrowRight,
413
+ * a press, or a resting pointer) that grows out of that row and lifts above
414
+ * the menu, with a highlight and a keyboard ring of its own. Every popup
415
+ * lifts 2 surface levels off whatever it opens over, a Dialog included.
416
+ *
417
+ * Statics:
418
+ * - `Dropdown.Menu` — the popup root: `open`, `defaultOpen`, `onOpenChange`, `disabled`, `size`.
419
+ * - `Dropdown.Trigger` — the trigger: `render` or `asChild`.
420
+ * - `Dropdown.Content` — the popup panel: `side`, `align`, `sideOffset`, the
421
+ * index selection, and the morph options `from`, `effect`, `hideSource`, `tier`.
422
+ * - `Dropdown.Item` — a row: `label` or `children`, `index`, `icon`, `inset`,
423
+ * `shortcut`, `checked`, `onSelect`, `closeOnClick`, `disabled`.
424
+ * - `Dropdown.CheckboxItem` — a row toggling itself: `checked`, `defaultChecked`, `onCheckedChange`.
425
+ * - `Dropdown.RadioGroup` / `Dropdown.RadioItem` — one choice by value: `value`,
426
+ * `defaultValue`, `onValueChange` on the group, `value` on each row.
427
+ * - `Dropdown.Group` / `Dropdown.Label` — a named run of rows and its caption (`inset`).
428
+ * - `Dropdown.Separator` — a rule between runs.
429
+ * - `Dropdown.Shortcut` — a muted caption at a row's end.
430
+ * - `Dropdown.Sub` / `Dropdown.SubTrigger` / `Dropdown.SubContent` — a submenu:
431
+ * its state, its trigger row (`openOnHover`, `delay`), its panel (`side`,
432
+ * `align`, `sideOffset`, `alignOffset`, `from`, `effect`, `tier`).
433
+ * - `Dropdown.Search` / `Dropdown.Empty` — a filter field and its no-match row.
434
+ *
435
+ * @example {@include ./examples.mdx}
436
+ */
437
+ const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
438
+ (
439
+ {
440
+ children,
441
+ checkedIndex,
442
+ defaultCheckedIndex,
443
+ onCheckedIndexChange,
444
+ checkedIndices,
445
+ defaultCheckedIndices,
446
+ onCheckedIndicesChange,
447
+ size,
448
+ className,
449
+ ...props
450
+ },
451
+ ref,
452
+ ) => {
453
+ const containerRef = useRef<HTMLDivElement>(null)
454
+ const hover = useFluidHover(containerRef, { isItemDisabled: isDisabledRow })
455
+ const { activeIndex, setActiveIndex, itemRects, handlers, registerItem } = hover
456
+
457
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null)
458
+ const rowOrder = useRowOrder()
459
+ const { valueChecked, reportChecked } = useValueChecked()
460
+
461
+ const rows = usePanelRows(children)
462
+ const selection = usePanelSelection(
463
+ {
464
+ checkedIndex,
465
+ defaultCheckedIndex,
466
+ onCheckedIndexChange,
467
+ checkedIndices,
468
+ defaultCheckedIndices,
469
+ onCheckedIndicesChange,
470
+ },
471
+ rows.sourceOf,
472
+ rows.filtering,
473
+ )
474
+
475
+ const multiple = selection.multiple
476
+ const checkedRect = !multiple && selection.checkedIndex != null ? itemRects[selection.checkedIndex] : null
477
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null
478
+ // Multiple: one merged block per contiguous run of checked rows, value-
479
+ // checked rows (CheckboxItem, RadioItem) included.
480
+ const runs = useSelectionRuns(
481
+ useMemo(
482
+ () => [...(selection.checkedIndices ?? NO_INDICES), ...valueChecked],
483
+ [selection.checkedIndices, valueChecked],
484
+ ),
485
+ )
486
+ const blocks = useMergeSplitBlocks(runs, itemRects, shape.bgRadius)
487
+ const panelCtx = useMemo(
488
+ () => ({
489
+ registerItem,
490
+ activeIndex,
491
+ checkedIndex: selection.checkedIndex,
492
+ multiple,
493
+ checkedIndices: selection.checkedIndices,
494
+ selfManaged: selection.selfManaged,
495
+ toggleIndex: selection.toggleIndex,
496
+ reportChecked,
497
+ }),
498
+ [
499
+ registerItem,
500
+ activeIndex,
501
+ selection.checkedIndex,
502
+ multiple,
503
+ selection.checkedIndices,
504
+ selection.selfManaged,
505
+ selection.toggleIndex,
506
+ reportChecked,
507
+ ],
508
+ )
509
+ const panel = (
510
+ <RowOrderContext.Provider value={rowOrder}>
511
+ <DropdownContext.Provider value={panelCtx}>
512
+ <DropdownFilterContext.Provider value={rows.filterCtx}>
513
+ <Elevated
514
+ offset={2}
515
+ shadowLevel={3}
516
+ ref={node => {
517
+ ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
518
+ if (typeof ref === 'function') ref(node)
519
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
520
+ }}
521
+ onMouseEnter={handlers.onMouseEnter}
522
+ onMouseMove={handlers.onMouseMove}
523
+ onMouseLeave={handlers.onMouseLeave}
524
+ onClick={handlers.onClick}
525
+ onFocus={e => {
526
+ const indexAttr = (e.target as HTMLElement)
527
+ .closest('[data-fluid-hover-index]')
528
+ ?.getAttribute('data-fluid-hover-index')
529
+ if (indexAttr != null) {
530
+ const idx = Number(indexAttr)
531
+ setActiveIndex(idx)
532
+ setFocusedIndex((e.target as HTMLElement).matches(':focus-visible') ? idx : null)
533
+ }
534
+ }}
535
+ onBlur={e => {
536
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return
537
+ setFocusedIndex(null)
538
+ setActiveIndex(null)
539
+ }}
540
+ onKeyDown={e => {
541
+ const items = Array.from(
542
+ containerRef.current?.querySelectorAll(
543
+ '[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"]',
544
+ ) ?? [],
545
+ ) as HTMLElement[]
546
+ const currentIdx = items.indexOf(e.target as HTMLElement)
547
+ if (currentIdx === -1) return
548
+
549
+ if (['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft'].includes(e.key)) {
550
+ e.preventDefault()
551
+ const next = ['ArrowDown', 'ArrowRight'].includes(e.key)
552
+ ? (currentIdx + 1) % items.length
553
+ : (currentIdx - 1 + items.length) % items.length
554
+ items[next]?.focus()
555
+ } else if (e.key === 'Home') {
556
+ e.preventDefault()
557
+ items[0]?.focus()
558
+ } else if (e.key === 'End') {
559
+ e.preventDefault()
560
+ items[items.length - 1]?.focus()
561
+ }
562
+ }}
563
+ role="group"
564
+ className={cn(`relative flex flex-col w-72 max-w-full p-1 select-none`, className)}
565
+ {...props}
566
+ >
567
+ {/* Selected backgrounds — merged runs in multiple mode and value-checked rows */}
568
+ <SelectionBackgrounds blocks={blocks} />
569
+
570
+ {/* Selected background */}
571
+ <AnimatePresence>
572
+ {checkedRect && (
573
+ <motion.div
574
+ className={`absolute ${shape.bg} bg-active pointer-events-none`}
575
+ initial={false}
576
+ animate={{
577
+ top: checkedRect.top,
578
+ left: checkedRect.left,
579
+ width: checkedRect.width,
580
+ height: checkedRect.height,
581
+ opacity: 1,
582
+ }}
583
+ exit={{ opacity: 0, transition: spring.moderate.exit }}
584
+ transition={{
585
+ ...spring.moderate,
586
+ opacity: { duration: spring.fast.duration },
587
+ }}
588
+ />
589
+ )}
590
+ </AnimatePresence>
591
+
592
+ {/* Hover background */}
593
+ <FluidHoverHighlight hover={hover} from={checkedRect} className={shape.bg} />
594
+
595
+ {/* Focus ring */}
596
+ <AnimatePresence>
597
+ {focusRect && (
598
+ <motion.div
599
+ className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-focus-ring`}
600
+ initial={false}
601
+ animate={{
602
+ left: focusRect.left - 2,
603
+ top: focusRect.top - 2,
604
+ width: focusRect.width + 4,
605
+ height: focusRect.height + 4,
606
+ }}
607
+ exit={{ opacity: 0, transition: spring.fast.exit }}
608
+ transition={{
609
+ ...spring.fast,
610
+ opacity: { duration: spring.fast.duration },
611
+ }}
612
+ />
613
+ )}
614
+ </AnimatePresence>
615
+
616
+ {rows.content}
617
+ </Elevated>
618
+ </DropdownFilterContext.Provider>
619
+ </DropdownContext.Provider>
620
+ </RowOrderContext.Provider>
621
+ )
622
+
623
+ // A size prop pins every row in the panel to one ladder step.
624
+ return size ? <SizeProvider size={size}>{panel}</SizeProvider> : panel
625
+ },
626
+ ) as DropdownComponent
627
+
628
+ Dropdown.displayName = 'Dropdown'
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // DropdownMenu (popup root)
632
+ //
633
+ // Built on Base UI's Menu primitive, which owns the trigger wiring,
634
+ // positioning (collision flipping, anchor tracking), dismissal (outside
635
+ // press, focus-out, Escape), roving highlight, typeahead, and close-on-select.
636
+ // This layer keeps the fluid-hover overlays and the morph open/close
637
+ // (lib/use-morph.ts) — the same pattern as select.tsx.
638
+ // ---------------------------------------------------------------------------
639
+
640
+ interface DropdownMenuContextValue {
641
+ open: boolean
642
+ origin: RefObject<MorphOrigin>
643
+ }
644
+
645
+ const DropdownMenuContext = createContext<DropdownMenuContextValue | null>(null)
646
+
647
+ function useDropdownMenuContext() {
648
+ const ctx = useContext(DropdownMenuContext)
649
+ if (!ctx) throw new Error('DropdownMenu compound components must be inside <DropdownMenu>')
650
+ return ctx
651
+ }
652
+
653
+ interface DropdownMenuProps {
654
+ /** `Dropdown.Trigger` and `Dropdown.Content`. */
655
+ children: ReactNode
656
+ /** Controlled open state. Pair with `onOpenChange`. */
657
+ open?: boolean
658
+ /** Initial open state — uncontrolled. Defaults to `false`. */
659
+ defaultOpen?: boolean
660
+ /** Called when the menu opens or closes. */
661
+ onOpenChange?: (open: boolean) => void
662
+ /** Disables the menu. Defaults to `false`. */
663
+ disabled?: boolean
664
+ /** Pins trigger-side content and the portalled popup rows to one step of the size ladder (default 36px, compact 28px — see Sizes). Omitted, they follow the surrounding SizeProvider. */
665
+ size?: SizeVariant
666
+ }
667
+
668
+ function DropdownMenu({
669
+ children,
670
+ open: openProp,
671
+ defaultOpen = false,
672
+ onOpenChange,
673
+ disabled = false,
674
+ size,
675
+ }: DropdownMenuProps) {
676
+ const [internalOpen, setInternalOpen] = useState(defaultOpen)
677
+ const open = openProp !== undefined ? openProp : internalOpen
678
+ const { origin, capture } = useMorphOrigin()
679
+
680
+ const handleOpenChange = useCallback(
681
+ (next: boolean, details: { trigger?: Element; event?: Event }) => {
682
+ if (next) capture(details)
683
+ if (openProp === undefined) setInternalOpen(next)
684
+ onOpenChange?.(next)
685
+ },
686
+ [openProp, onOpenChange, capture],
687
+ )
688
+
689
+ const ctx = useMemo(() => ({ open, origin }), [open, origin])
690
+
691
+ // A size prop pins the whole compound (trigger content + portalled popup —
692
+ // React context crosses portals) to one ladder step.
693
+ const root = (
694
+ <DropdownMenuContext.Provider value={ctx}>
695
+ <Menu.Root
696
+ open={open}
697
+ onOpenChange={handleOpenChange}
698
+ disabled={disabled}
699
+ // Non-modal: the page keeps scrolling and the Positioner tracks the
700
+ // anchor, so the popup follows its trigger instead of detaching.
701
+ modal={false}
702
+ >
703
+ {children}
704
+ </Menu.Root>
705
+ </DropdownMenuContext.Provider>
706
+ )
707
+
708
+ return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
709
+ }
710
+
711
+ DropdownMenu.displayName = 'DropdownMenu'
712
+
713
+ // ---------------------------------------------------------------------------
714
+ // DropdownTrigger
715
+ //
716
+ // Base UI's Menu.Trigger under the library name. Composes via the `render`
717
+ // prop, or Radix-style `asChild` onto its single child, so any element can
718
+ // be the trigger:
719
+ //
720
+ // <DropdownTrigger render={<Button variant="secondary">Open</Button>} />
721
+ // <DropdownTrigger asChild><Button variant="secondary">Open</Button></DropdownTrigger>
722
+ // ---------------------------------------------------------------------------
723
+
724
+ type DropdownTriggerProps = MenuTriggerProps & {
725
+ /** Compose onto the single child instead of `render`. Defaults to `false`. */
726
+ asChild?: boolean
727
+ }
728
+
729
+ const DropdownTrigger = forwardRef<HTMLButtonElement, DropdownTriggerProps>(
730
+ ({ asChild, render, children, ...props }, ref) =>
731
+ asChild && isValidElement(children) ? (
732
+ <Menu.Trigger ref={ref} render={children as ReactElement<Record<string, unknown>>} {...props} />
733
+ ) : (
734
+ <Menu.Trigger ref={ref} render={render} {...props}>
735
+ {children}
736
+ </Menu.Trigger>
737
+ ),
738
+ )
739
+
740
+ DropdownTrigger.displayName = 'DropdownTrigger'
741
+
742
+ // ---------------------------------------------------------------------------
743
+ // DropdownContent (popup panel)
744
+ //
745
+ // Portal > Positioner > Popup carrying the exact inline-panel visuals: the
746
+ // surface (painted by the morph layer), fluid-hover overlays, animated
747
+ // selected background, and animated focus ring. Children are wrapped in a Menu.RadioGroup so
748
+ // radio-style MenuItems (boolean `checked`) get correct aria-checked from
749
+ // `checkedIndex`.
750
+ // ---------------------------------------------------------------------------
751
+
752
+ type MenuPositionerProps = ComponentProps<typeof Menu.Positioner>
753
+
754
+ interface DropdownContentProps {
755
+ /** `Dropdown.Item` rows, plus `Dropdown.Label`, `Dropdown.Separator`, `Dropdown.Search`, `Dropdown.Empty`. */
756
+ children: ReactNode
757
+ /** Extra classes for the popup surface. */
758
+ className?: string
759
+ /** Index of the checked item. Drives the animated selected background and
760
+ * the radio-group value announced to assistive tech. */
761
+ checkedIndex?: number
762
+ /** Uncontrolled twin of `checkedIndex`: the popup owns the selection, so rows need no `checked` or `onSelect` of their own. */
763
+ defaultCheckedIndex?: number
764
+ /** Called with the newly checked row, or `undefined` when nothing is checked. */
765
+ onCheckedIndexChange?: (index: number | undefined) => void
766
+ /** Multiple selection: the checked rows. Rows become checkbox items that
767
+ * keep the menu open when toggled, and contiguous runs share one merged
768
+ * background (see Checkbox.Group). */
769
+ checkedIndices?: number[]
770
+ /** Uncontrolled twin of `checkedIndices`: the popup toggles rows itself. */
771
+ defaultCheckedIndices?: number[]
772
+ /** Called with every checked row after a toggle. */
773
+ onCheckedIndicesChange?: (indices: number[]) => void
774
+ /** Side of the trigger the popup prefers. Defaults to `"bottom"`. */
775
+ side?: MenuPositionerProps['side']
776
+ /** Alignment against the trigger. Defaults to `"start"`. */
777
+ align?: MenuPositionerProps['align']
778
+ /** Gap to the trigger, in px. Ignored when the menu opens in place over its trigger (`effect="morph"` with `hideSource`). Defaults to `6`. */
779
+ sideOffset?: number
780
+ /** Where the menu grows from (see Morph): the trigger, the pointer, its own center, a viewport edge, or a ref to any element. Defaults to `'trigger'`. */
781
+ from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
782
+ /** How it grows (see Morph): with the liquid goo neck, a plain morph, a slide or a fade. Defaults to `'goo'`. */
783
+ effect?: 'goo' | 'morph' | 'slide' | 'fade'
784
+ /** Hide the trigger while open, so it reads as turning into the menu. Defaults to `false`. */
785
+ hideSource?: boolean
786
+ /** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
787
+ tier?: 'moderate' | 'slow'
788
+ /** Keep the rows mounted (hidden) while the menu is closed, so rows that run their own state — `Dropdown.CheckboxItem defaultChecked`, `Dropdown.RadioGroup defaultValue`, an uncontrolled `Dropdown.Sub` — keep it across opens. Defaults to `false`. */
789
+ keepMounted?: boolean
790
+ }
791
+
792
+ const DropdownContent = forwardRef<HTMLDivElement, DropdownContentProps>(
793
+ (
794
+ {
795
+ className,
796
+ children,
797
+ checkedIndex,
798
+ defaultCheckedIndex,
799
+ onCheckedIndexChange,
800
+ checkedIndices,
801
+ defaultCheckedIndices,
802
+ onCheckedIndicesChange,
803
+ side = 'bottom',
804
+ align = 'start',
805
+ sideOffset = 6,
806
+ from,
807
+ effect,
808
+ hideSource,
809
+ tier = 'moderate',
810
+ keepMounted = false,
811
+ },
812
+ ref,
813
+ ) => {
814
+ const { open, origin } = useDropdownMenuContext()
815
+ const morph = useMorph(open, origin, { from, effect, hideSource, tier })
816
+ useImperativeHandle(ref, () => morph.popup as HTMLDivElement, [morph.popup])
817
+ // Lifts 2 levels off its substrate with a fixed shadow (see Elevated).
818
+ const level = Math.min(useSurface() + 2, 8)
819
+ const containerRef = useRef<HTMLDivElement>(null)
820
+
821
+ const panel = useMenuPanel(containerRef, open)
822
+ const { hover, focusRow, setFocusedIndex } = panel
823
+ const { setActiveIndex, itemRects, handlers, registerItem, remeasure } = hover
824
+
825
+ const rows = usePanelRows(children)
826
+ const selection = usePanelSelection(
827
+ {
828
+ checkedIndex,
829
+ defaultCheckedIndex,
830
+ onCheckedIndexChange,
831
+ checkedIndices,
832
+ defaultCheckedIndices,
833
+ onCheckedIndicesChange,
834
+ },
835
+ rows.sourceOf,
836
+ rows.filtering,
837
+ )
838
+
839
+ // An optional DropdownSearch child: typing on a focused row is
840
+ // redirected into the field. (The field takes focus itself, a frame
841
+ // after the primitive's own open autofocus.)
842
+ const {
843
+ host: searchHost,
844
+ hasSearch,
845
+ searchMounted,
846
+ onKeyDownCapture: redirectTypingToSearch,
847
+ isSearchField,
848
+ highlightFirst,
849
+ } = useDropdownSearchHost(open, { containerRef, setActiveIndex })
850
+
851
+ // Open ready to act: focus the first enabled row (a mounted search field
852
+ // takes focus itself instead). A frame after the primitive's own open
853
+ // autofocus, which lands on the popup for pointer opens.
854
+ useEffect(() => {
855
+ if (!open) return
856
+ let inner: number | undefined
857
+ const outer = requestAnimationFrame(() => {
858
+ inner = requestAnimationFrame(() => {
859
+ if (hasSearch()) return
860
+ const container = containerRef.current
861
+ if (!container || (container.contains(document.activeElement) && document.activeElement !== container)) return
862
+ const first = container.querySelector<HTMLElement>(
863
+ '[role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"])',
864
+ )
865
+ first?.focus()
866
+ })
867
+ })
868
+ return () => {
869
+ cancelAnimationFrame(outer)
870
+ if (inner !== undefined) cancelAnimationFrame(inner)
871
+ }
872
+ }, [open, hasSearch])
873
+
874
+ // The popup keeps its rows registered between opens, so their rects
875
+ // were taken while it was hidden: re-measure once it is open and laid out.
876
+ useEffect(() => {
877
+ if (!open) return
878
+ remeasure()
879
+ }, [open, remeasure])
880
+
881
+ const multiple = selection.multiple
882
+ const checkedRect = !multiple && selection.checkedIndex != null ? itemRects[selection.checkedIndex] : null
883
+ // Multiple: one merged block per contiguous run of checked rows, value-
884
+ // checked rows (CheckboxItem, RadioItem) included.
885
+ const runs = useSelectionRuns(
886
+ useMemo(
887
+ () => [...(selection.checkedIndices ?? NO_INDICES), ...panel.valueChecked],
888
+ [selection.checkedIndices, panel.valueChecked],
889
+ ),
890
+ )
891
+ const blocks = useMergeSplitBlocks(runs, open ? itemRects : NO_RECTS, shape.bgRadius)
892
+ // Inside the popup, Base UI's Menu.Item / Menu.RadioItem own the role,
893
+ // aria-checked, tabIndex, roving highlight, typeahead, and Enter/Space/
894
+ // click activation. Keyboard activation reaches only the primitive's own
895
+ // onClick, so the row's activation rides there. The render div carries the
896
+ // Fluid Functionalism visuals and the fluid-hover registration.
897
+ // (`renderPopupItem`, shared with Dropdown.SubContent.)
898
+
899
+ const contentCtx = useMemo(
900
+ () => ({
901
+ registerItem,
902
+ activeIndex: panel.lit,
903
+ checkedIndex: selection.checkedIndex,
904
+ multiple,
905
+ checkedIndices: selection.checkedIndices,
906
+ inMenu: true,
907
+ renderMenuItem: renderPopupItem,
908
+ selfManaged: selection.selfManaged,
909
+ toggleIndex: selection.toggleIndex,
910
+ pin: panel.pin,
911
+ reportChecked: panel.reportChecked,
912
+ }),
913
+ [
914
+ registerItem,
915
+ panel.lit,
916
+ selection.checkedIndex,
917
+ multiple,
918
+ selection.checkedIndices,
919
+ selection.selfManaged,
920
+ selection.toggleIndex,
921
+ panel.pin,
922
+ panel.reportChecked,
923
+ ],
924
+ )
925
+
926
+ return (
927
+ <Menu.Portal keepMounted={keepMounted}>
928
+ <Menu.Positioner
929
+ side={side}
930
+ align={align}
931
+ sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : sideOffset}
932
+ className="z-50 outline-none"
933
+ >
934
+ <DropdownContext.Provider value={contentCtx}>
935
+ <DropdownFilterContext.Provider value={rows.filterCtx}>
936
+ <DropdownSearchHostContext.Provider value={searchHost}>
937
+ <Menu.Popup
938
+ ref={morph.popupRef}
939
+ onKeyDownCapture={e => {
940
+ // A submenu's keys bubble here through the portal.
941
+ if (!ownEvent(e)) return
942
+ panel.trackKeyboardNav(e)
943
+ redirectTypingToSearch(e)
944
+ }}
945
+ onMouseEnter={() => {
946
+ handlers.onMouseEnter()
947
+ setFocusedIndex(null)
948
+ }}
949
+ onMouseMove={e => ownEvent(e) && handlers.onMouseMove(e)}
950
+ onClick={e => ownEvent(e) && handlers.onClick(e)}
951
+ onMouseLeave={() => {
952
+ handlers.onMouseLeave()
953
+ // The pointer's session is over; a focused search field
954
+ // gets its first-row highlight back.
955
+ if (isSearchField(document.activeElement)) highlightFirst()
956
+ }}
957
+ onFocus={e => {
958
+ // Keyboard navigation moves the hover background, and the
959
+ // ring once the keyboard earned it (useKeyboardNavGate).
960
+ if (!ownEvent(e) || focusRow(e)) return
961
+ if (isSearchField(e.target)) {
962
+ // The search field: the first row (what Enter picks)
963
+ // carries the highlight while it has focus.
964
+ highlightFirst()
965
+ } else if (e.target !== e.currentTarget) {
966
+ // Focus moved to some other non-row inside the popup: no
967
+ // row is highlighted any more. The popup focusing itself
968
+ // (pointer leaving a row) doesn't count.
969
+ setActiveIndex(null)
970
+ }
971
+ }}
972
+ onBlur={e => {
973
+ // The popup itself takes focus when the pointer leaves a row; only a
974
+ // departure from the whole popup ends the hover session.
975
+ if (!ownEvent(e) || e.currentTarget.contains(e.relatedTarget as Node)) return
976
+ setFocusedIndex(null)
977
+ setActiveIndex(null)
978
+ }}
979
+ className="relative select-none outline-none"
980
+ >
981
+ <SurfaceProvider value={level}>
982
+ <MorphSurface
983
+ morph={morph}
984
+ bg={SURFACE_BG[level]}
985
+ shadow={SURFACE_SHADOW[3]}
986
+ radius={shape.container}
987
+ className={cn(
988
+ // min-w tracks the trigger via the Positioner's
989
+ // --anchor-width var.
990
+ `flex flex-col w-72 max-w-full min-w-[var(--anchor-width)] max-h-[min(480px,var(--available-height))] overflow-hidden ${shape.container}`,
991
+ className,
992
+ )}
993
+ >
994
+ {/* The list scrolls inside a ScrollArea; this wrapper is the rows'
995
+ offsetParent, so the overlays scroll with them. */}
996
+ <ScrollArea
997
+ className={popupScrollAreaClass}
998
+ viewportClassName={cn(popupViewportClass, !searchMounted && 'scroll-fade')}
999
+ >
1000
+ <div ref={containerRef} className="relative flex flex-col p-1">
1001
+ {/* Selected backgrounds — merged runs in multiple mode and value-checked rows */}
1002
+ <SelectionBackgrounds blocks={blocks} />
1003
+
1004
+ {/* Selected background */}
1005
+ <AnimatePresence>
1006
+ {checkedRect && (
1007
+ <motion.div
1008
+ className={`absolute ${shape.bg} bg-active pointer-events-none`}
1009
+ initial={false}
1010
+ animate={{
1011
+ top: checkedRect.top,
1012
+ left: checkedRect.left,
1013
+ width: checkedRect.width,
1014
+ height: checkedRect.height,
1015
+ opacity: 1,
1016
+ }}
1017
+ exit={{ opacity: 0, transition: spring.moderate.exit }}
1018
+ transition={{
1019
+ ...spring.moderate,
1020
+ opacity: { duration: spring.fast.duration },
1021
+ }}
1022
+ />
1023
+ )}
1024
+ </AnimatePresence>
1025
+
1026
+ {/* Hover background (pinned on an open submenu's trigger) */}
1027
+ <FluidHoverHighlight hover={panel.shownHover} from={checkedRect} className={shape.bg} />
1028
+
1029
+ {/* Focus ring, keyboard only */}
1030
+ <FocusRing rect={open ? panel.focusRect : null} shape="rounded" />
1031
+
1032
+ {/* display: contents keeps items direct flex children of the
1033
+ wrapper so fluid hover measurement and gap layout still work,
1034
+ while the group provides the radio value context. */}
1035
+ <RowOrderContext.Provider value={panel.rowOrder}>
1036
+ <Menu.RadioGroup value={selection.checkedIndex ?? null} className="contents">
1037
+ {rows.content}
1038
+ </Menu.RadioGroup>
1039
+ </RowOrderContext.Provider>
1040
+ </div>
1041
+ </ScrollArea>
1042
+ </MorphSurface>
1043
+ </SurfaceProvider>
1044
+ </Menu.Popup>
1045
+ </DropdownSearchHostContext.Provider>
1046
+ </DropdownFilterContext.Provider>
1047
+ </DropdownContext.Provider>
1048
+ </Menu.Positioner>
1049
+ </Menu.Portal>
1050
+ )
1051
+ },
1052
+ )
1053
+
1054
+ DropdownContent.displayName = 'DropdownContent'
1055
+
1056
+ // ---------------------------------------------------------------------------
1057
+ // DropdownLabel
1058
+ // ---------------------------------------------------------------------------
1059
+
1060
+ interface DropdownLabelProps extends HTMLAttributes<HTMLDivElement> {
1061
+ /** The caption. */
1062
+ children?: ReactNode
1063
+ /** Indents the caption to the label column of rows with an icon. Defaults to `false`. */
1064
+ inset?: boolean
1065
+ /** Extra classes. */
1066
+ className?: string
1067
+ }
1068
+
1069
+ const DropdownLabel = forwardRef<HTMLDivElement, DropdownLabelProps>(({ className, inset, ...props }, ref) => {
1070
+ // Group labels are the caption role of the type scale — see /docs/sizes.
1071
+ const size = useSize()
1072
+ const own = useRef<HTMLDivElement>(null)
1073
+ // A place in the row order, so a selection run never melts across it.
1074
+ useRowSlot(own, undefined, true)
1075
+ const classes = cn(
1076
+ 'py-1.5 shrink-0 text-muted-foreground',
1077
+ inset ? INSET_PX[size.variant] : 'px-2',
1078
+ size.variant === 'compact' ? 'text-caption-compact' : 'text-caption',
1079
+ className,
1080
+ )
1081
+ // Inside a Dropdown.Group it names the group.
1082
+ return useContext(DropdownGroupContext) ? (
1083
+ <Menu.GroupLabel ref={mergeRefs(own, ref)} className={classes} {...props} />
1084
+ ) : (
1085
+ <div ref={mergeRefs(own, ref)} className={classes} {...props} />
1086
+ )
1087
+ })
1088
+
1089
+ DropdownLabel.displayName = 'DropdownLabel'
1090
+
1091
+ // ---------------------------------------------------------------------------
1092
+ // DropdownSeparator
1093
+ // ---------------------------------------------------------------------------
1094
+
1095
+ const DropdownSeparator = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
1096
+ const own = useRef<HTMLDivElement>(null)
1097
+ useRowSlot(own, undefined, true)
1098
+ return (
1099
+ <div
1100
+ ref={mergeRefs(own, ref)}
1101
+ role="separator"
1102
+ className={cn('my-1 -mx-1 h-px shrink-0 bg-border/60', className)}
1103
+ {...props}
1104
+ />
1105
+ )
1106
+ })
1107
+
1108
+ DropdownSeparator.displayName = 'DropdownSeparator'
1109
+
1110
+ Object.assign(Dropdown, {
1111
+ Menu: DropdownMenu,
1112
+ Trigger: DropdownTrigger,
1113
+ Content: DropdownContent,
1114
+ Item: MenuItem,
1115
+ Label: DropdownLabel,
1116
+ Separator: DropdownSeparator,
1117
+ Search: DropdownSearch,
1118
+ Empty: DropdownEmpty,
1119
+ Group: DropdownGroup,
1120
+ Shortcut: DropdownShortcut,
1121
+ CheckboxItem: DropdownCheckboxItem,
1122
+ RadioGroup: DropdownRadioGroup,
1123
+ RadioItem: DropdownRadioItem,
1124
+ Sub: DropdownSub,
1125
+ SubTrigger: DropdownSubTrigger,
1126
+ SubContent: DropdownSubContent,
1127
+ })
1128
+
1129
+ // DropdownContextValue and MenuItemRenderOptions are already re-exported
1130
+ // above next to their import — repeating them here is a duplicate-export
1131
+ // build error.
1132
+ export type {
1133
+ DropdownCheckboxItemProps,
1134
+ DropdownContentProps,
1135
+ DropdownGroupProps,
1136
+ DropdownLabelProps,
1137
+ DropdownMenuProps,
1138
+ DropdownProps,
1139
+ DropdownRadioGroupProps,
1140
+ DropdownRadioItemProps,
1141
+ DropdownSearchProps,
1142
+ DropdownShortcutProps,
1143
+ DropdownSubContentProps,
1144
+ DropdownSubProps,
1145
+ DropdownSubTriggerProps,
1146
+ DropdownTriggerProps,
1147
+ MenuItemProps,
1148
+ }
1149
+ export {
1150
+ Dropdown,
1151
+ DropdownCheckboxItem,
1152
+ DropdownContent,
1153
+ DropdownEmpty,
1154
+ DropdownGroup,
1155
+ DropdownLabel,
1156
+ DropdownMenu,
1157
+ DropdownRadioGroup,
1158
+ DropdownRadioItem,
1159
+ DropdownSearch,
1160
+ DropdownSeparator,
1161
+ DropdownShortcut,
1162
+ DropdownSub,
1163
+ DropdownSubContent,
1164
+ DropdownSubTrigger,
1165
+ DropdownTrigger,
1166
+ MenuItem,
1167
+ }
1168
+ export default Dropdown