@multicomma/arkcn 0.1.2 → 0.1.3

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 (148) hide show
  1. package/package.json +1 -1
  2. package/registry/index.json +1 -1
  3. package/registry/items/accordion.json +302 -0
  4. package/registry/items/activity-feed.json +408 -0
  5. package/registry/items/alert-dialog.json +676 -0
  6. package/registry/items/alert.json +113 -0
  7. package/registry/items/angle-slider.json +378 -0
  8. package/registry/items/app-shell.json +2654 -1
  9. package/registry/items/aspect-ratio.json +36 -0
  10. package/registry/items/attachment.json +299 -1
  11. package/registry/items/avatar.json +213 -0
  12. package/registry/items/badge.json +44 -0
  13. package/registry/items/breadcrumb.json +191 -0
  14. package/registry/items/bubble.json +146 -0
  15. package/registry/items/button-group.json +110 -0
  16. package/registry/items/button.json +52 -0
  17. package/registry/items/calendar.json +499 -1
  18. package/registry/items/canvas.json +446 -1
  19. package/registry/items/card.json +192 -0
  20. package/registry/items/carousel.json +208 -1
  21. package/registry/items/cascader.json +797 -1
  22. package/registry/items/chart.json +1764 -1
  23. package/registry/items/chat.json +993 -0
  24. package/registry/items/checkbox.json +503 -0
  25. package/registry/items/clipboard.json +308 -0
  26. package/registry/items/collapsible.json +226 -0
  27. package/registry/items/color-picker.json +1261 -0
  28. package/registry/items/combobox.json +1123 -1
  29. package/registry/items/command.json +837 -1
  30. package/registry/items/comment-thread.json +952 -1
  31. package/registry/items/context-menu.json +1344 -1
  32. package/registry/items/data-grid.json +449 -1
  33. package/registry/items/data-table.json +1310 -1
  34. package/registry/items/date-input.json +476 -0
  35. package/registry/items/dialog.json +592 -0
  36. package/registry/items/direction.json +29 -1
  37. package/registry/items/drawer.json +797 -0
  38. package/registry/items/dropdown-menu.json +1311 -1
  39. package/registry/items/editable.json +503 -0
  40. package/registry/items/empty.json +166 -0
  41. package/registry/items/field.json +286 -1
  42. package/registry/items/file-upload.json +638 -0
  43. package/registry/items/floating-panel.json +720 -0
  44. package/registry/items/floating-toolbar.json +43 -0
  45. package/registry/items/format.json +109 -0
  46. package/registry/items/gantt.json +773 -1
  47. package/registry/items/hooks/use-mobile.json +1 -0
  48. package/registry/items/hotkeys.json +99 -1
  49. package/registry/items/hover-card.json +357 -0
  50. package/registry/items/image-cropper.json +431 -0
  51. package/registry/items/input-group.json +189 -0
  52. package/registry/items/input-otp.json +181 -0
  53. package/registry/items/input.json +28 -0
  54. package/registry/items/item.json +308 -0
  55. package/registry/items/json-tree-view.json +336 -0
  56. package/registry/items/kanban.json +624 -1
  57. package/registry/items/kbd.json +54 -0
  58. package/registry/items/label.json +28 -0
  59. package/registry/items/lib/controllable.json +1 -0
  60. package/registry/items/lib/data-table-adapter.json +1 -0
  61. package/registry/items/lib/drag-preview.json +1 -0
  62. package/registry/items/lib/form.json +1 -0
  63. package/registry/items/lib/history.json +1 -0
  64. package/registry/items/lib/row-layout.json +1 -0
  65. package/registry/items/lib/table.json +1 -0
  66. package/registry/items/lib/time.json +1 -0
  67. package/registry/items/lib/utils.json +1 -0
  68. package/registry/items/live-region.json +43 -0
  69. package/registry/items/marker.json +96 -0
  70. package/registry/items/marquee.json +300 -0
  71. package/registry/items/menubar.json +1282 -1
  72. package/registry/items/message.json +166 -0
  73. package/registry/items/navigation-menu.json +561 -1
  74. package/registry/items/node-graph.json +1037 -1
  75. package/registry/items/number-input.json +519 -0
  76. package/registry/items/pagination.json +244 -1
  77. package/registry/items/password-input.json +311 -0
  78. package/registry/items/popconfirm.json +566 -0
  79. package/registry/items/popover.json +615 -0
  80. package/registry/items/progress.json +516 -0
  81. package/registry/items/qr-code.json +249 -0
  82. package/registry/items/query-builder.json +1368 -1
  83. package/registry/items/radio-group.json +362 -0
  84. package/registry/items/rating-group.json +304 -0
  85. package/registry/items/resizable.json +242 -1
  86. package/registry/items/rich-text-editor.json +934 -1
  87. package/registry/items/scheduler.json +1013 -1
  88. package/registry/items/scroll-area.json +227 -0
  89. package/registry/items/segment-group.json +363 -0
  90. package/registry/items/select.json +956 -0
  91. package/registry/items/separator.json +45 -1
  92. package/registry/items/sheet.json +592 -0
  93. package/registry/items/sidebar.json +785 -1
  94. package/registry/items/signature-pad.json +330 -0
  95. package/registry/items/skeleton.json +28 -0
  96. package/registry/items/slider.json +799 -0
  97. package/registry/items/sonner.json +135 -0
  98. package/registry/items/spinner.json +28 -0
  99. package/registry/items/steps.json +483 -0
  100. package/registry/items/swap.json +107 -0
  101. package/registry/items/switch.json +362 -0
  102. package/registry/items/table.json +210 -0
  103. package/registry/items/tabs.json +326 -0
  104. package/registry/items/tags-input.json +640 -0
  105. package/registry/items/textarea.json +28 -0
  106. package/registry/items/timer.json +289 -0
  107. package/registry/items/toc.json +372 -0
  108. package/registry/items/toggle-group.json +214 -1
  109. package/registry/items/toggle.json +124 -0
  110. package/registry/items/tooltip.json +417 -0
  111. package/registry/items/tour.json +498 -0
  112. package/registry/items/transfer-list.json +609 -1
  113. package/registry/items/tree-select.json +742 -1
  114. package/registry/items/tree-view.json +794 -0
  115. package/registry/items/virtual-list.json +214 -1
  116. package/src/components/ui/app-shell.tsx +11 -0
  117. package/src/components/ui/attachment.tsx +1 -0
  118. package/src/components/ui/calendar.tsx +1 -0
  119. package/src/components/ui/canvas.tsx +8 -0
  120. package/src/components/ui/carousel.tsx +3 -0
  121. package/src/components/ui/cascader.tsx +1 -0
  122. package/src/components/ui/chart.tsx +10 -0
  123. package/src/components/ui/combobox.tsx +3 -0
  124. package/src/components/ui/command.tsx +1 -0
  125. package/src/components/ui/comment-thread.tsx +11 -0
  126. package/src/components/ui/context-menu.tsx +6 -0
  127. package/src/components/ui/data-grid.tsx +8 -0
  128. package/src/components/ui/data-table.tsx +6 -0
  129. package/src/components/ui/direction.tsx +2 -0
  130. package/src/components/ui/dropdown-menu.tsx +6 -0
  131. package/src/components/ui/field.tsx +1 -0
  132. package/src/components/ui/gantt.tsx +15 -0
  133. package/src/components/ui/hotkeys.tsx +3 -0
  134. package/src/components/ui/kanban.tsx +3 -0
  135. package/src/components/ui/menubar.tsx +6 -0
  136. package/src/components/ui/navigation-menu.tsx +1 -0
  137. package/src/components/ui/node-graph.tsx +23 -0
  138. package/src/components/ui/pagination.tsx +1 -0
  139. package/src/components/ui/query-builder.tsx +7 -0
  140. package/src/components/ui/resizable.tsx +1 -0
  141. package/src/components/ui/rich-text-editor.tsx +54 -0
  142. package/src/components/ui/scheduler.tsx +26 -0
  143. package/src/components/ui/separator.tsx +1 -0
  144. package/src/components/ui/sidebar.tsx +5 -0
  145. package/src/components/ui/toggle-group.tsx +3 -0
  146. package/src/components/ui/transfer-list.tsx +11 -0
  147. package/src/components/ui/tree-select.tsx +5 -0
  148. package/src/components/ui/virtual-list.tsx +7 -0
@@ -23,6 +23,802 @@
23
23
  "createTreeCollection",
24
24
  "useCascader"
25
25
  ],
26
+ "parts": [
27
+ {
28
+ "name": "Cascader",
29
+ "props": [
30
+ {
31
+ "name": "children",
32
+ "type": "ReactNode",
33
+ "required": true,
34
+ "description": "Content rendered inside the part.",
35
+ "from": "arkcn"
36
+ },
37
+ {
38
+ "name": "collection",
39
+ "type": "TreeCollection<T>",
40
+ "required": true,
41
+ "description": "The collection of items to render.",
42
+ "from": "arkcn"
43
+ },
44
+ {
45
+ "name": "changeOnSelect",
46
+ "type": "boolean",
47
+ "required": false,
48
+ "default": "false",
49
+ "description": "Let a branch be the answer: clicking it picks it and keeps the popup open.",
50
+ "from": "arkcn"
51
+ },
52
+ {
53
+ "name": "defaultOpen",
54
+ "type": "boolean",
55
+ "required": false,
56
+ "default": "false",
57
+ "description": "The initial open state when uncontrolled.",
58
+ "from": "arkcn"
59
+ },
60
+ {
61
+ "name": "defaultValue",
62
+ "type": "string[]",
63
+ "required": false,
64
+ "default": "[]",
65
+ "description": "The initial value when uncontrolled.",
66
+ "from": "arkcn"
67
+ },
68
+ {
69
+ "name": "disabled",
70
+ "type": "boolean",
71
+ "required": false,
72
+ "default": "false",
73
+ "description": "Disables the part and everything inside it.",
74
+ "from": "arkcn"
75
+ },
76
+ {
77
+ "name": "expandTrigger",
78
+ "type": "\"click\" | \"hover\"",
79
+ "required": false,
80
+ "default": "\"click\"",
81
+ "description": "Open the next column on hover instead of click.",
82
+ "from": "arkcn"
83
+ },
84
+ {
85
+ "name": "invalid",
86
+ "type": "boolean",
87
+ "required": false,
88
+ "default": "false",
89
+ "description": "Marks the part invalid for styling and assistive technology.",
90
+ "from": "arkcn"
91
+ },
92
+ {
93
+ "name": "onOpenChange",
94
+ "type": "((details: { open: boolean; }) => void)",
95
+ "required": false,
96
+ "description": "Called when the open state changes.",
97
+ "from": "arkcn"
98
+ },
99
+ {
100
+ "name": "onValueChange",
101
+ "type": "((details: CascaderValueChangeDetails<T>) => void)",
102
+ "required": false,
103
+ "description": "Called with the new value whenever it changes.",
104
+ "from": "arkcn"
105
+ },
106
+ {
107
+ "name": "open",
108
+ "type": "boolean",
109
+ "required": false,
110
+ "description": "The controlled open state.",
111
+ "from": "arkcn"
112
+ },
113
+ {
114
+ "name": "readOnly",
115
+ "type": "boolean",
116
+ "required": false,
117
+ "default": "false",
118
+ "description": "Shows the value but prevents changes.",
119
+ "from": "arkcn"
120
+ },
121
+ {
122
+ "name": "value",
123
+ "type": "string[]",
124
+ "required": false,
125
+ "description": "The controlled value.",
126
+ "from": "arkcn"
127
+ },
128
+ {
129
+ "name": "autoFocus",
130
+ "type": "boolean",
131
+ "required": false,
132
+ "default": "true",
133
+ "description": "Whether to automatically set focus on the first focusable\ncontent within the popover when opened.",
134
+ "from": "@zag-js/popover"
135
+ },
136
+ {
137
+ "name": "closeOnEscape",
138
+ "type": "boolean",
139
+ "required": false,
140
+ "default": "true",
141
+ "description": "Whether to close the popover when the escape key is pressed.",
142
+ "from": "@zag-js/popover"
143
+ },
144
+ {
145
+ "name": "closeOnInteractOutside",
146
+ "type": "boolean",
147
+ "required": false,
148
+ "default": "true",
149
+ "description": "Whether to close the popover when the user clicks outside of the popover.",
150
+ "from": "@zag-js/popover"
151
+ },
152
+ {
153
+ "name": "defaultTriggerValue",
154
+ "type": "string | null",
155
+ "required": false,
156
+ "description": "The initial trigger value when rendered.\nUse when you don't need to control the trigger value.",
157
+ "from": "@zag-js/popover"
158
+ },
159
+ {
160
+ "name": "finalFocusEl",
161
+ "type": "(() => MaybeElement)",
162
+ "required": false,
163
+ "description": "Element to receive focus when the popover is closed.",
164
+ "from": "@zag-js/popover"
165
+ },
166
+ {
167
+ "name": "hideMode",
168
+ "type": "HideMode",
169
+ "required": false,
170
+ "default": "'display-none'",
171
+ "description": "How to hide content when mounted but not present.\n- `'display-none'`: HTML `hidden` attribute. Effects stay alive.\n- `'activity'`: React 19 `<Activity mode=\"hidden\">`. Effects pause. Requires React 19+.",
172
+ "from": "@ark-ui/react"
173
+ },
174
+ {
175
+ "name": "id",
176
+ "type": "string",
177
+ "required": false,
178
+ "description": "The unique identifier of the machine.",
179
+ "from": "@zag-js/types"
180
+ },
181
+ {
182
+ "name": "ids",
183
+ "type": "ElementIds",
184
+ "required": false,
185
+ "description": "The ids of the elements in the popover. Useful for composition.",
186
+ "from": "@zag-js/popover"
187
+ },
188
+ {
189
+ "name": "immediate",
190
+ "type": "boolean",
191
+ "required": false,
192
+ "description": "Whether to synchronize the present change immediately or defer it to the next frame",
193
+ "from": "@zag-js/presence"
194
+ },
195
+ {
196
+ "name": "initialFocusEl",
197
+ "type": "(() => HTMLElement | null)",
198
+ "required": false,
199
+ "description": "The element to focus on when the popover is opened.",
200
+ "from": "@zag-js/popover"
201
+ },
202
+ {
203
+ "name": "lazyMount",
204
+ "type": "boolean",
205
+ "required": false,
206
+ "default": "true",
207
+ "description": "Whether to enable lazy mounting",
208
+ "from": "@ark-ui/react"
209
+ },
210
+ {
211
+ "name": "modal",
212
+ "type": "boolean",
213
+ "required": false,
214
+ "default": "false",
215
+ "description": "Whether the popover should be modal. When set to `true`:\n- interaction with outside elements will be disabled\n- only popover content will be visible to screen readers\n- scrolling is blocked\n- focus is trapped within the popover",
216
+ "from": "@zag-js/popover"
217
+ },
218
+ {
219
+ "name": "onEnterComplete",
220
+ "type": "VoidFunction",
221
+ "required": false,
222
+ "description": "Function called when the animation ends in the open state",
223
+ "from": "@zag-js/presence"
224
+ },
225
+ {
226
+ "name": "onEscapeKeyDown",
227
+ "type": "((event: KeyboardEvent) => void)",
228
+ "required": false,
229
+ "description": "Function called when the escape key is pressed",
230
+ "from": "@zag-js/dismissable"
231
+ },
232
+ {
233
+ "name": "onExitComplete",
234
+ "type": "VoidFunction",
235
+ "required": false,
236
+ "description": "Function called when the animation ends in the closed state",
237
+ "from": "@zag-js/presence"
238
+ },
239
+ {
240
+ "name": "onFocusOutside",
241
+ "type": "((event: FocusOutsideEvent) => void)",
242
+ "required": false,
243
+ "description": "Function called when the focus is moved outside the component",
244
+ "from": "@zag-js/interact-outside"
245
+ },
246
+ {
247
+ "name": "onInteractOutside",
248
+ "type": "((event: InteractOutsideEvent) => void)",
249
+ "required": false,
250
+ "description": "Function called when an interaction happens outside the component",
251
+ "from": "@zag-js/interact-outside"
252
+ },
253
+ {
254
+ "name": "onPointerDownOutside",
255
+ "type": "((event: PointerDownOutsideEvent) => void)",
256
+ "required": false,
257
+ "description": "Function called when the pointer is pressed down outside the component",
258
+ "from": "@zag-js/interact-outside"
259
+ },
260
+ {
261
+ "name": "onRequestDismiss",
262
+ "type": "((event: LayerDismissEvent) => void)",
263
+ "required": false,
264
+ "description": "Function called when this layer is closed due to a parent layer being closed",
265
+ "from": "@zag-js/dismissable"
266
+ },
267
+ {
268
+ "name": "onTriggerValueChange",
269
+ "type": "((details: TriggerValueChangeDetails) => void)",
270
+ "required": false,
271
+ "description": "Function called when the trigger value changes.",
272
+ "from": "@zag-js/popover"
273
+ },
274
+ {
275
+ "name": "persistentElements",
276
+ "type": "(() => Element | null)[]",
277
+ "required": false,
278
+ "description": "Returns the persistent elements that:\n- should not have pointer-events disabled\n- should not trigger the dismiss event",
279
+ "from": "@zag-js/dismissable"
280
+ },
281
+ {
282
+ "name": "portalled",
283
+ "type": "boolean",
284
+ "required": false,
285
+ "default": "true",
286
+ "description": "Whether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position\nof the popover content.",
287
+ "from": "@zag-js/popover"
288
+ },
289
+ {
290
+ "name": "positioning",
291
+ "type": "PositioningOptions",
292
+ "required": false,
293
+ "description": "The user provided options used to position the popover content",
294
+ "from": "@zag-js/popover"
295
+ },
296
+ {
297
+ "name": "present",
298
+ "type": "boolean",
299
+ "required": false,
300
+ "description": "Whether the node is present (controlled by the user)",
301
+ "from": "@zag-js/presence"
302
+ },
303
+ {
304
+ "name": "restoreFocus",
305
+ "type": "boolean",
306
+ "required": false,
307
+ "default": "true",
308
+ "description": "Whether to restore focus to the element that had focus before the popover was opened.",
309
+ "from": "@zag-js/popover"
310
+ },
311
+ {
312
+ "name": "skipAnimationOnMount",
313
+ "type": "boolean",
314
+ "required": false,
315
+ "default": "false",
316
+ "description": "Whether to allow the initial presence animation.",
317
+ "from": "@ark-ui/react"
318
+ },
319
+ {
320
+ "name": "translations",
321
+ "type": "IntlTranslations",
322
+ "required": false,
323
+ "description": "Specifies the localized strings that identifies the accessibility elements and their states",
324
+ "from": "@zag-js/popover"
325
+ },
326
+ {
327
+ "name": "triggerValue",
328
+ "type": "string | null",
329
+ "required": false,
330
+ "description": "The controlled trigger value",
331
+ "from": "@zag-js/popover"
332
+ },
333
+ {
334
+ "name": "unmountOnExit",
335
+ "type": "boolean",
336
+ "required": false,
337
+ "default": "true",
338
+ "description": "Whether to unmount on exit.",
339
+ "from": "@ark-ui/react"
340
+ }
341
+ ]
342
+ },
343
+ {
344
+ "name": "CascaderClearTrigger",
345
+ "props": [
346
+ {
347
+ "name": "asChild",
348
+ "type": "boolean",
349
+ "required": false,
350
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
351
+ "from": "arkcn"
352
+ },
353
+ {
354
+ "name": "size",
355
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
356
+ "required": false,
357
+ "description": "Size variant; sets a `data-size` attribute.",
358
+ "from": "arkcn"
359
+ },
360
+ {
361
+ "name": "variant",
362
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
363
+ "required": false,
364
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
365
+ "from": "arkcn"
366
+ },
367
+ {
368
+ "name": "children",
369
+ "type": "ReactNode",
370
+ "required": false,
371
+ "description": "Content rendered inside the part.",
372
+ "from": "@types/react"
373
+ },
374
+ {
375
+ "name": "className",
376
+ "type": "string",
377
+ "required": false,
378
+ "description": "Extra classes merged into the part's own classes with `cn`.",
379
+ "from": "@types/react"
380
+ },
381
+ {
382
+ "name": "style",
383
+ "type": "CSSProperties",
384
+ "required": false,
385
+ "description": "Inline styles applied to the rendered element.",
386
+ "from": "@types/react"
387
+ }
388
+ ]
389
+ },
390
+ {
391
+ "name": "CascaderColumn",
392
+ "props": [
393
+ {
394
+ "name": "depth",
395
+ "type": "number",
396
+ "required": true,
397
+ "description": "Column depth, starting at 0 for the first column; sets `data-depth`.",
398
+ "from": "arkcn"
399
+ },
400
+ {
401
+ "name": "children",
402
+ "type": "ReactNode",
403
+ "required": false,
404
+ "description": "Content rendered inside the part.",
405
+ "from": "@types/react"
406
+ },
407
+ {
408
+ "name": "className",
409
+ "type": "string",
410
+ "required": false,
411
+ "description": "Extra classes merged into the part's own classes with `cn`.",
412
+ "from": "@types/react"
413
+ },
414
+ {
415
+ "name": "style",
416
+ "type": "CSSProperties",
417
+ "required": false,
418
+ "description": "Inline styles applied to the rendered element.",
419
+ "from": "@types/react"
420
+ }
421
+ ]
422
+ },
423
+ {
424
+ "name": "CascaderColumns",
425
+ "props": [
426
+ {
427
+ "name": "children",
428
+ "type": "((node: T) => ReactNode)",
429
+ "required": false,
430
+ "description": "Content rendered inside the part.",
431
+ "from": "arkcn"
432
+ },
433
+ {
434
+ "name": "className",
435
+ "type": "string",
436
+ "required": false,
437
+ "description": "Extra classes merged into the part's own classes with `cn`.",
438
+ "from": "@types/react"
439
+ },
440
+ {
441
+ "name": "style",
442
+ "type": "CSSProperties",
443
+ "required": false,
444
+ "description": "Inline styles applied to the rendered element.",
445
+ "from": "@types/react"
446
+ }
447
+ ]
448
+ },
449
+ {
450
+ "name": "CascaderContent",
451
+ "props": [
452
+ {
453
+ "name": "asChild",
454
+ "type": "boolean",
455
+ "required": false,
456
+ "description": "Use the provided child element as the default rendered element, combining their props and behavior.",
457
+ "from": "@ark-ui/react"
458
+ },
459
+ {
460
+ "name": "children",
461
+ "type": "ReactNode",
462
+ "required": false,
463
+ "description": "Content rendered inside the part.",
464
+ "from": "@types/react"
465
+ },
466
+ {
467
+ "name": "className",
468
+ "type": "string",
469
+ "required": false,
470
+ "description": "Extra classes merged into the part's own classes with `cn`.",
471
+ "from": "@types/react"
472
+ },
473
+ {
474
+ "name": "style",
475
+ "type": "CSSProperties",
476
+ "required": false,
477
+ "description": "Inline styles applied to the rendered element.",
478
+ "from": "@types/react"
479
+ }
480
+ ]
481
+ },
482
+ {
483
+ "name": "CascaderEmpty",
484
+ "props": [
485
+ {
486
+ "name": "children",
487
+ "type": "ReactNode",
488
+ "required": false,
489
+ "description": "Content rendered inside the part.",
490
+ "from": "@types/react"
491
+ },
492
+ {
493
+ "name": "className",
494
+ "type": "string",
495
+ "required": false,
496
+ "description": "Extra classes merged into the part's own classes with `cn`.",
497
+ "from": "@types/react"
498
+ },
499
+ {
500
+ "name": "style",
501
+ "type": "CSSProperties",
502
+ "required": false,
503
+ "description": "Inline styles applied to the rendered element.",
504
+ "from": "@types/react"
505
+ }
506
+ ]
507
+ },
508
+ {
509
+ "name": "CascaderHiddenInput",
510
+ "props": [
511
+ {
512
+ "name": "children",
513
+ "type": "ReactNode",
514
+ "required": false,
515
+ "description": "Content rendered inside the part.",
516
+ "from": "@types/react"
517
+ },
518
+ {
519
+ "name": "className",
520
+ "type": "string",
521
+ "required": false,
522
+ "description": "Extra classes merged into the part's own classes with `cn`.",
523
+ "from": "@types/react"
524
+ },
525
+ {
526
+ "name": "style",
527
+ "type": "CSSProperties",
528
+ "required": false,
529
+ "description": "Inline styles applied to the rendered element.",
530
+ "from": "@types/react"
531
+ }
532
+ ]
533
+ },
534
+ {
535
+ "name": "CascaderIndicator",
536
+ "props": [
537
+ {
538
+ "name": "children",
539
+ "type": "ReactNode",
540
+ "required": false,
541
+ "description": "Content rendered inside the part.",
542
+ "from": "@types/react"
543
+ },
544
+ {
545
+ "name": "className",
546
+ "type": "string",
547
+ "required": false,
548
+ "description": "Extra classes merged into the part's own classes with `cn`.",
549
+ "from": "@types/react"
550
+ },
551
+ {
552
+ "name": "style",
553
+ "type": "CSSProperties",
554
+ "required": false,
555
+ "description": "Inline styles applied to the rendered element.",
556
+ "from": "@types/react"
557
+ }
558
+ ]
559
+ },
560
+ {
561
+ "name": "CascaderItem",
562
+ "props": [
563
+ {
564
+ "name": "node",
565
+ "type": "T",
566
+ "required": true,
567
+ "description": "The tree node this part renders.",
568
+ "from": "arkcn"
569
+ },
570
+ {
571
+ "name": "children",
572
+ "type": "ReactNode",
573
+ "required": false,
574
+ "description": "Content rendered inside the part.",
575
+ "from": "arkcn"
576
+ },
577
+ {
578
+ "name": "className",
579
+ "type": "string",
580
+ "required": false,
581
+ "description": "Extra classes merged into the part's own classes with `cn`.",
582
+ "from": "@types/react"
583
+ },
584
+ {
585
+ "name": "style",
586
+ "type": "CSSProperties",
587
+ "required": false,
588
+ "description": "Inline styles applied to the rendered element.",
589
+ "from": "@types/react"
590
+ }
591
+ ]
592
+ },
593
+ {
594
+ "name": "CascaderItemIndicator",
595
+ "props": [
596
+ {
597
+ "name": "children",
598
+ "type": "ReactNode",
599
+ "required": false,
600
+ "description": "Content rendered inside the part.",
601
+ "from": "@types/react"
602
+ },
603
+ {
604
+ "name": "className",
605
+ "type": "string",
606
+ "required": false,
607
+ "description": "Extra classes merged into the part's own classes with `cn`.",
608
+ "from": "@types/react"
609
+ },
610
+ {
611
+ "name": "style",
612
+ "type": "CSSProperties",
613
+ "required": false,
614
+ "description": "Inline styles applied to the rendered element.",
615
+ "from": "@types/react"
616
+ }
617
+ ]
618
+ },
619
+ {
620
+ "name": "CascaderItemText",
621
+ "props": [
622
+ {
623
+ "name": "children",
624
+ "type": "ReactNode",
625
+ "required": false,
626
+ "description": "Content rendered inside the part.",
627
+ "from": "@types/react"
628
+ },
629
+ {
630
+ "name": "className",
631
+ "type": "string",
632
+ "required": false,
633
+ "description": "Extra classes merged into the part's own classes with `cn`.",
634
+ "from": "@types/react"
635
+ },
636
+ {
637
+ "name": "style",
638
+ "type": "CSSProperties",
639
+ "required": false,
640
+ "description": "Inline styles applied to the rendered element.",
641
+ "from": "@types/react"
642
+ }
643
+ ]
644
+ },
645
+ {
646
+ "name": "CascaderSearch",
647
+ "props": [
648
+ {
649
+ "name": "children",
650
+ "type": "ReactNode",
651
+ "required": false,
652
+ "description": "Content rendered inside the part.",
653
+ "from": "@types/react"
654
+ },
655
+ {
656
+ "name": "className",
657
+ "type": "string",
658
+ "required": false,
659
+ "description": "Extra classes merged into the part's own classes with `cn`.",
660
+ "from": "@types/react"
661
+ },
662
+ {
663
+ "name": "style",
664
+ "type": "CSSProperties",
665
+ "required": false,
666
+ "description": "Inline styles applied to the rendered element.",
667
+ "from": "@types/react"
668
+ }
669
+ ]
670
+ },
671
+ {
672
+ "name": "CascaderSearchResult",
673
+ "props": [
674
+ {
675
+ "name": "path",
676
+ "type": "T[]",
677
+ "required": true,
678
+ "description": "Nodes from the first level down to the matched node.",
679
+ "from": "arkcn"
680
+ },
681
+ {
682
+ "name": "separator",
683
+ "type": "ReactNode",
684
+ "required": false,
685
+ "default": "\" / \"",
686
+ "description": "Rendered between path segments.",
687
+ "from": "arkcn"
688
+ },
689
+ {
690
+ "name": "children",
691
+ "type": "ReactNode",
692
+ "required": false,
693
+ "description": "Content rendered inside the part.",
694
+ "from": "@types/react"
695
+ },
696
+ {
697
+ "name": "className",
698
+ "type": "string",
699
+ "required": false,
700
+ "description": "Extra classes merged into the part's own classes with `cn`.",
701
+ "from": "@types/react"
702
+ },
703
+ {
704
+ "name": "style",
705
+ "type": "CSSProperties",
706
+ "required": false,
707
+ "description": "Inline styles applied to the rendered element.",
708
+ "from": "@types/react"
709
+ }
710
+ ]
711
+ },
712
+ {
713
+ "name": "CascaderSearchResults",
714
+ "props": [
715
+ {
716
+ "name": "children",
717
+ "type": "((path: T[]) => ReactNode)",
718
+ "required": false,
719
+ "description": "Content rendered inside the part.",
720
+ "from": "arkcn"
721
+ },
722
+ {
723
+ "name": "className",
724
+ "type": "string",
725
+ "required": false,
726
+ "description": "Extra classes merged into the part's own classes with `cn`.",
727
+ "from": "@types/react"
728
+ },
729
+ {
730
+ "name": "style",
731
+ "type": "CSSProperties",
732
+ "required": false,
733
+ "description": "Inline styles applied to the rendered element.",
734
+ "from": "@types/react"
735
+ }
736
+ ]
737
+ },
738
+ {
739
+ "name": "CascaderTrigger",
740
+ "props": [
741
+ {
742
+ "name": "size",
743
+ "type": "\"default\" | \"sm\"",
744
+ "required": false,
745
+ "default": "\"default\"",
746
+ "description": "Size variant; sets a `data-size` attribute.",
747
+ "from": "arkcn"
748
+ },
749
+ {
750
+ "name": "variant",
751
+ "type": "\"default\" | \"unstyled\"",
752
+ "required": false,
753
+ "default": "\"default\"",
754
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
755
+ "from": "arkcn"
756
+ },
757
+ {
758
+ "name": "children",
759
+ "type": "ReactNode",
760
+ "required": false,
761
+ "description": "Content rendered inside the part.",
762
+ "from": "@types/react"
763
+ },
764
+ {
765
+ "name": "className",
766
+ "type": "string",
767
+ "required": false,
768
+ "description": "Extra classes merged into the part's own classes with `cn`.",
769
+ "from": "@types/react"
770
+ },
771
+ {
772
+ "name": "style",
773
+ "type": "CSSProperties",
774
+ "required": false,
775
+ "description": "Inline styles applied to the rendered element.",
776
+ "from": "@types/react"
777
+ }
778
+ ]
779
+ },
780
+ {
781
+ "name": "CascaderValue",
782
+ "props": [
783
+ {
784
+ "name": "children",
785
+ "type": "((path: T[]) => ReactNode)",
786
+ "required": false,
787
+ "description": "Content rendered inside the part.",
788
+ "from": "arkcn"
789
+ },
790
+ {
791
+ "name": "placeholder",
792
+ "type": "ReactNode",
793
+ "required": false,
794
+ "description": "Text shown while there is no value.",
795
+ "from": "arkcn"
796
+ },
797
+ {
798
+ "name": "separator",
799
+ "type": "ReactNode",
800
+ "required": false,
801
+ "default": "\" / \"",
802
+ "description": "Rendered between path segments.",
803
+ "from": "arkcn"
804
+ },
805
+ {
806
+ "name": "className",
807
+ "type": "string",
808
+ "required": false,
809
+ "description": "Extra classes merged into the part's own classes with `cn`.",
810
+ "from": "@types/react"
811
+ },
812
+ {
813
+ "name": "style",
814
+ "type": "CSSProperties",
815
+ "required": false,
816
+ "description": "Inline styles applied to the rendered element.",
817
+ "from": "@types/react"
818
+ }
819
+ ]
820
+ }
821
+ ],
26
822
  "dependencies": {
27
823
  "@ark-ui/react": "^5.39.1",
28
824
  "lucide-react": "^1.41.0"
@@ -37,7 +833,7 @@
37
833
  "files": [
38
834
  {
39
835
  "path": "components/ui/cascader.tsx",
40
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { CheckIcon, ChevronDownIcon, ChevronRightIcon, SearchIcon, XIcon } from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\nimport { PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\n\n/* -------------------------------- helpers -------------------------------- */\n\n/** Ancestors (root excluded) followed by the node itself. */\nfunction pathNodes<T extends TreeNode>(collection: TreeCollection<T>, value: string): T[] {\n const node = collection.findNode(value)\n if (!node) return []\n const parents = collection.getParentNodes(value).filter((n) => !collection.isRootNode(n))\n return [...parents, node]\n}\n\nfunction pathValues<T extends TreeNode>(collection: TreeCollection<T>, value: string): string[] {\n return pathNodes(collection, value).map((n) => collection.getNodeValue(n))\n}\n\nconst itemId = (base: string, value: string) => `${base}-${value.replace(/[^a-zA-Z0-9_-]/g, \"_\")}`\n\n/* -------------------------------- context -------------------------------- */\n\ntype CascaderValueChangeDetails<T extends TreeNode = TreeNode> = {\n /** `[]` or the selected node's value. */\n value: string[]\n /** Nodes from the first level down to the selected node. */\n path: T[]\n}\n\ntype CascaderContextValue<T extends TreeNode = TreeNode> = {\n collection: TreeCollection<T>\n value: string[]\n setValue: (value: string[]) => void\n /** Branch values open from the first column down; column `i + 1` lists the children of `expandedPath[i]`. */\n expandedPath: string[]\n setExpandedPath: (path: string[]) => void\n highlighted: string | null\n setHighlighted: (value: string | null) => void\n open: boolean\n setOpen: (open: boolean) => void\n query: string\n setQuery: (query: string) => void\n changeOnSelect: boolean\n expandTrigger: \"click\" | \"hover\"\n disabled: boolean\n readOnly: boolean\n invalid: boolean\n /** Activate a node: pick a leaf (and close), or open a branch (and pick it when `changeOnSelect`). */\n activate: (node: T) => void\n /** Expand a branch and highlight it, without picking. */\n expand: (node: T) => void\n clear: () => void\n /** Nodes on the path to the current value. */\n valuePath: T[]\n /** Flat search matches: each entry is a path of nodes. */\n matches: T[][]\n columns: T[][]\n ids: { content: string; item: (value: string) => string }\n contentRef: React.RefObject<HTMLDivElement | null>\n moveHighlight: (key: string) => boolean\n}\n\nconst CascaderContext = React.createContext<CascaderContextValue | null>(null)\n\nfunction useCascader<T extends TreeNode = TreeNode>() {\n const ctx = React.useContext(CascaderContext)\n if (!ctx) throw new Error(\"Cascader parts must be used inside <Cascader>\")\n return ctx as unknown as CascaderContextValue<T>\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype CascaderProps<T extends TreeNode> = Omit<\n React.ComponentProps<typeof PopoverPrimitive.Root>,\n \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"children\"\n> & {\n collection: TreeCollection<T>\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: CascaderValueChangeDetails<T>) => void\n /** Let a branch be the answer: clicking it picks it and keeps the popup open. */\n changeOnSelect?: boolean\n /** Open the next column on hover instead of click. */\n expandTrigger?: \"click\" | \"hover\"\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (details: { open: boolean }) => void\n disabled?: boolean\n readOnly?: boolean\n invalid?: boolean\n children: React.ReactNode\n}\n\nfunction Cascader<T extends TreeNode>({\n collection,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n changeOnSelect = false,\n expandTrigger = \"click\",\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n readOnly = false,\n invalid = false,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ids: idsProp,\n children,\n ...props\n}: CascaderProps<T>) {\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>\n onValueChange?.({ value: next, path: next[0] != null ? pathNodes(collection, next[0]) : [] })\n )\n const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))\n const [expandedPath, setExpandedPath] = React.useState<string[]>([])\n const [highlighted, setHighlighted] = React.useState<string | null>(null)\n const [query, setQueryState] = React.useState(\"\")\n const contentRef = React.useRef<HTMLDivElement | null>(null)\n const base = React.useId()\n const ids = React.useMemo(\n () => ({ content: idsProp?.content ?? `${base}-content`, item: (v: string) => itemId(`${base}-item`, v) }),\n [base, idsProp?.content]\n )\n\n const setValue = React.useCallback(\n (next: string[]) => {\n if (!readOnly) setValueState(next)\n },\n [readOnly, setValueState]\n )\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (disabled) return\n if (next) {\n // Start from the current value: its ancestors open, itself highlighted.\n const current = value[0]\n const path = current != null ? pathValues(collection, current) : []\n setExpandedPath(path.slice(0, -1))\n setHighlighted(current ?? null)\n } else {\n setQueryState(\"\")\n }\n setOpenState(next)\n },\n [disabled, value, collection, setOpenState]\n )\n\n const columns = React.useMemo(() => {\n const cols: T[][] = [collection.getNodeChildren(collection.rootNode)]\n for (const v of expandedPath) {\n const node = collection.findNode(v)\n if (!node) break\n cols.push(collection.getNodeChildren(node))\n }\n return cols\n }, [collection, expandedPath])\n\n const matches = React.useMemo(() => {\n const tokens = query.trim().toLowerCase().split(/\\s+/).filter(Boolean)\n if (tokens.length === 0) return []\n const out: T[][] = []\n collection.visit({\n onEnter: (node) => {\n if (collection.isRootNode(node)) return\n if (collection.isBranchNode(node) && !changeOnSelect) return\n if (collection.getNodeDisabled(node)) return\n const path = pathNodes(collection, collection.getNodeValue(node))\n const text = path\n .map((n) => collection.stringifyNode(n))\n .join(\" \")\n .toLowerCase()\n if (tokens.every((t) => text.includes(t))) out.push(path)\n },\n })\n return out.slice(0, 50)\n }, [collection, query, changeOnSelect])\n\n const setQuery = React.useCallback((next: string) => {\n setQueryState(next)\n setHighlighted(null)\n }, [])\n\n const expand = React.useCallback(\n (node: T) => {\n const v = collection.getNodeValue(node)\n setHighlighted(v)\n if (collection.isBranchNode(node)) setExpandedPath(pathValues(collection, v))\n else setExpandedPath(pathValues(collection, v).slice(0, -1))\n },\n [collection]\n )\n\n const activate = React.useCallback(\n (node: T) => {\n if (collection.getNodeDisabled(node)) return\n const v = collection.getNodeValue(node)\n if (collection.isBranchNode(node)) {\n expand(node)\n if (changeOnSelect) setValue([v])\n return\n }\n setValue([v])\n setOpen(false)\n },\n [collection, changeOnSelect, expand, setValue, setOpen]\n )\n\n const clear = React.useCallback(() => setValue([]), [setValue])\n\n /** Keyboard navigation over the columns (or the search matches). Returns true when handled. */\n const moveHighlight = React.useCallback(\n (key: string) => {\n if (matches.length > 0) {\n const values = matches.map((path) => collection.getNodeValue(path[path.length - 1]))\n const index = highlighted ? values.indexOf(highlighted) : -1\n if (key === \"ArrowDown\") setHighlighted(values[Math.min(values.length - 1, index + 1)])\n else if (key === \"ArrowUp\") setHighlighted(values[Math.max(0, index - 1)])\n else if (key === \"Home\") setHighlighted(values[0])\n else if (key === \"End\") setHighlighted(values[values.length - 1])\n else if (key === \"Enter\" && highlighted) {\n const node = collection.findNode(highlighted)\n if (node) activate(node)\n } else return false\n return true\n }\n const enabled = (list: T[]) => list.filter((n) => !collection.getNodeDisabled(n))\n const node = highlighted ? collection.findNode(highlighted) : undefined\n if (!node) {\n const col = enabled(columns[columns.length - 1] ?? [])\n if (col.length && [\"ArrowDown\", \"ArrowUp\", \"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"].includes(key)) {\n setHighlighted(collection.getNodeValue(key === \"ArrowUp\" || key === \"End\" ? col[col.length - 1] : col[0]))\n return true\n }\n return false\n }\n const indexPath = collection.getIndexPath(highlighted!) ?? []\n const siblings = enabled(collection.getSiblingNodes(indexPath))\n const index = siblings.indexOf(node)\n const isBranch = collection.isBranchNode(node)\n switch (key) {\n case \"ArrowDown\":\n if (siblings[index + 1]) setHighlighted(collection.getNodeValue(siblings[index + 1]))\n return true\n case \"ArrowUp\":\n if (siblings[index - 1]) setHighlighted(collection.getNodeValue(siblings[index - 1]))\n return true\n case \"Home\":\n setHighlighted(collection.getNodeValue(siblings[0]))\n return true\n case \"End\":\n setHighlighted(collection.getNodeValue(siblings[siblings.length - 1]))\n return true\n case \"ArrowRight\": {\n if (!isBranch) return true\n const first = enabled(collection.getNodeChildren(node))[0]\n setExpandedPath(pathValues(collection, highlighted!))\n if (first) setHighlighted(collection.getNodeValue(first))\n return true\n }\n case \"ArrowLeft\": {\n const parent = collection.getParentNode(highlighted!)\n if (!parent || collection.isRootNode(parent)) return true\n const pv = collection.getNodeValue(parent)\n setHighlighted(pv)\n setExpandedPath(pathValues(collection, pv).slice(0, -1))\n return true\n }\n case \"Enter\":\n case \" \":\n activate(node)\n return true\n default:\n return false\n }\n },\n [collection, columns, matches, highlighted, activate]\n )\n\n const valuePath = React.useMemo(() => (value[0] != null ? pathNodes(collection, value[0]) : []), [collection, value])\n\n const ctx: CascaderContextValue<T> = {\n collection,\n value,\n setValue,\n expandedPath,\n setExpandedPath,\n highlighted,\n setHighlighted,\n open,\n setOpen,\n query,\n setQuery,\n changeOnSelect,\n expandTrigger,\n disabled,\n readOnly,\n invalid,\n activate,\n expand,\n clear,\n valuePath,\n matches,\n columns,\n ids,\n contentRef,\n moveHighlight,\n }\n\n return (\n <CascaderContext.Provider value={ctx as unknown as CascaderContextValue}>\n <PopoverPrimitive.Root\n open={open}\n onOpenChange={({ open: next }) => setOpen(next)}\n positioning={{ placement: \"bottom-start\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n ids={{ ...idsProp, content: ids.content }}\n {...props}\n >\n {children}\n </PopoverPrimitive.Root>\n </CascaderContext.Provider>\n )\n}\n\n/* -------------------------------- trigger -------------------------------- */\n\nfunction CascaderTrigger({\n className,\n size = \"default\",\n variant = \"default\",\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { size?: \"sm\" | \"default\"; variant?: \"default\" | \"unstyled\" }) {\n const ctx = useCascader()\n return (\n <PopoverTrigger asChild>\n <div\n data-slot=\"cascader-trigger\"\n role=\"combobox\"\n aria-haspopup=\"tree\"\n aria-expanded={ctx.open}\n aria-controls={ctx.open ? ctx.ids.content : undefined}\n aria-disabled={ctx.disabled || undefined}\n aria-invalid={ctx.invalid || undefined}\n aria-readonly={ctx.readOnly || undefined}\n tabIndex={ctx.disabled ? -1 : 0}\n data-size={size}\n data-variant={variant}\n data-state={ctx.open ? \"open\" : \"closed\"}\n data-disabled={ctx.disabled ? \"\" : undefined}\n data-invalid={ctx.invalid ? \"\" : undefined}\n data-placeholder-shown={ctx.value.length === 0 ? \"\" : undefined}\n className={cn(\n \"flex cursor-default items-center justify-between gap-1.5 text-sm whitespace-nowrap outline-none select-none data-placeholder-shown:text-muted-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"default\" &&\n \"w-full rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40\",\n className\n )}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n event.preventDefault()\n ctx.setOpen(true)\n } else if ((event.key === \"Backspace\" || event.key === \"Delete\") && ctx.value.length) {\n ctx.clear()\n }\n }}\n {...props}\n >\n {children}\n </div>\n </PopoverTrigger>\n )\n}\n\nfunction CascaderIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const ctx = useCascader()\n return (\n <span\n data-slot=\"cascader-indicator\"\n data-state={ctx.open ? \"open\" : \"closed\"}\n className={cn(\"ml-auto flex shrink-0 items-center text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon className=\"size-4\" />}\n </span>\n )\n}\n\n/** The selected path as text (\"Country / State / City\"), or the placeholder. */\nfunction CascaderValue<T extends TreeNode>({\n placeholder,\n separator = \" / \",\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n placeholder?: React.ReactNode\n separator?: React.ReactNode\n children?: (path: T[]) => React.ReactNode\n}) {\n const ctx = useCascader<T>()\n const empty = ctx.valuePath.length === 0\n return (\n <span\n data-slot=\"cascader-value\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"flex min-w-0 flex-1 items-center gap-1 truncate text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : children\n ? children(ctx.valuePath)\n : ctx.valuePath.map((node, index) => (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>\n {index > 0 && (\n <span data-slot=\"cascader-value-separator\" className=\"text-muted-foreground\">\n {separator}\n </span>\n )}\n <span className={cn(index < ctx.valuePath.length - 1 && \"text-muted-foreground\")}>\n {ctx.collection.stringifyNode(node)}\n </span>\n </React.Fragment>\n ))}\n </span>\n )\n}\n\nfunction CascaderClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useCascader()\n if (ctx.value.length === 0 || ctx.readOnly) return null\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"cascader-clear-trigger\"\n aria-label=\"Clear selection\"\n disabled={ctx.disabled}\n className={cn(\"-mr-1 text-muted-foreground hover:text-foreground\", className)}\n onClick={(event) => {\n event.stopPropagation()\n ctx.clear()\n }}\n onKeyDown={(event) => event.stopPropagation()}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n )\n}\n\nfunction CascaderHiddenInput({\n name,\n ...props\n}: Omit<React.ComponentProps<\"input\">, \"type\" | \"value\"> & { name: string }) {\n const ctx = useCascader()\n return <input type=\"hidden\" name={name} value={ctx.value[0] ?? \"\"} data-slot=\"cascader-hidden-input\" {...props} />\n}\n\n/* -------------------------------- content -------------------------------- */\n\nfunction CascaderContent({ className, onKeyDown, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const ctx = useCascader()\n // Keep the highlighted item in view.\n React.useEffect(() => {\n if (!ctx.highlighted) return\n const el = ctx.contentRef.current?.querySelector<HTMLElement>(`#${CSS.escape(ctx.ids.item(ctx.highlighted))}`)\n el?.scrollIntoView({ block: \"nearest\", inline: \"nearest\" })\n }, [ctx.highlighted, ctx.ids, ctx.contentRef, ctx.columns.length])\n return (\n <PopoverContent\n ref={ctx.contentRef}\n data-slot=\"cascader-content\"\n className={cn(\"w-auto min-w-(--reference-width) gap-1 p-1\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if (event.target instanceof HTMLInputElement) {\n // The search keeps left/right for its caret only while it has text.\n const vertical = [\"ArrowDown\", \"ArrowUp\", \"Enter\"].includes(event.key)\n const horizontal = [\"ArrowLeft\", \"ArrowRight\"].includes(event.key) && event.target.value === \"\"\n if (!vertical && !horizontal) return\n }\n if (ctx.moveHighlight(event.key)) event.preventDefault()\n }}\n {...props}\n />\n )\n}\n\nfunction CascaderSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useCascader()\n return (\n <InputGroup data-slot=\"cascader-search\" className={cn(\"h-8 shrink-0\", className)}>\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query}\n onChange={(event) => ctx.setQuery(event.target.value)}\n aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}\n {...props}\n />\n </InputGroup>\n )\n}\n\n/** The column view. Hidden while a search query has matches to show. */\nfunction CascaderColumns<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (node: T) => React.ReactNode }) {\n const ctx = useCascader<T>()\n if (ctx.query.trim()) return null\n return (\n <div\n data-slot=\"cascader-columns\"\n role=\"tree\"\n tabIndex={0}\n aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}\n className={cn(\"flex max-h-72 overflow-x-auto outline-none\", className)}\n {...props}\n >\n {ctx.columns.map((nodes, depth) => (\n <CascaderColumn key={depth} depth={depth}>\n {nodes.map((node) =>\n children ? (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>\n ) : (\n <CascaderItem key={ctx.collection.getNodeValue(node)} node={node} />\n )\n )}\n </CascaderColumn>\n ))}\n </div>\n )\n}\n\nfunction CascaderColumn({ className, depth, ...props }: React.ComponentProps<\"div\"> & { depth: number }) {\n return (\n <div\n data-slot=\"cascader-column\"\n role=\"group\"\n data-depth={depth}\n className={cn(\n \"flex min-w-40 shrink-0 flex-col gap-px overflow-y-auto not-first:border-l not-first:pl-1 not-last:pr-1\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CascaderItem<T extends TreeNode>({\n node,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { node: T; children?: React.ReactNode }) {\n const ctx = useCascader<T>()\n const value = ctx.collection.getNodeValue(node)\n const isBranch = ctx.collection.isBranchNode(node)\n const disabled = ctx.collection.getNodeDisabled(node)\n const expanded = ctx.expandedPath.includes(value)\n const selected = ctx.value[0] === value || ctx.valuePath.some((n) => ctx.collection.getNodeValue(n) === value)\n return (\n <div\n id={ctx.ids.item(value)}\n data-slot=\"cascader-item\"\n role=\"treeitem\"\n aria-level={ctx.collection.getDepth(value)}\n aria-expanded={isBranch ? expanded : undefined}\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n data-value={value}\n data-branch={isBranch ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n data-selected={selected ? \"\" : undefined}\n data-highlighted={ctx.highlighted === value ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n \"relative flex h-7 cursor-default items-center gap-1.5 rounded-md px-2 text-sm outline-none select-none data-expanded:bg-muted data-highlighted:bg-muted data-selected:font-medium data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n onMouseEnter={() => {\n if (disabled) return\n if (ctx.expandTrigger === \"hover\") ctx.expand(node)\n else ctx.setHighlighted(value)\n }}\n onClick={() => ctx.activate(node)}\n {...props}\n >\n {children ?? (\n <>\n <CascaderItemText>{ctx.collection.stringifyNode(node)}</CascaderItemText>\n <CascaderItemIndicator />\n </>\n )}\n </div>\n )\n}\n\nfunction CascaderItemText({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"cascader-item-text\" className={cn(\"flex-1 truncate\", className)} {...props} />\n}\n\n/** Chevron on branches, check on the selected leaf. Reads the nearest item's state from the DOM attributes. */\nfunction CascaderItemIndicator({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"cascader-item-indicator\"\n className={cn(\n \"ml-auto text-muted-foreground not-in-data-selected:**:data-check:hidden in-data-branch:**:data-check:hidden not-in-data-branch:**:data-chevron:hidden\",\n className\n )}\n {...props}\n >\n <ChevronRightIcon data-chevron=\"\" />\n <CheckIcon data-check=\"\" />\n </span>\n )\n}\n\n/** Flat list of matching paths while searching. */\nfunction CascaderSearchResults<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (path: T[]) => React.ReactNode }) {\n const ctx = useCascader<T>()\n if (!ctx.query.trim()) return null\n return (\n <div\n data-slot=\"cascader-search-results\"\n role=\"listbox\"\n className={cn(\"flex max-h-72 flex-col gap-px overflow-y-auto\", className)}\n {...props}\n >\n {ctx.matches.map((path) => {\n const last = path[path.length - 1]\n const value = ctx.collection.getNodeValue(last)\n return children ? (\n <React.Fragment key={value}>{children(path)}</React.Fragment>\n ) : (\n <CascaderSearchResult key={value} path={path} />\n )\n })}\n </div>\n )\n}\n\nfunction CascaderSearchResult<T extends TreeNode>({\n path,\n separator = \" / \",\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { path: T[]; separator?: React.ReactNode }) {\n const ctx = useCascader<T>()\n const last = path[path.length - 1]\n const value = ctx.collection.getNodeValue(last)\n return (\n <div\n id={ctx.ids.item(value)}\n data-slot=\"cascader-search-result\"\n role=\"option\"\n aria-selected={ctx.value[0] === value}\n data-value={value}\n data-selected={ctx.value[0] === value ? \"\" : undefined}\n data-highlighted={ctx.highlighted === value ? \"\" : undefined}\n className={cn(\n \"flex h-7 cursor-default items-center gap-1 rounded-md px-2 text-sm outline-none select-none data-highlighted:bg-muted data-selected:font-medium\",\n className\n )}\n onMouseEnter={() => ctx.setHighlighted(value)}\n onClick={() => ctx.activate(last)}\n {...props}\n >\n {children ??\n path.map((node, index) => (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>\n {index > 0 && <span className=\"text-muted-foreground\">{separator}</span>}\n <span className={cn(\"truncate\", index < path.length - 1 && \"text-muted-foreground\")}>\n {ctx.collection.stringifyNode(node)}\n </span>\n </React.Fragment>\n ))}\n </div>\n )\n}\n\nfunction CascaderEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useCascader()\n const searching = ctx.query.trim().length > 0\n if (searching ? ctx.matches.length > 0 : ctx.columns[0].length > 0) return null\n return (\n <div\n data-slot=\"cascader-empty\"\n className={cn(\"px-2 py-4 text-center text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"No results\"}\n </div>\n )\n}\n\nexport {\n Cascader,\n CascaderClearTrigger,\n CascaderColumn,\n CascaderColumns,\n CascaderContent,\n CascaderEmpty,\n CascaderHiddenInput,\n CascaderIndicator,\n CascaderItem,\n CascaderItemIndicator,\n CascaderItemText,\n CascaderSearch,\n CascaderSearchResult,\n CascaderSearchResults,\n CascaderTrigger,\n CascaderValue,\n createTreeCollection,\n useCascader,\n type CascaderProps,\n type CascaderValueChangeDetails,\n type TreeCollection,\n type TreeNode,\n}\n"
836
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive, createTreeCollection, type TreeCollection, type TreeNode } from \"@ark-ui/react\"\nimport { CheckIcon, ChevronDownIcon, ChevronRightIcon, SearchIcon, XIcon } from \"lucide-react\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\"\nimport { PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\n\n/* -------------------------------- helpers -------------------------------- */\n\n/** Ancestors (root excluded) followed by the node itself. */\nfunction pathNodes<T extends TreeNode>(collection: TreeCollection<T>, value: string): T[] {\n const node = collection.findNode(value)\n if (!node) return []\n const parents = collection.getParentNodes(value).filter((n) => !collection.isRootNode(n))\n return [...parents, node]\n}\n\nfunction pathValues<T extends TreeNode>(collection: TreeCollection<T>, value: string): string[] {\n return pathNodes(collection, value).map((n) => collection.getNodeValue(n))\n}\n\nconst itemId = (base: string, value: string) => `${base}-${value.replace(/[^a-zA-Z0-9_-]/g, \"_\")}`\n\n/* -------------------------------- context -------------------------------- */\n\ntype CascaderValueChangeDetails<T extends TreeNode = TreeNode> = {\n /** `[]` or the selected node's value. */\n value: string[]\n /** Nodes from the first level down to the selected node. */\n path: T[]\n}\n\ntype CascaderContextValue<T extends TreeNode = TreeNode> = {\n collection: TreeCollection<T>\n value: string[]\n setValue: (value: string[]) => void\n /** Branch values open from the first column down; column `i + 1` lists the children of `expandedPath[i]`. */\n expandedPath: string[]\n setExpandedPath: (path: string[]) => void\n highlighted: string | null\n setHighlighted: (value: string | null) => void\n open: boolean\n setOpen: (open: boolean) => void\n query: string\n setQuery: (query: string) => void\n changeOnSelect: boolean\n expandTrigger: \"click\" | \"hover\"\n disabled: boolean\n readOnly: boolean\n invalid: boolean\n /** Activate a node: pick a leaf (and close), or open a branch (and pick it when `changeOnSelect`). */\n activate: (node: T) => void\n /** Expand a branch and highlight it, without picking. */\n expand: (node: T) => void\n clear: () => void\n /** Nodes on the path to the current value. */\n valuePath: T[]\n /** Flat search matches: each entry is a path of nodes. */\n matches: T[][]\n columns: T[][]\n ids: { content: string; item: (value: string) => string }\n contentRef: React.RefObject<HTMLDivElement | null>\n moveHighlight: (key: string) => boolean\n}\n\nconst CascaderContext = React.createContext<CascaderContextValue | null>(null)\n\nfunction useCascader<T extends TreeNode = TreeNode>() {\n const ctx = React.useContext(CascaderContext)\n if (!ctx) throw new Error(\"Cascader parts must be used inside <Cascader>\")\n return ctx as unknown as CascaderContextValue<T>\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype CascaderProps<T extends TreeNode> = Omit<\n React.ComponentProps<typeof PopoverPrimitive.Root>,\n \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"children\"\n> & {\n collection: TreeCollection<T>\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (details: CascaderValueChangeDetails<T>) => void\n /** Let a branch be the answer: clicking it picks it and keeps the popup open. */\n changeOnSelect?: boolean\n /** Open the next column on hover instead of click. */\n expandTrigger?: \"click\" | \"hover\"\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (details: { open: boolean }) => void\n disabled?: boolean\n readOnly?: boolean\n invalid?: boolean\n children: React.ReactNode\n}\n\nfunction Cascader<T extends TreeNode>({\n collection,\n value: valueProp,\n defaultValue = [],\n onValueChange,\n changeOnSelect = false,\n expandTrigger = \"click\",\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n disabled = false,\n readOnly = false,\n invalid = false,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ids: idsProp,\n children,\n ...props\n}: CascaderProps<T>) {\n const [value, setValueState] = useControllable<string[]>(valueProp, defaultValue, (next) =>\n onValueChange?.({ value: next, path: next[0] != null ? pathNodes(collection, next[0]) : [] })\n )\n const [open, setOpenState] = useControllable<boolean>(openProp, defaultOpen, (next) => onOpenChange?.({ open: next }))\n const [expandedPath, setExpandedPath] = React.useState<string[]>([])\n const [highlighted, setHighlighted] = React.useState<string | null>(null)\n const [query, setQueryState] = React.useState(\"\")\n const contentRef = React.useRef<HTMLDivElement | null>(null)\n const base = React.useId()\n const ids = React.useMemo(\n () => ({ content: idsProp?.content ?? `${base}-content`, item: (v: string) => itemId(`${base}-item`, v) }),\n [base, idsProp?.content]\n )\n\n const setValue = React.useCallback(\n (next: string[]) => {\n if (!readOnly) setValueState(next)\n },\n [readOnly, setValueState]\n )\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (disabled) return\n if (next) {\n // Start from the current value: its ancestors open, itself highlighted.\n const current = value[0]\n const path = current != null ? pathValues(collection, current) : []\n setExpandedPath(path.slice(0, -1))\n setHighlighted(current ?? null)\n } else {\n setQueryState(\"\")\n }\n setOpenState(next)\n },\n [disabled, value, collection, setOpenState]\n )\n\n const columns = React.useMemo(() => {\n const cols: T[][] = [collection.getNodeChildren(collection.rootNode)]\n for (const v of expandedPath) {\n const node = collection.findNode(v)\n if (!node) break\n cols.push(collection.getNodeChildren(node))\n }\n return cols\n }, [collection, expandedPath])\n\n const matches = React.useMemo(() => {\n const tokens = query.trim().toLowerCase().split(/\\s+/).filter(Boolean)\n if (tokens.length === 0) return []\n const out: T[][] = []\n collection.visit({\n onEnter: (node) => {\n if (collection.isRootNode(node)) return\n if (collection.isBranchNode(node) && !changeOnSelect) return\n if (collection.getNodeDisabled(node)) return\n const path = pathNodes(collection, collection.getNodeValue(node))\n const text = path\n .map((n) => collection.stringifyNode(n))\n .join(\" \")\n .toLowerCase()\n if (tokens.every((t) => text.includes(t))) out.push(path)\n },\n })\n return out.slice(0, 50)\n }, [collection, query, changeOnSelect])\n\n const setQuery = React.useCallback((next: string) => {\n setQueryState(next)\n setHighlighted(null)\n }, [])\n\n const expand = React.useCallback(\n (node: T) => {\n const v = collection.getNodeValue(node)\n setHighlighted(v)\n if (collection.isBranchNode(node)) setExpandedPath(pathValues(collection, v))\n else setExpandedPath(pathValues(collection, v).slice(0, -1))\n },\n [collection]\n )\n\n const activate = React.useCallback(\n (node: T) => {\n if (collection.getNodeDisabled(node)) return\n const v = collection.getNodeValue(node)\n if (collection.isBranchNode(node)) {\n expand(node)\n if (changeOnSelect) setValue([v])\n return\n }\n setValue([v])\n setOpen(false)\n },\n [collection, changeOnSelect, expand, setValue, setOpen]\n )\n\n const clear = React.useCallback(() => setValue([]), [setValue])\n\n /** Keyboard navigation over the columns (or the search matches). Returns true when handled. */\n const moveHighlight = React.useCallback(\n (key: string) => {\n if (matches.length > 0) {\n const values = matches.map((path) => collection.getNodeValue(path[path.length - 1]))\n const index = highlighted ? values.indexOf(highlighted) : -1\n if (key === \"ArrowDown\") setHighlighted(values[Math.min(values.length - 1, index + 1)])\n else if (key === \"ArrowUp\") setHighlighted(values[Math.max(0, index - 1)])\n else if (key === \"Home\") setHighlighted(values[0])\n else if (key === \"End\") setHighlighted(values[values.length - 1])\n else if (key === \"Enter\" && highlighted) {\n const node = collection.findNode(highlighted)\n if (node) activate(node)\n } else return false\n return true\n }\n const enabled = (list: T[]) => list.filter((n) => !collection.getNodeDisabled(n))\n const node = highlighted ? collection.findNode(highlighted) : undefined\n if (!node) {\n const col = enabled(columns[columns.length - 1] ?? [])\n if (col.length && [\"ArrowDown\", \"ArrowUp\", \"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"].includes(key)) {\n setHighlighted(collection.getNodeValue(key === \"ArrowUp\" || key === \"End\" ? col[col.length - 1] : col[0]))\n return true\n }\n return false\n }\n const indexPath = collection.getIndexPath(highlighted!) ?? []\n const siblings = enabled(collection.getSiblingNodes(indexPath))\n const index = siblings.indexOf(node)\n const isBranch = collection.isBranchNode(node)\n switch (key) {\n case \"ArrowDown\":\n if (siblings[index + 1]) setHighlighted(collection.getNodeValue(siblings[index + 1]))\n return true\n case \"ArrowUp\":\n if (siblings[index - 1]) setHighlighted(collection.getNodeValue(siblings[index - 1]))\n return true\n case \"Home\":\n setHighlighted(collection.getNodeValue(siblings[0]))\n return true\n case \"End\":\n setHighlighted(collection.getNodeValue(siblings[siblings.length - 1]))\n return true\n case \"ArrowRight\": {\n if (!isBranch) return true\n const first = enabled(collection.getNodeChildren(node))[0]\n setExpandedPath(pathValues(collection, highlighted!))\n if (first) setHighlighted(collection.getNodeValue(first))\n return true\n }\n case \"ArrowLeft\": {\n const parent = collection.getParentNode(highlighted!)\n if (!parent || collection.isRootNode(parent)) return true\n const pv = collection.getNodeValue(parent)\n setHighlighted(pv)\n setExpandedPath(pathValues(collection, pv).slice(0, -1))\n return true\n }\n case \"Enter\":\n case \" \":\n activate(node)\n return true\n default:\n return false\n }\n },\n [collection, columns, matches, highlighted, activate]\n )\n\n const valuePath = React.useMemo(() => (value[0] != null ? pathNodes(collection, value[0]) : []), [collection, value])\n\n const ctx: CascaderContextValue<T> = {\n collection,\n value,\n setValue,\n expandedPath,\n setExpandedPath,\n highlighted,\n setHighlighted,\n open,\n setOpen,\n query,\n setQuery,\n changeOnSelect,\n expandTrigger,\n disabled,\n readOnly,\n invalid,\n activate,\n expand,\n clear,\n valuePath,\n matches,\n columns,\n ids,\n contentRef,\n moveHighlight,\n }\n\n return (\n <CascaderContext.Provider value={ctx as unknown as CascaderContextValue}>\n <PopoverPrimitive.Root\n open={open}\n onOpenChange={({ open: next }) => setOpen(next)}\n positioning={{ placement: \"bottom-start\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n ids={{ ...idsProp, content: ids.content }}\n {...props}\n >\n {children}\n </PopoverPrimitive.Root>\n </CascaderContext.Provider>\n )\n}\n\n/* -------------------------------- trigger -------------------------------- */\n\nfunction CascaderTrigger({\n className,\n size = \"default\",\n variant = \"default\",\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { size?: \"sm\" | \"default\"; variant?: \"default\" | \"unstyled\" }) {\n const ctx = useCascader()\n return (\n <PopoverTrigger asChild>\n <div\n data-slot=\"cascader-trigger\"\n role=\"combobox\"\n aria-haspopup=\"tree\"\n aria-expanded={ctx.open}\n aria-controls={ctx.open ? ctx.ids.content : undefined}\n aria-disabled={ctx.disabled || undefined}\n aria-invalid={ctx.invalid || undefined}\n aria-readonly={ctx.readOnly || undefined}\n tabIndex={ctx.disabled ? -1 : 0}\n data-size={size}\n data-variant={variant}\n data-state={ctx.open ? \"open\" : \"closed\"}\n data-disabled={ctx.disabled ? \"\" : undefined}\n data-invalid={ctx.invalid ? \"\" : undefined}\n data-placeholder-shown={ctx.value.length === 0 ? \"\" : undefined}\n className={cn(\n \"flex cursor-default items-center justify-between gap-1.5 text-sm whitespace-nowrap outline-none select-none data-placeholder-shown:text-muted-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"default\" &&\n \"w-full rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] dark:bg-input/30 dark:hover:bg-input/50 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40\",\n className\n )}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || ctx.disabled) return\n if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n event.preventDefault()\n ctx.setOpen(true)\n } else if ((event.key === \"Backspace\" || event.key === \"Delete\") && ctx.value.length) {\n ctx.clear()\n }\n }}\n {...props}\n >\n {children}\n </div>\n </PopoverTrigger>\n )\n}\n\nfunction CascaderIndicator({ className, children, ...props }: React.ComponentProps<\"span\">) {\n const ctx = useCascader()\n return (\n <span\n data-slot=\"cascader-indicator\"\n data-state={ctx.open ? \"open\" : \"closed\"}\n className={cn(\"ml-auto flex shrink-0 items-center text-muted-foreground\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon className=\"size-4\" />}\n </span>\n )\n}\n\n/** The selected path as text (\"Country / State / City\"), or the placeholder. */\nfunction CascaderValue<T extends TreeNode>({\n placeholder,\n separator = \" / \",\n children,\n className,\n ...props\n}: Omit<React.ComponentProps<\"span\">, \"children\"> & {\n placeholder?: React.ReactNode\n /** Rendered between path segments. */\n separator?: React.ReactNode\n children?: (path: T[]) => React.ReactNode\n}) {\n const ctx = useCascader<T>()\n const empty = ctx.valuePath.length === 0\n return (\n <span\n data-slot=\"cascader-value\"\n data-placeholder-shown={empty ? \"\" : undefined}\n className={cn(\"flex min-w-0 flex-1 items-center gap-1 truncate text-left\", className)}\n {...props}\n >\n {empty\n ? placeholder\n : children\n ? children(ctx.valuePath)\n : ctx.valuePath.map((node, index) => (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>\n {index > 0 && (\n <span data-slot=\"cascader-value-separator\" className=\"text-muted-foreground\">\n {separator}\n </span>\n )}\n <span className={cn(index < ctx.valuePath.length - 1 && \"text-muted-foreground\")}>\n {ctx.collection.stringifyNode(node)}\n </span>\n </React.Fragment>\n ))}\n </span>\n )\n}\n\nfunction CascaderClearTrigger({ className, children, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useCascader()\n if (ctx.value.length === 0 || ctx.readOnly) return null\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"cascader-clear-trigger\"\n aria-label=\"Clear selection\"\n disabled={ctx.disabled}\n className={cn(\"-mr-1 text-muted-foreground hover:text-foreground\", className)}\n onClick={(event) => {\n event.stopPropagation()\n ctx.clear()\n }}\n onKeyDown={(event) => event.stopPropagation()}\n {...props}\n >\n {children ?? <XIcon />}\n </Button>\n )\n}\n\nfunction CascaderHiddenInput({\n name,\n ...props\n}: Omit<React.ComponentProps<\"input\">, \"type\" | \"value\"> & { name: string }) {\n const ctx = useCascader()\n return <input type=\"hidden\" name={name} value={ctx.value[0] ?? \"\"} data-slot=\"cascader-hidden-input\" {...props} />\n}\n\n/* -------------------------------- content -------------------------------- */\n\nfunction CascaderContent({ className, onKeyDown, ...props }: React.ComponentProps<typeof PopoverContent>) {\n const ctx = useCascader()\n // Keep the highlighted item in view.\n React.useEffect(() => {\n if (!ctx.highlighted) return\n const el = ctx.contentRef.current?.querySelector<HTMLElement>(`#${CSS.escape(ctx.ids.item(ctx.highlighted))}`)\n el?.scrollIntoView({ block: \"nearest\", inline: \"nearest\" })\n }, [ctx.highlighted, ctx.ids, ctx.contentRef, ctx.columns.length])\n return (\n <PopoverContent\n ref={ctx.contentRef}\n data-slot=\"cascader-content\"\n className={cn(\"w-auto min-w-(--reference-width) gap-1 p-1\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if (event.target instanceof HTMLInputElement) {\n // The search keeps left/right for its caret only while it has text.\n const vertical = [\"ArrowDown\", \"ArrowUp\", \"Enter\"].includes(event.key)\n const horizontal = [\"ArrowLeft\", \"ArrowRight\"].includes(event.key) && event.target.value === \"\"\n if (!vertical && !horizontal) return\n }\n if (ctx.moveHighlight(event.key)) event.preventDefault()\n }}\n {...props}\n />\n )\n}\n\nfunction CascaderSearch({\n className,\n placeholder = \"Search…\",\n ...props\n}: Omit<React.ComponentProps<typeof InputGroupInput>, \"value\" | \"onChange\">) {\n const ctx = useCascader()\n return (\n <InputGroup data-slot=\"cascader-search\" className={cn(\"h-8 shrink-0\", className)}>\n <InputGroupAddon>\n <SearchIcon />\n </InputGroupAddon>\n <InputGroupInput\n role=\"searchbox\"\n placeholder={placeholder}\n value={ctx.query}\n onChange={(event) => ctx.setQuery(event.target.value)}\n aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}\n {...props}\n />\n </InputGroup>\n )\n}\n\n/** The column view. Hidden while a search query has matches to show. */\nfunction CascaderColumns<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (node: T) => React.ReactNode }) {\n const ctx = useCascader<T>()\n if (ctx.query.trim()) return null\n return (\n <div\n data-slot=\"cascader-columns\"\n role=\"tree\"\n tabIndex={0}\n aria-activedescendant={ctx.highlighted ? ctx.ids.item(ctx.highlighted) : undefined}\n className={cn(\"flex max-h-72 overflow-x-auto outline-none\", className)}\n {...props}\n >\n {ctx.columns.map((nodes, depth) => (\n <CascaderColumn key={depth} depth={depth}>\n {nodes.map((node) =>\n children ? (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>{children(node)}</React.Fragment>\n ) : (\n <CascaderItem key={ctx.collection.getNodeValue(node)} node={node} />\n )\n )}\n </CascaderColumn>\n ))}\n </div>\n )\n}\n\nfunction CascaderColumn({ className, depth, ...props }: React.ComponentProps<\"div\"> & { depth: number }) {\n return (\n <div\n data-slot=\"cascader-column\"\n role=\"group\"\n data-depth={depth}\n className={cn(\n \"flex min-w-40 shrink-0 flex-col gap-px overflow-y-auto not-first:border-l not-first:pl-1 not-last:pr-1\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CascaderItem<T extends TreeNode>({\n node,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { node: T; children?: React.ReactNode }) {\n const ctx = useCascader<T>()\n const value = ctx.collection.getNodeValue(node)\n const isBranch = ctx.collection.isBranchNode(node)\n const disabled = ctx.collection.getNodeDisabled(node)\n const expanded = ctx.expandedPath.includes(value)\n const selected = ctx.value[0] === value || ctx.valuePath.some((n) => ctx.collection.getNodeValue(n) === value)\n return (\n <div\n id={ctx.ids.item(value)}\n data-slot=\"cascader-item\"\n role=\"treeitem\"\n aria-level={ctx.collection.getDepth(value)}\n aria-expanded={isBranch ? expanded : undefined}\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n data-value={value}\n data-branch={isBranch ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n data-selected={selected ? \"\" : undefined}\n data-highlighted={ctx.highlighted === value ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\n \"relative flex h-7 cursor-default items-center gap-1.5 rounded-md px-2 text-sm outline-none select-none data-expanded:bg-muted data-highlighted:bg-muted data-selected:font-medium data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n onMouseEnter={() => {\n if (disabled) return\n if (ctx.expandTrigger === \"hover\") ctx.expand(node)\n else ctx.setHighlighted(value)\n }}\n onClick={() => ctx.activate(node)}\n {...props}\n >\n {children ?? (\n <>\n <CascaderItemText>{ctx.collection.stringifyNode(node)}</CascaderItemText>\n <CascaderItemIndicator />\n </>\n )}\n </div>\n )\n}\n\nfunction CascaderItemText({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"cascader-item-text\" className={cn(\"flex-1 truncate\", className)} {...props} />\n}\n\n/** Chevron on branches, check on the selected leaf. Reads the nearest item's state from the DOM attributes. */\nfunction CascaderItemIndicator({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"cascader-item-indicator\"\n className={cn(\n \"ml-auto text-muted-foreground not-in-data-selected:**:data-check:hidden in-data-branch:**:data-check:hidden not-in-data-branch:**:data-chevron:hidden\",\n className\n )}\n {...props}\n >\n <ChevronRightIcon data-chevron=\"\" />\n <CheckIcon data-check=\"\" />\n </span>\n )\n}\n\n/** Flat list of matching paths while searching. */\nfunction CascaderSearchResults<T extends TreeNode>({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & { children?: (path: T[]) => React.ReactNode }) {\n const ctx = useCascader<T>()\n if (!ctx.query.trim()) return null\n return (\n <div\n data-slot=\"cascader-search-results\"\n role=\"listbox\"\n className={cn(\"flex max-h-72 flex-col gap-px overflow-y-auto\", className)}\n {...props}\n >\n {ctx.matches.map((path) => {\n const last = path[path.length - 1]\n const value = ctx.collection.getNodeValue(last)\n return children ? (\n <React.Fragment key={value}>{children(path)}</React.Fragment>\n ) : (\n <CascaderSearchResult key={value} path={path} />\n )\n })}\n </div>\n )\n}\n\nfunction CascaderSearchResult<T extends TreeNode>({\n path,\n separator = \" / \",\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { path: T[]; separator?: React.ReactNode }) {\n const ctx = useCascader<T>()\n const last = path[path.length - 1]\n const value = ctx.collection.getNodeValue(last)\n return (\n <div\n id={ctx.ids.item(value)}\n data-slot=\"cascader-search-result\"\n role=\"option\"\n aria-selected={ctx.value[0] === value}\n data-value={value}\n data-selected={ctx.value[0] === value ? \"\" : undefined}\n data-highlighted={ctx.highlighted === value ? \"\" : undefined}\n className={cn(\n \"flex h-7 cursor-default items-center gap-1 rounded-md px-2 text-sm outline-none select-none data-highlighted:bg-muted data-selected:font-medium\",\n className\n )}\n onMouseEnter={() => ctx.setHighlighted(value)}\n onClick={() => ctx.activate(last)}\n {...props}\n >\n {children ??\n path.map((node, index) => (\n <React.Fragment key={ctx.collection.getNodeValue(node)}>\n {index > 0 && <span className=\"text-muted-foreground\">{separator}</span>}\n <span className={cn(\"truncate\", index < path.length - 1 && \"text-muted-foreground\")}>\n {ctx.collection.stringifyNode(node)}\n </span>\n </React.Fragment>\n ))}\n </div>\n )\n}\n\nfunction CascaderEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useCascader()\n const searching = ctx.query.trim().length > 0\n if (searching ? ctx.matches.length > 0 : ctx.columns[0].length > 0) return null\n return (\n <div\n data-slot=\"cascader-empty\"\n className={cn(\"px-2 py-4 text-center text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"No results\"}\n </div>\n )\n}\n\nexport {\n Cascader,\n CascaderClearTrigger,\n CascaderColumn,\n CascaderColumns,\n CascaderContent,\n CascaderEmpty,\n CascaderHiddenInput,\n CascaderIndicator,\n CascaderItem,\n CascaderItemIndicator,\n CascaderItemText,\n CascaderSearch,\n CascaderSearchResult,\n CascaderSearchResults,\n CascaderTrigger,\n CascaderValue,\n createTreeCollection,\n useCascader,\n type CascaderProps,\n type CascaderValueChangeDetails,\n type TreeCollection,\n type TreeNode,\n}\n"
41
837
  }
42
838
  ],
43
839
  "docs": "## Tree select and cascader\nBoth live on Ark's `TreeCollection` (`createTreeCollection`, re-exported) and a Popover, and own\nonly the open state, the search query, and the value (`value`/`defaultValue`/`onValueChange`,\nalways a `string[]`); the trigger is a focusable `div[role=combobox]` via `PopoverTrigger asChild`\nso chips and clear buttons inside it stay real buttons. Both use `src/lib/controllable.ts`\n`useControllable(controlled, default, onChange)` (the shared controllable-state hook; prefer it\nover local copies).\n`src/components/ui/tree-select.tsx`: `TreeSelect { collection, multiple, selectableBranches,\ncloseOnSelect, expandedValue…, open…, disabled, readOnly, invalid }` > `TreeSelectTrigger`\n(`size`, `variant=\"unstyled\"`; Enter/Space/ArrowDown open, Backspace removes the last chip) >\n`TreeSelectValue` (labels joined, render-prop child gets the nodes) or `TreeSelectChips` >\n`TreeSelectChip node=` (remove button), `TreeSelectClearTrigger`, `TreeSelectIndicator`;\n`TreeSelectContent` (same width as the trigger) > `TreeSelectSearch` (filters the collection with\nancestors kept and auto-expands matches; ArrowDown moves into the tree), `TreeSelectTree`\n(renders the toolkit `TreeView` parts recursively through `TreeSelectNode`; render-prop child\ncustomises the row label), `TreeSelectEmpty`; `TreeSelectHiddenInput name=`. Multiple mode is\nArk checkbox semantics: `value` holds leaf values only, a branch toggles all its leaves, and\n`collapseValues(collection, value)` (used by the value/chips) shows a fully selected branch as one\nchip. Single mode ignores branch picks unless `selectableBranches`. `useTreeSelect()` exposes it all.\n`src/components/ui/cascader.tsx`: `Cascader { collection, changeOnSelect, expandTrigger:\nclick|hover, open…, disabled, readOnly, invalid }` > `CascaderTrigger` > `CascaderValue`\n(the path as \"A / B / C\", `separator`, render-prop child gets the path nodes), `CascaderClearTrigger`,\n`CascaderIndicator`; `CascaderContent` (owns the keyboard: ↑/↓ within a column, → opens a branch,\n← back to the parent, Home/End, Enter/Space activates) > `CascaderSearch` (matches every level,\nflat results replace the columns), `CascaderColumns` (`role=\"tree\"`, `aria-activedescendant`;\ncolumn `i + 1` lists the children of `expandedPath[i]`; render-prop child customises items) >\n`CascaderColumn depth=` > `CascaderItem node=` (`data-branch/expanded/selected/highlighted/disabled`)\n> `CascaderItemText` + `CascaderItemIndicator` (chevron on branches, check on the selected leaf),\n`CascaderSearchResults` > `CascaderSearchResult path=`, `CascaderEmpty`, `CascaderHiddenInput`.\nPicking a leaf closes; a branch opens the next column and, with `changeOnSelect`, is itself the\nvalue. Opening starts from the current value's path. `useCascader()` exposes it all.\n`src/demo/pickers/` is the reference (`/access`, \"Roles & access\"): folder picker with branches selectable and\nicons, people/teams multi-select with chips, hover-expanding location cascader with search, and a\nchange-on-select category cascader."