@boxcustodia/library 2.0.0-alpha.11 → 2.0.0-alpha.13

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 (352) hide show
  1. package/dist/index.cjs.js +38 -38
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +28 -29
  4. package/dist/index.es.js +6804 -6792
  5. package/package.json +4 -3
  6. package/src/__doc__/Changelog.mdx +6 -0
  7. package/src/__doc__/Components.mdx +73 -0
  8. package/src/__doc__/Examples.tsx +69 -0
  9. package/src/__doc__/Icons.mdx +41 -0
  10. package/src/__doc__/Intro.mdx +138 -0
  11. package/src/__doc__/MCP.mdx +71 -0
  12. package/src/__doc__/Migration.mdx +451 -0
  13. package/src/__doc__/Theme.mdx +132 -0
  14. package/src/__doc__/Types.mdx +252 -0
  15. package/src/components/alert/alert.stories.tsx +142 -0
  16. package/src/components/alert/alert.tsx +109 -0
  17. package/src/components/alert/index.ts +7 -0
  18. package/src/components/alert-dialog/alert-dialog.stories.tsx +173 -0
  19. package/src/components/alert-dialog/alert-dialog.test.tsx +49 -0
  20. package/src/components/alert-dialog/alert-dialog.tsx +265 -0
  21. package/src/components/alert-dialog/index.ts +1 -0
  22. package/src/components/auto-complete/auto-complete-primitives.tsx +155 -0
  23. package/src/components/auto-complete/auto-complete.stories.tsx +241 -0
  24. package/src/components/auto-complete/auto-complete.tsx +82 -0
  25. package/src/components/auto-complete/index.ts +2 -0
  26. package/src/components/avatar/avatar.stories.tsx +84 -0
  27. package/src/components/avatar/avatar.test.tsx +61 -0
  28. package/src/components/avatar/avatar.tsx +104 -0
  29. package/src/components/avatar/index.ts +1 -0
  30. package/src/components/background-image/background-image.stories.tsx +21 -0
  31. package/src/components/background-image/background-image.test.tsx +29 -0
  32. package/src/components/background-image/background-image.tsx +23 -0
  33. package/src/components/background-image/index.ts +1 -0
  34. package/src/components/button/button.stories.tsx +396 -0
  35. package/src/components/button/button.test.tsx +58 -0
  36. package/src/components/button/button.tsx +31 -0
  37. package/src/components/button/button.variants.ts +44 -0
  38. package/src/components/button/components/base-button.tsx +86 -0
  39. package/src/components/button/components/loader-overlay.tsx +21 -0
  40. package/src/components/button/components/loading-icon.tsx +47 -0
  41. package/src/components/button/index.ts +3 -0
  42. package/src/components/calendar/calendar.model.ts +86 -0
  43. package/src/components/calendar/calendar.stories.tsx +155 -0
  44. package/src/components/calendar/calendar.test.tsx +12 -0
  45. package/src/components/calendar/calendar.tsx +185 -0
  46. package/src/components/calendar/components/calendar-navigation.tsx +141 -0
  47. package/src/components/calendar/components/day.tsx +61 -0
  48. package/src/components/calendar/components/decade-view.tsx +45 -0
  49. package/src/components/calendar/components/index.ts +6 -0
  50. package/src/components/calendar/components/month-view.tsx +58 -0
  51. package/src/components/calendar/components/week-days.tsx +27 -0
  52. package/src/components/calendar/components/year-view.tsx +29 -0
  53. package/src/components/calendar/hooks/index.ts +4 -0
  54. package/src/components/calendar/hooks/use-calendar-navigation.ts +79 -0
  55. package/src/components/calendar/hooks/use-calendar.ts +90 -0
  56. package/src/components/calendar/hooks/use-multiple-calendar.ts +34 -0
  57. package/src/components/calendar/hooks/use-range-calendar.ts +91 -0
  58. package/src/components/calendar/hooks/use-single-calendar.ts +18 -0
  59. package/src/components/calendar/index.ts +1 -0
  60. package/src/components/calendar/utils/typeguards.ts +7 -0
  61. package/src/components/card/card.stories.tsx +116 -0
  62. package/src/components/card/card.tsx +74 -0
  63. package/src/components/card/index.ts +1 -0
  64. package/src/components/center/center.stories.tsx +81 -0
  65. package/src/components/center/center.tsx +24 -0
  66. package/src/components/center/index.ts +1 -0
  67. package/src/components/checkbox/checkbox.stories.tsx +307 -0
  68. package/src/components/checkbox/checkbox.tsx +273 -0
  69. package/src/components/checkbox/index.ts +1 -0
  70. package/src/components/checkbox-group/checkbox-group.stories.tsx +104 -0
  71. package/src/components/checkbox-group/checkbox-group.tsx +16 -0
  72. package/src/components/checkbox-group/index.ts +1 -0
  73. package/src/components/combobox/combobox.stories.tsx +339 -0
  74. package/src/components/combobox/combobox.tsx +898 -0
  75. package/src/components/combobox/index.ts +1 -0
  76. package/src/components/date-picker/date-input.stories.tsx +158 -0
  77. package/src/components/date-picker/date-input.tsx +163 -0
  78. package/src/components/date-picker/date-picker.model.ts +90 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +200 -0
  80. package/src/components/date-picker/date-picker.test.tsx +23 -0
  81. package/src/components/date-picker/date-picker.tsx +298 -0
  82. package/src/components/date-picker/date-picker.utils.ts +260 -0
  83. package/src/components/date-picker/index.ts +3 -0
  84. package/src/components/date-picker/use-date-input-popover.ts +48 -0
  85. package/src/components/date-picker/use-date-input.ts +125 -0
  86. package/src/components/dialog/dialog.stories.tsx +171 -0
  87. package/src/components/dialog/dialog.test.tsx +68 -0
  88. package/src/components/dialog/dialog.tsx +277 -0
  89. package/src/components/dialog/index.ts +1 -0
  90. package/src/components/divider/divider.stories.tsx +139 -0
  91. package/src/components/divider/divider.test.tsx +22 -0
  92. package/src/components/divider/divider.tsx +23 -0
  93. package/src/components/divider/index.ts +1 -0
  94. package/src/components/dropzone/dropzone.stories.tsx +210 -0
  95. package/src/components/dropzone/dropzone.tsx +154 -0
  96. package/src/components/dropzone/file-types.ts +64 -0
  97. package/src/components/dropzone/index.ts +3 -0
  98. package/src/components/dropzone/upload-primitives.tsx +310 -0
  99. package/src/components/dropzone/use-dropzone.ts +122 -0
  100. package/src/components/empty-state/empty-state.stories.tsx +56 -0
  101. package/src/components/empty-state/empty-state.tsx +39 -0
  102. package/src/components/empty-state/index.ts +1 -0
  103. package/src/components/field/field.stories.tsx +223 -0
  104. package/src/components/field/field.tsx +229 -0
  105. package/src/components/field/index.ts +1 -0
  106. package/src/components/form/form.stories.tsx +594 -0
  107. package/src/components/form/form.tsx +30 -0
  108. package/src/components/form/index.ts +1 -0
  109. package/src/components/heading/heading.stories.tsx +74 -0
  110. package/src/components/heading/heading.tsx +28 -0
  111. package/src/components/heading/heading.variants.ts +27 -0
  112. package/src/components/heading/index.ts +1 -0
  113. package/src/components/index.ts +46 -0
  114. package/src/components/input/index.ts +1 -0
  115. package/src/components/input/input.stories.tsx +104 -0
  116. package/src/components/input/input.tsx +75 -0
  117. package/src/components/kbd/index.ts +1 -0
  118. package/src/components/kbd/kbd.stories.tsx +40 -0
  119. package/src/components/kbd/kbd.tsx +31 -0
  120. package/src/components/kbd/kbd.variants.ts +26 -0
  121. package/src/components/label/index.ts +1 -0
  122. package/src/components/label/label.stories.tsx +68 -0
  123. package/src/components/label/label.test.tsx +61 -0
  124. package/src/components/label/label.tsx +62 -0
  125. package/src/components/loader/index.ts +1 -0
  126. package/src/components/loader/loader.stories.tsx +60 -0
  127. package/src/components/loader/loader.test.tsx +26 -0
  128. package/src/components/loader/loader.tsx +60 -0
  129. package/src/components/menu/index.ts +2 -0
  130. package/src/components/menu/menu-primitives.tsx +248 -0
  131. package/src/components/menu/menu.stories.tsx +203 -0
  132. package/src/components/menu/menu.tsx +100 -0
  133. package/src/components/menu/util/render-menu-item.tsx +54 -0
  134. package/src/components/multi-select/hooks/use-multi-select.ts +66 -0
  135. package/src/components/multi-select/index.ts +1 -0
  136. package/src/components/multi-select/multi-select.stories.tsx +294 -0
  137. package/src/components/multi-select/multi-select.tsx +300 -0
  138. package/src/components/multi-select/multi-select.variants.ts +22 -0
  139. package/src/components/number-input/index.ts +1 -0
  140. package/src/components/number-input/number-input.stories.tsx +209 -0
  141. package/src/components/number-input/number-input.test.tsx +87 -0
  142. package/src/components/number-input/number-input.tsx +232 -0
  143. package/src/components/pagination/components/pagination-option.tsx +27 -0
  144. package/src/components/pagination/index.ts +1 -0
  145. package/src/components/pagination/pagination.stories.tsx +80 -0
  146. package/src/components/pagination/pagination.test.tsx +76 -0
  147. package/src/components/pagination/pagination.tsx +102 -0
  148. package/src/components/password/index.ts +1 -0
  149. package/src/components/password/password.stories.tsx +104 -0
  150. package/src/components/password/password.tsx +75 -0
  151. package/src/components/popover/index.ts +1 -0
  152. package/src/components/popover/popover.stories.tsx +213 -0
  153. package/src/components/popover/popover.tsx +203 -0
  154. package/src/components/progress/index.ts +1 -0
  155. package/src/components/progress/progress.stories.tsx +124 -0
  156. package/src/components/progress/progress.test.tsx +25 -0
  157. package/src/components/progress/progress.tsx +124 -0
  158. package/src/components/scroll-area/index.ts +1 -0
  159. package/src/components/scroll-area/scroll-area.stories.tsx +166 -0
  160. package/src/components/scroll-area/scroll-area.tsx +64 -0
  161. package/src/components/select/index.ts +1 -0
  162. package/src/components/select/select.stories.tsx +253 -0
  163. package/src/components/select/select.tsx +430 -0
  164. package/src/components/show/index.ts +1 -0
  165. package/src/components/show/show.stories.tsx +197 -0
  166. package/src/components/show/show.test.tsx +41 -0
  167. package/src/components/show/show.tsx +16 -0
  168. package/src/components/skeleton/index.ts +1 -0
  169. package/src/components/skeleton/skeleton.stories.tsx +36 -0
  170. package/src/components/skeleton/skeleton.test.tsx +14 -0
  171. package/src/components/skeleton/skeleton.tsx +15 -0
  172. package/src/components/stack/index.ts +1 -0
  173. package/src/components/stack/stack.stories.tsx +194 -0
  174. package/src/components/stack/stack.tsx +52 -0
  175. package/src/components/stepper/Stepper.tsx +190 -0
  176. package/src/components/stepper/context/stepper-context.tsx +11 -0
  177. package/src/components/stepper/index.ts +1 -0
  178. package/src/components/stepper/stepper.stories.tsx +130 -0
  179. package/src/components/stepper/stepper.test.tsx +91 -0
  180. package/src/components/switch/index.ts +1 -0
  181. package/src/components/switch/switch.stories.tsx +122 -0
  182. package/src/components/switch/switch.test.tsx +30 -0
  183. package/src/components/switch/switch.tsx +86 -0
  184. package/src/components/table/index.ts +3 -0
  185. package/src/components/table/table-primitives.tsx +122 -0
  186. package/src/components/table/table.model.ts +20 -0
  187. package/src/components/table/table.stories.tsx +169 -0
  188. package/src/components/table/table.test.tsx +91 -0
  189. package/src/components/table/table.tsx +109 -0
  190. package/src/components/table-pagination/index.ts +2 -0
  191. package/src/components/table-pagination/table-pagination.model.ts +2 -0
  192. package/src/components/table-pagination/table-pagination.stories.tsx +23 -0
  193. package/src/components/table-pagination/table-pagination.test.tsx +32 -0
  194. package/src/components/table-pagination/table-pagination.tsx +108 -0
  195. package/src/components/tabs/context/tabs-context.tsx +14 -0
  196. package/src/components/tabs/index.ts +1 -0
  197. package/src/components/tabs/tabs.stories.tsx +182 -0
  198. package/src/components/tabs/tabs.test.tsx +61 -0
  199. package/src/components/tabs/tabs.tsx +175 -0
  200. package/src/components/tag/index.ts +2 -0
  201. package/src/components/tag/tag.stories.tsx +170 -0
  202. package/src/components/tag/tag.test.tsx +18 -0
  203. package/src/components/tag/tag.tsx +99 -0
  204. package/src/components/tag/tag.variants.ts +31 -0
  205. package/src/components/textarea/index.ts +1 -0
  206. package/src/components/textarea/textarea.stories.tsx +73 -0
  207. package/src/components/textarea/textarea.tsx +105 -0
  208. package/src/components/timeline/index.ts +1 -0
  209. package/src/components/timeline/timeline-status.ts +5 -0
  210. package/src/components/timeline/timeline.stories.tsx +84 -0
  211. package/src/components/timeline/timeline.tsx +147 -0
  212. package/src/components/toast/index.ts +1 -0
  213. package/src/components/toast/toast.stories.tsx +392 -0
  214. package/src/components/toast/toast.test.tsx +50 -0
  215. package/src/components/toast/toast.tsx +411 -0
  216. package/src/components/tooltip/index.ts +1 -0
  217. package/src/components/tooltip/tooltip.stories.tsx +226 -0
  218. package/src/components/tooltip/tooltip.test.tsx +46 -0
  219. package/src/components/tooltip/tooltip.tsx +171 -0
  220. package/src/components/tree/hooks/use-controllable-tree-state.ts +80 -0
  221. package/src/components/tree/index.ts +2 -0
  222. package/src/components/tree/tree-primitives.tsx +126 -0
  223. package/src/components/tree/tree.stories.tsx +468 -0
  224. package/src/components/tree/tree.tsx +42 -0
  225. package/src/hooks/index.ts +26 -0
  226. package/src/hooks/useArray/__doc__/useArray.stories.tsx +100 -0
  227. package/src/hooks/useArray/__test__/useArray.test.tsx +88 -0
  228. package/src/hooks/useArray/index.ts +1 -0
  229. package/src/hooks/useArray/useArray.ts +76 -0
  230. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +149 -0
  231. package/src/hooks/useAsync/__test__/useAsync.test.tsx +68 -0
  232. package/src/hooks/useAsync/index.ts +1 -0
  233. package/src/hooks/useAsync/useAsync.ts +58 -0
  234. package/src/hooks/useClickOutside/__doc__/useClickOutside.stories.tsx +40 -0
  235. package/src/hooks/useClickOutside/__test__/useClickOutside.test.tsx +33 -0
  236. package/src/hooks/useClickOutside/index.ts +1 -0
  237. package/src/hooks/useClickOutside/useClickOutside.ts +26 -0
  238. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +45 -0
  239. package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +19 -0
  240. package/src/hooks/useClipboard/index.ts +1 -0
  241. package/src/hooks/useClipboard/useClipboard.tsx +28 -0
  242. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +84 -0
  243. package/src/hooks/useDebounceCallback/index.ts +1 -0
  244. package/src/hooks/useDebounceCallback/useDebouncedCallback.ts +23 -0
  245. package/src/hooks/useDebounceValue/__doc__/useDebouncedValue.stories.tsx +75 -0
  246. package/src/hooks/useDebounceValue/index.ts +1 -0
  247. package/src/hooks/useDebounceValue/useDebouncedValue.ts +17 -0
  248. package/src/hooks/useDisclosure/__doc__/useDisclosure.stories.tsx +39 -0
  249. package/src/hooks/useDisclosure/__test__/useDisclosure.test.ts +43 -0
  250. package/src/hooks/useDisclosure/index.ts +1 -0
  251. package/src/hooks/useDisclosure/useDisclosure.ts +37 -0
  252. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +26 -0
  253. package/src/hooks/useDocumentTitle/index.ts +1 -0
  254. package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +11 -0
  255. package/src/hooks/useEventListener/__doc__/useEventListener.stories.tsx +28 -0
  256. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +26 -0
  257. package/src/hooks/useEventListener/index.ts +1 -0
  258. package/src/hooks/useEventListener/useEventListener.ts +25 -0
  259. package/src/hooks/useFocusTrap/__doc__/useFocusTrap.stories.tsx +37 -0
  260. package/src/hooks/useFocusTrap/index.ts +1 -0
  261. package/src/hooks/useFocusTrap/scopeTab.ts +38 -0
  262. package/src/hooks/useFocusTrap/tabbable.ts +70 -0
  263. package/src/hooks/useFocusTrap/useFocusTrap.ts +78 -0
  264. package/src/hooks/useHotkey/__docs__/useHotkey.stories.tsx +116 -0
  265. package/src/hooks/useHotkey/__test__/useHotkey.test.tsx +105 -0
  266. package/src/hooks/useHotkey/__utils__/create-hotkey-listener.ts +25 -0
  267. package/src/hooks/useHotkey/__utils__/index.ts +3 -0
  268. package/src/hooks/useHotkey/__utils__/is-input-field.ts +14 -0
  269. package/src/hooks/useHotkey/__utils__/match-key-modifiers.ts +25 -0
  270. package/src/hooks/useHotkey/index.ts +1 -0
  271. package/src/hooks/useHotkey/useHotkey.ts +34 -0
  272. package/src/hooks/useHover/__doc__/useHover.stories.tsx +41 -0
  273. package/src/hooks/useHover/__test__/useHover.test.tsx +45 -0
  274. package/src/hooks/useHover/index.ts +1 -0
  275. package/src/hooks/useHover/useHover.tsx +40 -0
  276. package/src/hooks/useIsVisible/__doc__/useIsVisible.stories.tsx +60 -0
  277. package/src/hooks/useIsVisible/index.ts +1 -0
  278. package/src/hooks/useIsVisible/useIsVisible.tsx +50 -0
  279. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +86 -0
  280. package/src/hooks/useLocalStorage/__test__/useLocalStorage.test.ts +85 -0
  281. package/src/hooks/useLocalStorage/index.ts +1 -0
  282. package/src/hooks/useLocalStorage/useLocalStorage.ts +57 -0
  283. package/src/hooks/useMediaQuery/__doc__/useMediaQuery.stories.tsx +39 -0
  284. package/src/hooks/useMediaQuery/index.ts +1 -0
  285. package/src/hooks/useMediaQuery/useMediaQuery.ts +22 -0
  286. package/src/hooks/useMemoizedFn/index.ts +1 -0
  287. package/src/hooks/useMemoizedFn/useMemoizedFn.ts +32 -0
  288. package/src/hooks/useMutation/__doc__/useMutation.stories.tsx +111 -0
  289. package/src/hooks/useMutation/__test__/useMutation.test.tsx +83 -0
  290. package/src/hooks/useMutation/index.ts +1 -0
  291. package/src/hooks/useMutation/useMutation.tsx +60 -0
  292. package/src/hooks/useObject/__doc__/useObject.stories.tsx +119 -0
  293. package/src/hooks/useObject/__test__/useObject.test.tsx +87 -0
  294. package/src/hooks/useObject/index.ts +1 -0
  295. package/src/hooks/useObject/useObject.tsx +48 -0
  296. package/src/hooks/usePagination/__doc__/usePagination.stories.tsx +72 -0
  297. package/src/hooks/usePagination/__test__/usePagination.test.tsx +98 -0
  298. package/src/hooks/usePagination/index.ts +2 -0
  299. package/src/hooks/usePagination/usePagination.tsx +74 -0
  300. package/src/hooks/usePortal/__doc__/usePortal.stories.tsx +19 -0
  301. package/src/hooks/usePortal/__test__/usePortal.test.tsx +20 -0
  302. package/src/hooks/usePortal/index.ts +1 -0
  303. package/src/hooks/usePortal/usePortal.ts +40 -0
  304. package/src/hooks/usePreventCloseWindow/__doc__/usePreventCloseWindow.stories.tsx +32 -0
  305. package/src/hooks/usePreventCloseWindow/index.ts +1 -0
  306. package/src/hooks/usePreventCloseWindow/usePreventCloseWindow.ts +33 -0
  307. package/src/hooks/useRangePagination/__test__/useRangePagination.test.tsx +63 -0
  308. package/src/hooks/useRangePagination/index.ts +2 -0
  309. package/src/hooks/useRangePagination/useRangePagination.tsx +72 -0
  310. package/src/hooks/useSelection/__doc__/useSelection.stories.tsx +140 -0
  311. package/src/hooks/useSelection/__test__/useSelection.test.tsx +57 -0
  312. package/src/hooks/useSelection/index.ts +1 -0
  313. package/src/hooks/useSelection/useSelection.ts +121 -0
  314. package/src/hooks/useStep/__doc__/useStep.stories.tsx +98 -0
  315. package/src/hooks/useStep/__test__/useStep.test.ts +51 -0
  316. package/src/hooks/useStep/index.ts +1 -0
  317. package/src/hooks/useStep/useStep.ts +57 -0
  318. package/src/hooks/useToggle/__doc__/useToggle.stories.tsx +25 -0
  319. package/src/hooks/useToggle/__test__/useToggle.test.tsx +43 -0
  320. package/src/hooks/useToggle/index.ts +1 -0
  321. package/src/hooks/useToggle/useToggle.ts +16 -0
  322. package/src/index.ts +6 -0
  323. package/src/lib/cn.ts +8 -0
  324. package/src/lib/index.ts +1 -0
  325. package/src/models/Generic.model.ts +67 -0
  326. package/src/models/index.ts +1 -0
  327. package/src/providers/index.ts +2 -0
  328. package/src/providers/library-provider.tsx +44 -0
  329. package/src/providers/theme/ThemeProvider.tsx +25 -0
  330. package/src/providers/theme/index.ts +3 -0
  331. package/src/providers/theme/types.ts +11 -0
  332. package/src/providers/theme/useThemeProps.ts +25 -0
  333. package/src/stores/theme.store.ts +31 -0
  334. package/src/styles/components.css +4 -0
  335. package/src/styles/index.css +2 -0
  336. package/src/styles/library.css +2 -0
  337. package/src/styles/theme.css +232 -0
  338. package/src/utils/dates/parseDateRange.utility.ts +39 -0
  339. package/src/utils/form.tsx +91 -0
  340. package/src/utils/functions/createSafeContext.ts +17 -0
  341. package/src/utils/functions/ensureReactElement.tsx +30 -0
  342. package/src/utils/functions/getFormData.ts +19 -0
  343. package/src/utils/functions/index.ts +4 -0
  344. package/src/utils/functions/mergeRefs.ts +18 -0
  345. package/src/utils/index.ts +3 -0
  346. package/src/utils/strings/extractInitials.utility.ts +10 -0
  347. package/src/utils/strings/index.ts +1 -0
  348. package/src/utils/tests/click.ts +3 -0
  349. package/src/utils/tests/index.ts +2 -0
  350. package/src/utils/tests/keyboard.ts +21 -0
  351. package/src/utils/tests/type.ts +6 -0
  352. package/dist/components.css +0 -2
@@ -0,0 +1,898 @@
1
+ import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
2
+ import { Field as FieldPrimitive } from "@base-ui/react/field";
3
+ import { ChevronsUpDownIcon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../../lib";
6
+ import { Input, inputBaseClasses } from "../input";
7
+ import { ScrollArea } from "../scroll-area";
8
+
9
+ // ─── Context ─────────────────────────────────────────────────────────────────
10
+
11
+ export const ComboboxContext: React.Context<{
12
+ chipsRef: React.RefObject<Element | null> | null;
13
+ multiple: boolean;
14
+ }> = React.createContext<{
15
+ chipsRef: React.RefObject<Element | null> | null;
16
+ multiple: boolean;
17
+ }>({
18
+ chipsRef: null,
19
+ multiple: false,
20
+ });
21
+
22
+ // ─── Primitives ───────────────────────────────────────────────────────────────
23
+
24
+ export function ComboboxRoot<
25
+ Value,
26
+ Multiple extends boolean | undefined = false,
27
+ >({
28
+ onChange,
29
+ ...props
30
+ }: Omit<ComboboxPrimitive.Root.Props<Value, Multiple>, "onValueChange"> & {
31
+ onChange?: ComboboxPrimitive.Root.Props<Value, Multiple>["onValueChange"];
32
+ }): React.ReactElement {
33
+ const chipsRef = React.useRef<Element | null>(null);
34
+ return (
35
+ <ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}>
36
+ <ComboboxPrimitive.Root onValueChange={onChange} {...props} />
37
+ </ComboboxContext.Provider>
38
+ );
39
+ }
40
+
41
+ export function ComboboxChipsInput({
42
+ className,
43
+ ...props
44
+ }: Omit<ComboboxPrimitive.Input.Props, "size"> & {
45
+ ref?: React.Ref<HTMLInputElement>;
46
+ }): React.ReactElement {
47
+ return (
48
+ <ComboboxPrimitive.Input
49
+ className={cn(
50
+ "min-w-12 flex-1 text-base outline-none placeholder:text-muted-foreground sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5",
51
+ className,
52
+ )}
53
+ data-slot="combobox-chips-input"
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ export function ComboboxInput({
60
+ className,
61
+ showTrigger = true,
62
+ showClear = false,
63
+ startAddon,
64
+ triggerProps,
65
+ clearProps,
66
+ ...props
67
+ }: Omit<ComboboxPrimitive.Input.Props, "size"> & {
68
+ showTrigger?: boolean;
69
+ showClear?: boolean;
70
+ startAddon?: React.ReactNode;
71
+ ref?: React.Ref<HTMLInputElement>;
72
+ triggerProps?: ComboboxPrimitive.Trigger.Props;
73
+ clearProps?: ComboboxPrimitive.Clear.Props;
74
+ }): React.ReactElement {
75
+ return (
76
+ <ComboboxPrimitive.InputGroup
77
+ className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64"
78
+ data-slot="combobox-input-group"
79
+ >
80
+ {startAddon && (
81
+ <div
82
+ aria-hidden="true"
83
+ className="pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5"
84
+ data-slot="combobox-start-addon"
85
+ >
86
+ {startAddon}
87
+ </div>
88
+ )}
89
+ <ComboboxPrimitive.Input
90
+ className={cn(
91
+ startAddon &&
92
+ "*:data-[slot=combobox-input]:ps-[calc(--spacing(8.5)-1px)] sm:*:data-[slot=combobox-input]:ps-[calc(--spacing(8)-1px)]",
93
+ "pr-6",
94
+ className,
95
+ )}
96
+ data-slot="combobox-input"
97
+ render={(props) => (
98
+ <Input
99
+ className="has-disabled:opacity-100"
100
+ nativeInput
101
+ {...props}
102
+ onChange={(_, event) => props?.onChange?.(event)}
103
+ />
104
+ )}
105
+ {...props}
106
+ />
107
+ <ComboboxEndAdornment
108
+ showTrigger={showTrigger}
109
+ showClear={showClear}
110
+ triggerProps={triggerProps}
111
+ clearProps={clearProps}
112
+ />
113
+ </ComboboxPrimitive.InputGroup>
114
+ );
115
+ }
116
+
117
+ export function ComboboxTrigger({
118
+ className,
119
+ children,
120
+ ...props
121
+ }: ComboboxPrimitive.Trigger.Props): React.ReactElement {
122
+ return (
123
+ <ComboboxPrimitive.Trigger
124
+ className={className}
125
+ data-slot="combobox-trigger"
126
+ {...props}
127
+ >
128
+ {children}
129
+ </ComboboxPrimitive.Trigger>
130
+ );
131
+ }
132
+
133
+ export function ComboboxSelectTrigger({
134
+ className,
135
+ placeholder,
136
+ children,
137
+ showClear,
138
+ clearProps,
139
+ ...props
140
+ }: ComboboxPrimitive.Trigger.Props & {
141
+ placeholder?: React.ReactNode;
142
+ showClear?: boolean;
143
+ clearProps?: ComboboxPrimitive.Clear.Props;
144
+ }): React.ReactElement {
145
+ const trigger = (
146
+ <ComboboxPrimitive.Trigger
147
+ className={cn(
148
+ "relative flex w-full items-center rounded-md border border-input bg-background px-3 py-2 pe-9 text-sm outline-none transition-shadow",
149
+ "focus-visible:border-ring",
150
+ "aria-invalid:border-error focus-visible:aria-invalid:ring-error/20",
151
+ "data-disabled:cursor-not-allowed data-disabled:opacity-50",
152
+ "data-placeholder:text-muted-foreground",
153
+ className,
154
+ )}
155
+ data-slot="combobox-select-trigger"
156
+ {...props}
157
+ >
158
+ {children ?? <ComboboxPrimitive.Value placeholder={placeholder} />}
159
+ </ComboboxPrimitive.Trigger>
160
+ );
161
+
162
+ if (!showClear) {
163
+ return (
164
+ <ComboboxPrimitive.Trigger
165
+ className={cn(
166
+ "relative flex w-full items-center rounded-md border border-input bg-background px-3 py-2 pe-9 text-sm outline-none transition-shadow",
167
+ "focus-visible:border-ring",
168
+ "aria-invalid:border-error focus-visible:aria-invalid:ring-error/20",
169
+ "data-disabled:cursor-not-allowed data-disabled:opacity-50",
170
+ "data-placeholder:text-muted-foreground",
171
+ className,
172
+ )}
173
+ data-slot="combobox-select-trigger"
174
+ {...props}
175
+ >
176
+ {children ?? (
177
+ <>
178
+ <ComboboxPrimitive.Value placeholder={placeholder} />
179
+ <div
180
+ aria-hidden="true"
181
+ className="pointer-events-none absolute end-0.5 top-1/2 inline-flex size-8 -translate-y-1/2 items-center justify-center text-muted-foreground opacity-80 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"
182
+ >
183
+ <ChevronsUpDownIcon />
184
+ </div>
185
+ </>
186
+ )}
187
+ </ComboboxPrimitive.Trigger>
188
+ );
189
+ }
190
+
191
+ return (
192
+ <div
193
+ className="relative w-full text-foreground has-disabled:opacity-64"
194
+ data-slot="combobox-select-trigger-group"
195
+ >
196
+ {trigger}
197
+ <ComboboxEndAdornment showTrigger showClear clearProps={clearProps} />
198
+ </div>
199
+ );
200
+ }
201
+
202
+ export function ComboboxSearchInput({
203
+ className,
204
+ ...props
205
+ }: Omit<ComboboxPrimitive.Input.Props, "size"> & {
206
+ ref?: React.Ref<HTMLInputElement>;
207
+ }): React.ReactElement {
208
+ return (
209
+ <div className="border-b p-2" data-slot="combobox-search-input-wrapper">
210
+ <ComboboxPrimitive.Input
211
+ className={cn(
212
+ "flex h-9 w-full rounded-md bg-transparent text-base placeholder:text-muted-foreground outline-none disabled:cursor-not-allowed disabled:opacity-64 sm:h-8 sm:text-sm",
213
+ className,
214
+ )}
215
+ autoComplete="off"
216
+ data-slot="combobox-search-input"
217
+ {...props}
218
+ />
219
+ </div>
220
+ );
221
+ }
222
+
223
+ export function ComboboxPopup({
224
+ className,
225
+ children,
226
+ side = "bottom",
227
+ sideOffset = 4,
228
+ alignOffset,
229
+ align = "start",
230
+ anchor: anchorProp,
231
+ portalProps,
232
+ ...props
233
+ }: ComboboxPrimitive.Popup.Props & {
234
+ align?: ComboboxPrimitive.Positioner.Props["align"];
235
+ sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"];
236
+ alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"];
237
+ side?: ComboboxPrimitive.Positioner.Props["side"];
238
+ anchor?: ComboboxPrimitive.Positioner.Props["anchor"];
239
+ portalProps?: ComboboxPrimitive.Portal.Props;
240
+ }): React.ReactElement {
241
+ const { chipsRef } = React.useContext(ComboboxContext);
242
+ const anchor = anchorProp ?? chipsRef;
243
+
244
+ return (
245
+ <ComboboxPrimitive.Portal {...portalProps}>
246
+ <ComboboxPrimitive.Positioner
247
+ align={align}
248
+ alignOffset={alignOffset}
249
+ anchor={anchor}
250
+ className="z-50 select-none"
251
+ data-slot="combobox-positioner"
252
+ side={side}
253
+ sideOffset={sideOffset}
254
+ >
255
+ <span
256
+ className={cn(
257
+ "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
258
+ className,
259
+ )}
260
+ >
261
+ <ComboboxPrimitive.Popup
262
+ className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
263
+ data-slot="combobox-popup"
264
+ {...props}
265
+ >
266
+ {children}
267
+ </ComboboxPrimitive.Popup>
268
+ </span>
269
+ </ComboboxPrimitive.Positioner>
270
+ </ComboboxPrimitive.Portal>
271
+ );
272
+ }
273
+
274
+ export function ComboboxItem({
275
+ className,
276
+ children,
277
+ ...props
278
+ }: ComboboxPrimitive.Item.Props): React.ReactElement {
279
+ return (
280
+ <ComboboxPrimitive.Item
281
+ className={cn(
282
+ "flex justify-between min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 px-2 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
283
+ className,
284
+ )}
285
+ data-slot="combobox-item"
286
+ {...props}
287
+ >
288
+ {children}
289
+ <ComboboxPrimitive.ItemIndicator className="col-end-1">
290
+ <svg
291
+ aria-hidden="true"
292
+ fill="none"
293
+ height="24"
294
+ stroke="currentColor"
295
+ strokeLinecap="round"
296
+ strokeLinejoin="round"
297
+ strokeWidth="2"
298
+ viewBox="0 0 24 24"
299
+ width="24"
300
+ xmlns="http://www.w3.org/2000/svg"
301
+ >
302
+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
303
+ </svg>
304
+ </ComboboxPrimitive.ItemIndicator>
305
+ </ComboboxPrimitive.Item>
306
+ );
307
+ }
308
+
309
+ export function ComboboxSeparator({
310
+ className,
311
+ ...props
312
+ }: ComboboxPrimitive.Separator.Props): React.ReactElement {
313
+ return (
314
+ <ComboboxPrimitive.Separator
315
+ className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
316
+ data-slot="combobox-separator"
317
+ {...props}
318
+ />
319
+ );
320
+ }
321
+
322
+ export function ComboboxGroup({
323
+ className,
324
+ ...props
325
+ }: ComboboxPrimitive.Group.Props): React.ReactElement {
326
+ return (
327
+ <ComboboxPrimitive.Group
328
+ className={cn("[[role=group]+&]:mt-1.5", className)}
329
+ data-slot="combobox-group"
330
+ {...props}
331
+ />
332
+ );
333
+ }
334
+
335
+ export function ComboboxGroupLabel({
336
+ className,
337
+ ...props
338
+ }: ComboboxPrimitive.GroupLabel.Props): React.ReactElement {
339
+ return (
340
+ <ComboboxPrimitive.GroupLabel
341
+ className={cn(
342
+ "px-2 py-1.5 font-medium text-muted-foreground text-xs",
343
+ className,
344
+ )}
345
+ data-slot="combobox-group-label"
346
+ {...props}
347
+ />
348
+ );
349
+ }
350
+
351
+ export function ComboboxEmpty({
352
+ className,
353
+ ...props
354
+ }: ComboboxPrimitive.Empty.Props): React.ReactElement {
355
+ return (
356
+ <ComboboxPrimitive.Empty
357
+ className={cn(
358
+ "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
359
+ className,
360
+ )}
361
+ data-slot="combobox-empty"
362
+ {...props}
363
+ />
364
+ );
365
+ }
366
+
367
+ export function ComboboxRow({
368
+ className,
369
+ ...props
370
+ }: ComboboxPrimitive.Row.Props): React.ReactElement {
371
+ return (
372
+ <ComboboxPrimitive.Row
373
+ className={className}
374
+ data-slot="combobox-row"
375
+ {...props}
376
+ />
377
+ );
378
+ }
379
+
380
+ export function ComboboxValue({
381
+ ...props
382
+ }: ComboboxPrimitive.Value.Props): React.ReactElement {
383
+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
384
+ }
385
+
386
+ export function ComboboxList({
387
+ className,
388
+ ...props
389
+ }: ComboboxPrimitive.List.Props): React.ReactElement {
390
+ return (
391
+ <ScrollArea scrollbarGutter scrollFade>
392
+ <ComboboxPrimitive.List
393
+ className={cn(
394
+ "not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
395
+ className,
396
+ )}
397
+ data-slot="combobox-list"
398
+ {...props}
399
+ />
400
+ </ScrollArea>
401
+ );
402
+ }
403
+
404
+ export function ComboboxClear({
405
+ className,
406
+ ...props
407
+ }: ComboboxPrimitive.Clear.Props): React.ReactElement {
408
+ return (
409
+ <ComboboxPrimitive.Clear
410
+ className={className}
411
+ data-slot="combobox-clear"
412
+ {...props}
413
+ />
414
+ );
415
+ }
416
+
417
+ const endBtnClass =
418
+ "absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0";
419
+
420
+ function ComboboxEndAdornment({
421
+ showTrigger = true,
422
+ showClear = false,
423
+ triggerProps,
424
+ clearProps,
425
+ }: {
426
+ showTrigger?: boolean;
427
+ showClear?: boolean;
428
+ triggerProps?: ComboboxPrimitive.Trigger.Props;
429
+ clearProps?: ComboboxPrimitive.Clear.Props;
430
+ }): React.ReactElement | null {
431
+ if (!showTrigger && !showClear) return null;
432
+ return (
433
+ <div className="text-muted-foreground hover:text-foreground transition-colors">
434
+ {showTrigger && (
435
+ <ComboboxTrigger
436
+ className={cn(
437
+ endBtnClass,
438
+ "text-muted-foreground has-[+[data-slot=combobox-clear]]:hidden",
439
+ )}
440
+ {...triggerProps}
441
+ >
442
+ <ComboboxPrimitive.Icon data-slot="combobox-icon">
443
+ <ChevronsUpDownIcon />
444
+ </ComboboxPrimitive.Icon>
445
+ </ComboboxTrigger>
446
+ )}
447
+ {showClear && (
448
+ <ComboboxClear className={endBtnClass} {...clearProps}>
449
+ <XIcon />
450
+ </ComboboxClear>
451
+ )}
452
+ </div>
453
+ );
454
+ }
455
+
456
+ export function ComboboxStatus({
457
+ className,
458
+ ...props
459
+ }: ComboboxPrimitive.Status.Props): React.ReactElement {
460
+ return (
461
+ <ComboboxPrimitive.Status
462
+ className={cn(
463
+ "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
464
+ className,
465
+ )}
466
+ data-slot="combobox-status"
467
+ {...props}
468
+ />
469
+ );
470
+ }
471
+
472
+ export function ComboboxCollection(
473
+ props: ComboboxPrimitive.Collection.Props,
474
+ ): React.ReactElement {
475
+ return (
476
+ <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
477
+ );
478
+ }
479
+
480
+ export function ComboboxChips({
481
+ className,
482
+ children,
483
+ startAddon,
484
+ ...props
485
+ }: ComboboxPrimitive.Chips.Props & {
486
+ startAddon?: React.ReactNode;
487
+ }): React.ReactElement {
488
+ const { chipsRef } = React.useContext(ComboboxContext);
489
+
490
+ return (
491
+ <FieldPrimitive.Validity>
492
+ {({ validity }) => (
493
+ <ComboboxPrimitive.Chips
494
+ aria-invalid={validity.valid === false ? true : undefined}
495
+ className={cn(
496
+ inputBaseClasses,
497
+ "relative inline-flex pr-6 w-full flex-wrap gap-1",
498
+ "placeholder:text-muted-foreground",
499
+ "focus-within:border-ring",
500
+ "aria-invalid:border-error focus-within:aria-invalid:ring-error/20",
501
+ "has-disabled:cursor-not-allowed has-disabled:opacity-50",
502
+ className,
503
+ )}
504
+ data-slot="combobox-chips"
505
+ ref={chipsRef as React.Ref<HTMLDivElement> | null}
506
+ {...props}
507
+ >
508
+ {startAddon && (
509
+ <div
510
+ aria-hidden="true"
511
+ className="flex shrink-0 items-center ps-2 opacity-80 has-[+[data-slot=combobox-chip]]:pe-2 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-ms-0.5 [&_svg]:-me-1.5"
512
+ data-slot="combobox-start-addon"
513
+ >
514
+ {startAddon}
515
+ </div>
516
+ )}
517
+ {children}
518
+ </ComboboxPrimitive.Chips>
519
+ )}
520
+ </FieldPrimitive.Validity>
521
+ );
522
+ }
523
+
524
+ export function ComboboxChip({
525
+ children,
526
+ removeProps,
527
+ ...props
528
+ }: ComboboxPrimitive.Chip.Props & {
529
+ removeProps?: ComboboxPrimitive.ChipRemove.Props;
530
+ }): React.ReactElement {
531
+ return (
532
+ <ComboboxPrimitive.Chip
533
+ className="flex items-center pl-1 rounded-[calc(var(--radius-md)-1px)] bg-accent font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
534
+ data-slot="combobox-chip"
535
+ {...props}
536
+ >
537
+ {children}
538
+ <ComboboxChipRemove {...removeProps} />
539
+ </ComboboxPrimitive.Chip>
540
+ );
541
+ }
542
+
543
+ export function ComboboxChipRemove(
544
+ props: ComboboxPrimitive.ChipRemove.Props,
545
+ ): React.ReactElement {
546
+ return (
547
+ <ComboboxPrimitive.ChipRemove
548
+ aria-label="Remove"
549
+ className="h-full shrink-0 cursor-pointer px-1 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
550
+ data-slot="combobox-chip-remove"
551
+ {...props}
552
+ >
553
+ <XIcon />
554
+ </ComboboxPrimitive.ChipRemove>
555
+ );
556
+ }
557
+
558
+ // ─── Chip Overflow ────────────────────────────────────────────────────────────
559
+
560
+ const CHIP_GAP = 4; // gap-1
561
+ const CHIPS_PADDING_X = 24; // px-3 × 2
562
+ const END_ADORNMENT = 36; // clear button + end-0.5 offset
563
+ const OVERFLOW_WIDTH = 68; // max "+99 más" at text-sm + px-1
564
+
565
+ function calculateChipVisible(
566
+ containerWidth: number,
567
+ chipWidths: number[],
568
+ ): number {
569
+ const available = containerWidth - CHIPS_PADDING_X - END_ADORNMENT;
570
+ let usedWidth = 0;
571
+ let count = 0;
572
+
573
+ for (let i = 0; i < chipWidths.length; i++) {
574
+ const chipWidth = chipWidths[i];
575
+ const willHaveOverflow = i < chipWidths.length - 1;
576
+
577
+ const projected =
578
+ usedWidth +
579
+ (i > 0 ? CHIP_GAP : 0) +
580
+ chipWidth +
581
+ (willHaveOverflow ? CHIP_GAP + OVERFLOW_WIDTH : 0);
582
+
583
+ if (projected > available) break;
584
+
585
+ usedWidth += (i > 0 ? CHIP_GAP : 0) + chipWidth;
586
+ count++;
587
+ }
588
+
589
+ return count;
590
+ }
591
+
592
+ function useChipOverflow(
593
+ itemCount: number,
594
+ containerRef: React.RefObject<Element | null> | null,
595
+ ): { visibleCount: number; isMeasuring: boolean } {
596
+ const [stableCount, setStableCount] = React.useState<{
597
+ items: number;
598
+ visible: number;
599
+ } | null>(null);
600
+ const chipWidthsRef = React.useRef<number[]>([]);
601
+
602
+ const isMeasuring = stableCount === null || stableCount.items !== itemCount;
603
+
604
+ React.useLayoutEffect(() => {
605
+ if (!isMeasuring) return;
606
+
607
+ const container = containerRef?.current as HTMLElement | null;
608
+ if (!container) return;
609
+
610
+ const chipEls = container.querySelectorAll('[data-slot="combobox-chip"]');
611
+ chipWidthsRef.current = Array.from(chipEls).map(
612
+ (el) => (el as HTMLElement).offsetWidth,
613
+ );
614
+
615
+ const visible = calculateChipVisible(
616
+ container.clientWidth,
617
+ chipWidthsRef.current,
618
+ );
619
+ setStableCount({ items: itemCount, visible });
620
+ }, [isMeasuring, itemCount, containerRef]);
621
+
622
+ React.useEffect(() => {
623
+ const container = containerRef?.current as HTMLElement | null;
624
+ if (!container) return;
625
+
626
+ const observer = new ResizeObserver(() => {
627
+ if (!chipWidthsRef.current.length) return;
628
+ const visible = calculateChipVisible(
629
+ container.clientWidth,
630
+ chipWidthsRef.current,
631
+ );
632
+ setStableCount((prev) => (prev ? { ...prev, visible } : null));
633
+ });
634
+
635
+ observer.observe(container);
636
+ return () => observer.disconnect();
637
+ }, [containerRef]);
638
+
639
+ return { visibleCount: stableCount?.visible ?? itemCount, isMeasuring };
640
+ }
641
+
642
+ function ComboboxMultipleChipsContent({
643
+ value,
644
+ getLabel,
645
+ getValue,
646
+ }: {
647
+ value: unknown[];
648
+ getLabel: (item: unknown) => string;
649
+ getValue: (item: unknown) => string;
650
+ }): React.ReactElement {
651
+ const { chipsRef } = React.useContext(ComboboxContext);
652
+ const { visibleCount, isMeasuring } = useChipOverflow(value.length, chipsRef);
653
+
654
+ const visibleItems = isMeasuring ? value : value.slice(0, visibleCount);
655
+ const hiddenCount = isMeasuring ? 0 : value.length - visibleCount;
656
+
657
+ return (
658
+ <>
659
+ {visibleItems.map((item) => (
660
+ <ComboboxChip key={getValue(item)} aria-label={getLabel(item)}>
661
+ {getLabel(item)}
662
+ </ComboboxChip>
663
+ ))}
664
+ {hiddenCount > 0 && (
665
+ <span className="flex items-center whitespace-nowrap px-1 text-sm text-muted-foreground">
666
+ +{hiddenCount} más
667
+ </span>
668
+ )}
669
+ </>
670
+ );
671
+ }
672
+
673
+ // ─── Composite ────────────────────────────────────────────────────────────────
674
+
675
+ function defaultGetLabel(item: unknown): string {
676
+ if (item == null) return "";
677
+ if (typeof item === "string" || typeof item === "number") return String(item);
678
+ if (typeof item === "object" && "label" in item)
679
+ return String((item as Record<string, unknown>).label);
680
+ return String(item);
681
+ }
682
+
683
+ function defaultGetValue(item: unknown): string {
684
+ if (item == null) return "";
685
+ if (typeof item === "string" || typeof item === "number") return String(item);
686
+ if (typeof item === "object" && "value" in item)
687
+ return String((item as Record<string, unknown>).value);
688
+ return String(item);
689
+ }
690
+
691
+ function ComboboxSingleTrigger({
692
+ placeholder,
693
+ showClear,
694
+ inputProps,
695
+ }: {
696
+ placeholder?: string;
697
+ showClear?: boolean;
698
+ inputProps?: React.ComponentProps<typeof ComboboxInput>;
699
+ }): React.ReactElement {
700
+ return (
701
+ <ComboboxInput
702
+ placeholder={placeholder}
703
+ showClear={showClear}
704
+ {...inputProps}
705
+ />
706
+ );
707
+ }
708
+
709
+ function ComboboxMultipleTrigger({
710
+ getLabel,
711
+ getValue,
712
+ chipsProps,
713
+ showClear,
714
+ clearProps,
715
+ }: {
716
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
717
+ getLabel: (item: any) => string;
718
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
719
+ getValue: (item: any) => string;
720
+ chipsProps?: React.ComponentProps<typeof ComboboxChips>;
721
+ showClear?: boolean;
722
+ clearProps?: ComboboxPrimitive.Clear.Props;
723
+ }): React.ReactElement {
724
+ return (
725
+ <ComboboxChips {...chipsProps}>
726
+ <ComboboxValue>
727
+ {(value: unknown[]) => (
728
+ <ComboboxMultipleChipsContent
729
+ value={value}
730
+ getLabel={getLabel}
731
+ getValue={getValue}
732
+ />
733
+ )}
734
+ </ComboboxValue>
735
+ <ComboboxEndAdornment
736
+ showTrigger={!showClear}
737
+ showClear={showClear}
738
+ clearProps={clearProps}
739
+ />
740
+ </ComboboxChips>
741
+ );
742
+ }
743
+
744
+ type ComboboxRootPropsAlias<V, M extends boolean | undefined = false> = Omit<
745
+ ComboboxPrimitive.Root.Props<V, M>,
746
+ "onValueChange"
747
+ > & {
748
+ onChange?: ComboboxPrimitive.Root.Props<V, M>["onValueChange"];
749
+ };
750
+
751
+ type ComboboxBaseProps<TItem = unknown> = Omit<
752
+ ComboboxRootPropsAlias<TItem, boolean>,
753
+ | "items"
754
+ | "itemToStringLabel"
755
+ | "itemToStringValue"
756
+ | "children"
757
+ | "multiple"
758
+ | "onChange"
759
+ | "value"
760
+ | "defaultValue"
761
+ > & {
762
+ items: readonly TItem[];
763
+ getLabel?: (item: TItem) => string;
764
+ getValue?: (item: TItem) => string;
765
+ renderItem?: (item: TItem) => React.ReactNode;
766
+ placeholder?: string;
767
+ emptyText?: string;
768
+ inputProps?: React.ComponentProps<typeof ComboboxInput>;
769
+ chipsProps?: React.ComponentProps<typeof ComboboxChips>;
770
+ chipsInputProps?: React.ComponentProps<typeof ComboboxChipsInput>;
771
+ popupProps?: React.ComponentProps<typeof ComboboxPopup>;
772
+ itemProps?: ComboboxPrimitive.Item.Props;
773
+ listProps?: Omit<ComboboxPrimitive.List.Props, "children">;
774
+ showClear?: boolean;
775
+ };
776
+
777
+ export type ComboboxProps<TItem = unknown> =
778
+ | (ComboboxBaseProps<TItem> & {
779
+ multiple?: false;
780
+ value?: TItem | null;
781
+ defaultValue?: TItem | null;
782
+ onChange?: (value: TItem | null) => void;
783
+ })
784
+ | (ComboboxBaseProps<TItem> & {
785
+ multiple: true;
786
+ value?: TItem[];
787
+ defaultValue?: TItem[];
788
+ onChange?: (value: TItem[]) => void;
789
+ });
790
+
791
+ /**
792
+ * Composite combobox for single and multiple selection.
793
+ *
794
+ * Items shaped as `{ label, value }` or plain strings/numbers work with no
795
+ * extra props. For other shapes, provide `getLabel` and/or `getValue`.
796
+ *
797
+ * In multiple mode, chips are fit dynamically based on the container width —
798
+ * overflowing items show as "+N más". The popup anchors to the chips container automatically.
799
+ *
800
+ * `renderItem` customizes the content **inside** each `ComboboxItem` (the
801
+ * checkmark indicator is always rendered by the item wrapper).
802
+ *
803
+ * Use `ComboboxRoot` + primitives directly when you need full structural
804
+ * control beyond what escape-hatch props offer.
805
+ */
806
+ export function Combobox<TItem = unknown>(
807
+ allProps: ComboboxProps<TItem>,
808
+ ): React.ReactElement {
809
+ // Cast internally — ComboboxProps discriminated union enforces correctness for consumers.
810
+ // The union can't be spread directly into ComboboxRoot because TypeScript can't resolve
811
+ // which branch is active at the call site.
812
+ const {
813
+ items,
814
+ getLabel: getLabelProp,
815
+ getValue: getValueProp,
816
+ renderItem,
817
+ placeholder,
818
+ emptyText = "Sin resultados.",
819
+ multiple,
820
+ onChange,
821
+ value,
822
+ defaultValue,
823
+ inputProps,
824
+ chipsProps,
825
+ chipsInputProps,
826
+ popupProps,
827
+ itemProps,
828
+ listProps,
829
+ showClear = true,
830
+ ...rest
831
+ } = allProps as ComboboxBaseProps<TItem> & {
832
+ multiple?: boolean;
833
+ onChange?: (value: any, eventDetails?: any) => void;
834
+ value?: any;
835
+ defaultValue?: any;
836
+ };
837
+
838
+ const getLabel: (item: TItem) => string = getLabelProp ?? defaultGetLabel;
839
+ const getValue: (item: TItem) => string = getValueProp ?? defaultGetValue;
840
+
841
+ return (
842
+ <ComboboxRoot
843
+ items={items}
844
+ itemToStringLabel={getLabel}
845
+ itemToStringValue={getValue}
846
+ multiple={multiple as boolean}
847
+ onChange={onChange}
848
+ value={value}
849
+ defaultValue={defaultValue}
850
+ autoHighlight
851
+ loopFocus
852
+ {...rest}
853
+ >
854
+ {multiple ? (
855
+ <ComboboxMultipleTrigger
856
+ getLabel={getLabel}
857
+ getValue={getValue}
858
+ chipsProps={chipsProps}
859
+ showClear={showClear}
860
+ />
861
+ ) : (
862
+ <ComboboxSingleTrigger
863
+ placeholder={placeholder}
864
+ showClear={showClear}
865
+ inputProps={inputProps}
866
+ />
867
+ )}
868
+ <ComboboxPopup {...popupProps}>
869
+ {multiple && (
870
+ <div
871
+ className="border-b p-2"
872
+ data-slot="combobox-chips-input-wrapper"
873
+ >
874
+ <ComboboxChipsInput
875
+ placeholder={placeholder}
876
+ {...chipsInputProps}
877
+ />
878
+ </div>
879
+ )}
880
+ <ComboboxEmpty>{emptyText}</ComboboxEmpty>
881
+ <ComboboxList {...listProps}>
882
+ {(item: TItem) => (
883
+ <ComboboxItem key={getValue(item)} value={item} {...itemProps}>
884
+ {renderItem ? renderItem(item) : getLabel(item)}
885
+ </ComboboxItem>
886
+ )}
887
+ </ComboboxList>
888
+ </ComboboxPopup>
889
+ </ComboboxRoot>
890
+ );
891
+ }
892
+
893
+ // ─── Exports ──────────────────────────────────────────────────────────────────
894
+
895
+ export const useComboboxFilter: typeof ComboboxPrimitive.useFilter =
896
+ ComboboxPrimitive.useFilter;
897
+
898
+ export { ComboboxPrimitive };