@multicomma/arkcn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "canvas",
3
+ "type": "ui",
4
+ "description": "Canvas — a compositional, data-agnostic drag-and-drop layout surface on Pragmatic drag and drop. Items come from a palette (`CanvasPaletteItem`, carrying arbitrary `data`) or are existing nodes (`CanvasNode`). Nodes are drop targets on all ",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
8
+ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^3.2.0",
9
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.0",
10
+ "lucide-react": "^1.41.0"
11
+ },
12
+ "registryDependencies": [
13
+ "drag-preview",
14
+ "live-region",
15
+ "utils"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "components/ui/canvas.tsx",
20
+ "content": "import * as React from \"react\"\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\"\nimport { draggable, dropTargetForElements, monitorForElements } from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\"\nimport { autoScrollForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\"\nimport { attachClosestEdge, extractClosestEdge, type Edge } from \"@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge\"\nimport { ark } from \"@ark-ui/react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport { cloneDragPreview } from \"@/lib/drag-preview\"\nimport { cn } from \"@/lib/utils\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Canvas — a compositional, data-agnostic drag-and-drop layout surface on\n * Pragmatic drag and drop. Items come from a palette (`CanvasPaletteItem`,\n * carrying arbitrary `data`) or are existing nodes (`CanvasNode`). Nodes are\n * drop targets on all four edges; the empty area is a target too. The canvas\n * reports every drop as an intent (`onDrop`) and leaves layout state to the\n * consumer, which renders rows/nodes from it. `CanvasResizeHandle` reports\n * pointer resizes between siblings as a fraction of the row width.\n */\n\ntype CanvasSource = { type: \"palette\"; data: unknown } | { type: \"node\"; id: string }\ntype CanvasTarget = { type: \"node\"; id: string; edge: Edge } | { type: \"area\" }\ntype CanvasDropDetails = { source: CanvasSource; target: CanvasTarget }\n\ntype DragData =\n { instanceId: symbol; type: \"palette\"; data: unknown } | { instanceId: symbol; type: \"node\"; id: string }\n\nconst NODE_TARGET = \"canvas-node-target\"\nconst AREA_TARGET = \"canvas-area-target\"\n\ntype CanvasDirection = \"up\" | \"down\" | \"left\" | \"right\"\n\ntype CanvasContextValue = {\n instanceId: symbol\n dragging: { type: \"palette\" | \"node\"; id?: string } | null\n /** Keyboard \"pick up\" state for nodes; arrows emit drop intents. */\n grabbed: string | null\n grab: (nodeId: string | null) => void\n moveByKey: (nodeId: string, direction: CanvasDirection) => void\n /** Keyboard activation of a palette item: drops it onto the area. */\n dropFromPalette: (data: unknown) => void\n}\n\n/** Focus the first selector that matches, after React has committed. */\nconst focusLater = (...selectors: string[]) =>\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n for (const selector of selectors) {\n const el = document.querySelector<HTMLElement>(selector)\n if (el) {\n el.focus()\n return\n }\n }\n })\n )\n\nconst cssEscape = (value: string) => (typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(value) : value)\n\nconst CanvasContext = React.createContext<CanvasContextValue | null>(null)\n\nfunction useCanvas() {\n const ctx = React.useContext(CanvasContext)\n if (!ctx) throw new Error(\"Canvas parts must be used within <Canvas>\")\n return ctx\n}\n\nconst isOwnData = (instanceId: symbol, data: Record<string | symbol, unknown>): data is DragData =>\n data.instanceId === instanceId && (data.type === \"palette\" || data.type === \"node\")\n\nfunction Canvas({\n onDrop,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onDrop\"> & { onDrop?: (details: CanvasDropDetails) => void }) {\n const [instanceId] = React.useState(() => Symbol(\"canvas\"))\n const [dragging, setDragging] = React.useState<CanvasContextValue[\"dragging\"]>(null)\n const [grabbed, setGrabbed] = React.useState<string | null>(null)\n const { message: announcement, announce: setAnnouncement } = useLiveRegion()\n const ref = React.useRef<HTMLDivElement>(null)\n const onDropRef = React.useRef(onDrop)\n React.useEffect(() => {\n onDropRef.current = onDrop\n })\n\n const grab = React.useCallback(\n (nodeId: string | null) => {\n setGrabbed(nodeId)\n setAnnouncement(\n nodeId\n ? `Picked up ${nodeId}. Arrow up or down moves it to its own row, left or right moves it beside a neighbour. Space or Enter drops, Escape cancels.`\n : \"Dropped.\"\n )\n },\n [setAnnouncement]\n )\n\n const moveByKey = React.useCallback(\n (nodeId: string, direction: CanvasDirection) => {\n const root = ref.current\n const node = root?.querySelector<HTMLElement>(`[data-slot=canvas-node][data-value=\"${cssEscape(nodeId)}\"]`)\n const row = node?.closest<HTMLElement>(\"[data-slot=canvas-row]\")\n if (!root || !node || !row) return\n const siblings = Array.from(row.querySelectorAll<HTMLElement>(\":scope > [data-slot=canvas-node]\"))\n const index = siblings.indexOf(node)\n let target: CanvasTarget | null = null\n if (direction === \"up\") {\n const first = siblings[0]\n // Already alone at the top of its row: hop above the previous row instead.\n if (siblings.length === 1) {\n const rows = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=canvas-row]\"))\n const prev = rows[rows.indexOf(row) - 1]?.querySelector<HTMLElement>(\"[data-slot=canvas-node]\")\n if (!prev) return\n target = { type: \"node\", id: prev.dataset.value ?? \"\", edge: \"top\" }\n } else target = { type: \"node\", id: first.dataset.value ?? \"\", edge: \"top\" }\n } else if (direction === \"down\") {\n const last = siblings[siblings.length - 1]\n if (siblings.length === 1) {\n const rows = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=canvas-row]\"))\n const next = rows[rows.indexOf(row) + 1]?.querySelector<HTMLElement>(\"[data-slot=canvas-node]\")\n if (!next) return\n target = { type: \"node\", id: next.dataset.value ?? \"\", edge: \"bottom\" }\n } else target = { type: \"node\", id: last.dataset.value ?? \"\", edge: \"bottom\" }\n } else {\n const neighbour = siblings[direction === \"left\" ? index - 1 : index + 1]\n if (!neighbour) return\n target = { type: \"node\", id: neighbour.dataset.value ?? \"\", edge: direction }\n }\n // A vertical drop on the node itself means \"pull me out into a new row\"; horizontal is a no-op.\n if (target.type === \"node\" && target.id === nodeId && (target.edge === \"left\" || target.edge === \"right\")) return\n onDropRef.current?.({ source: { type: \"node\", id: nodeId }, target })\n setAnnouncement(`Moved ${nodeId} ${direction}.`)\n const nodeSelector = `[data-slot=canvas-node][data-value=\"${cssEscape(nodeId)}\"]`\n focusLater(`${nodeSelector} [data-slot=canvas-node-handle]`, nodeSelector)\n },\n [setAnnouncement]\n )\n\n const dropFromPalette = React.useCallback(\n (data: unknown) => {\n onDropRef.current?.({ source: { type: \"palette\", data }, target: { type: \"area\" } })\n setAnnouncement(\"Added to the canvas.\")\n },\n [setAnnouncement]\n )\n\n React.useEffect(\n () =>\n monitorForElements({\n canMonitor: ({ source }) => isOwnData(instanceId, source.data),\n onDragStart: ({ source }) => {\n const data = source.data as DragData\n setDragging(data.type === \"node\" ? { type: \"node\", id: data.id } : { type: \"palette\" })\n },\n onDrop: ({ source, location }) => {\n setDragging(null)\n const data = source.data as DragData\n const targets = location.current.dropTargets\n const nodeTarget = targets.find((t) => t.data[NODE_TARGET] === true)\n const areaTarget = targets.find((t) => t.data[AREA_TARGET] === true)\n const src: CanvasSource =\n data.type === \"node\" ? { type: \"node\", id: data.id } : { type: \"palette\", data: data.data }\n if (nodeTarget) {\n const id = String(nodeTarget.data.id)\n if (src.type === \"node\" && src.id === id) return\n const edge = extractClosestEdge(nodeTarget.data) ?? \"bottom\"\n onDropRef.current?.({ source: src, target: { type: \"node\", id, edge } })\n } else if (areaTarget) {\n onDropRef.current?.({ source: src, target: { type: \"area\" } })\n }\n },\n }),\n [instanceId]\n )\n\n const ctx = React.useMemo(\n () => ({ instanceId, dragging, grabbed, grab, moveByKey, dropFromPalette }),\n [instanceId, dragging, grabbed, grab, moveByKey, dropFromPalette]\n )\n return (\n <CanvasContext.Provider value={ctx}>\n <div\n ref={ref}\n data-slot=\"canvas\"\n data-dragging={dragging ? dragging.type : undefined}\n data-grabbed={grabbed ? \"\" : undefined}\n className={cn(\"flex min-h-0 flex-1 flex-col gap-4 lg:flex-row\", className)}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"canvas-live-region\" message={announcement} />\n </div>\n </CanvasContext.Provider>\n )\n}\n\n/** Keyboard handling shared by nodes and their handles. */\nfunction useCanvasKeyboard(nodeId: string) {\n const { grabbed, grab, moveByKey } = useCanvas()\n const isGrabbed = grabbed === nodeId\n const onKeyDown = (event: React.KeyboardEvent) => {\n const key = event.key\n if (key === \" \" || key === \"Enter\") {\n event.preventDefault()\n grab(isGrabbed ? null : nodeId)\n return\n }\n if (!isGrabbed) return\n if (key === \"Escape\") {\n event.preventDefault()\n grab(null)\n return\n }\n const direction = { ArrowUp: \"up\", ArrowDown: \"down\", ArrowLeft: \"left\", ArrowRight: \"right\" }[key] as\n CanvasDirection | undefined\n if (!direction) return\n event.preventDefault()\n moveByKey(nodeId, direction)\n }\n return { isGrabbed, onKeyDown, onBlur: () => isGrabbed && grab(null) }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Palette */\n/* -------------------------------------------------------------------------- */\n\nfunction CanvasPalette({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"canvas-palette\"\n className={cn(\n \"flex shrink-0 flex-row flex-wrap gap-1 lg:w-56 lg:flex-col lg:flex-nowrap lg:overflow-y-auto\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** A draggable source. `data` is handed back untouched in `onDrop`. */\nfunction CanvasPaletteItem({ data, className, onKeyDown, ...props }: React.ComponentProps<\"div\"> & { data: unknown }) {\n const { instanceId, dropFromPalette } = useCanvas()\n const ref = React.useRef<HTMLDivElement>(null)\n const dataRef = React.useRef(data)\n React.useEffect(() => {\n dataRef.current = data\n })\n const [isDragging, setIsDragging] = React.useState(false)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return draggable({\n element: el,\n getInitialData: (): DragData => ({ instanceId, type: \"palette\", data: dataRef.current }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n }, [instanceId])\n\n return (\n <div\n ref={ref}\n data-slot=\"canvas-palette-item\"\n data-dragging={isDragging ? \"\" : undefined}\n tabIndex={0}\n role=\"button\"\n aria-roledescription=\"draggable palette item\"\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault()\n dropFromPalette(dataRef.current)\n }\n }}\n className={cn(\n \"flex cursor-grab items-center gap-2 rounded-lg border bg-card px-2.5 py-2 text-sm shadow-xs transition-colors outline-none select-none hover:bg-accent focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-dragging:opacity-50 [&_svg]:size-4 [&_svg]:text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Area + rows */\n/* -------------------------------------------------------------------------- */\n\n/** The drop surface. Dropping here (not on a node) reports `{ type: \"area\" }`. */\nfunction CanvasArea({ className, ...props }: React.ComponentProps<\"div\">) {\n const { instanceId } = useCanvas()\n const ref = React.useRef<HTMLDivElement>(null)\n const [isOver, setIsOver] = React.useState(false)\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n return combine(\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) => isOwnData(instanceId, source.data),\n getData: () => ({ [AREA_TARGET]: true }),\n onDragEnter: ({ self, location }) => setIsOver(location.current.dropTargets[0]?.element === self.element),\n onDrag: ({ self, location }) => {\n const over = location.current.dropTargets[0]?.element === self.element\n setIsOver((prev) => (prev === over ? prev : over))\n },\n onDragLeave: () => setIsOver(false),\n onDrop: () => setIsOver(false),\n }),\n autoScrollForElements({ element: el, canScroll: ({ source }) => isOwnData(instanceId, source.data) })\n )\n }, [instanceId])\n\n return (\n <div\n ref={ref}\n data-slot=\"canvas-area\"\n data-over={isOver ? \"\" : undefined}\n className={cn(\n \"flex min-h-40 min-w-0 flex-1 flex-col gap-3 overflow-y-auto rounded-xl border border-dashed bg-muted/30 p-4 transition-colors data-over:border-primary/50 data-over:bg-primary/5\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CanvasEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"canvas-empty\"\n className={cn(\n \"flex flex-1 flex-col items-center justify-center gap-1 py-16 text-center text-sm text-muted-foreground [&_svg]:mb-1 [&_svg]:size-6\",\n className\n )}\n {...props}\n />\n )\n}\n\ntype RowContextValue = {\n ref: React.RefObject<HTMLDivElement | null>\n /** True while a `CanvasResizeHandle` in this row is being dragged. */\n resizing: boolean\n setResizing: (resizing: boolean) => void\n}\nconst RowContext = React.createContext<RowContextValue | null>(null)\n\n/** A horizontal row of nodes. Node widths are `flex-grow` weights. */\nfunction CanvasRow({ className, ...props }: React.ComponentProps<\"div\">) {\n const ref = React.useRef<HTMLDivElement>(null)\n const [resizing, setResizing] = React.useState(false)\n const ctx = React.useMemo(() => ({ ref, resizing, setResizing }), [resizing])\n return (\n <RowContext.Provider value={ctx}>\n <div\n ref={ref}\n data-slot=\"canvas-row\"\n data-resizing={resizing ? \"\" : undefined}\n className={cn(\"flex items-stretch gap-3\", className)}\n {...props}\n />\n </RowContext.Provider>\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Node */\n/* -------------------------------------------------------------------------- */\n\ntype NodeContextValue = { handleRef: React.RefObject<HTMLElement | null>; isDragging: boolean; value: string }\nconst NodeContext = React.createContext<NodeContextValue | null>(null)\n\nfunction CanvasNode({\n value,\n width = 1,\n draggable: isDraggable = true,\n selected,\n className,\n children,\n style,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"div\"> & {\n value: string\n /** Relative width within the row (flex-grow weight). */\n width?: number\n draggable?: boolean\n selected?: boolean\n}) {\n const { instanceId } = useCanvas()\n const keyboard = useCanvasKeyboard(value)\n const row = React.useContext(RowContext)\n const ref = React.useRef<HTMLDivElement>(null)\n const handleRef = React.useRef<HTMLElement | null>(null)\n const [isDragging, setIsDragging] = React.useState(false)\n const [closestEdge, setClosestEdge] = React.useState<Edge | null>(null)\n const [percent, setPercent] = React.useState<number | null>(null)\n const resizing = row?.resizing ?? false\n\n // While the row is being resized, show this node's share of the row.\n React.useLayoutEffect(() => {\n const el = ref.current\n const rowEl = row?.ref.current\n if (!resizing || !el || !rowEl) {\n setPercent(null)\n return\n }\n const siblings = Array.from(rowEl.querySelectorAll<HTMLElement>(\":scope > [data-slot=canvas-node]\"))\n const total = siblings.reduce((sum, n) => sum + n.getBoundingClientRect().width, 0)\n setPercent(total ? Math.round((el.getBoundingClientRect().width / total) * 100) : null)\n }, [resizing, width, row])\n\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const cleanups = [\n dropTargetForElements({\n element: el,\n canDrop: ({ source }) =>\n isOwnData(instanceId, source.data) && !(source.data.type === \"node\" && source.data.id === value),\n getIsSticky: () => true,\n getData: ({ input, element }) =>\n attachClosestEdge(\n { [NODE_TARGET]: true, id: value },\n { element, input, allowedEdges: [\"top\", \"bottom\", \"left\", \"right\"] }\n ),\n onDragEnter: ({ self }) => setClosestEdge(extractClosestEdge(self.data)),\n onDrag: ({ self }) => {\n const edge = extractClosestEdge(self.data)\n setClosestEdge((prev) => (prev === edge ? prev : edge))\n },\n onDragLeave: () => setClosestEdge(null),\n onDrop: () => setClosestEdge(null),\n }),\n ]\n if (isDraggable) {\n cleanups.push(\n draggable({\n element: el,\n dragHandle: handleRef.current ?? undefined,\n getInitialData: (): DragData => ({ instanceId, type: \"node\", id: value }),\n onGenerateDragPreview: (args) => cloneDragPreview(args),\n onDragStart: () => setIsDragging(true),\n onDrop: () => setIsDragging(false),\n })\n )\n }\n return combine(...cleanups)\n }, [instanceId, value, isDraggable])\n\n const [hasHandle, setHasHandle] = React.useState(false)\n React.useEffect(() => setHasHandle(!!handleRef.current), [])\n const ctx = React.useMemo(() => ({ handleRef, isDragging, value }), [isDragging, value])\n return (\n <NodeContext.Provider value={ctx}>\n <div\n ref={ref}\n data-slot=\"canvas-node\"\n data-value={value}\n data-dragging={isDragging ? \"\" : undefined}\n data-edge={closestEdge ?? undefined}\n data-selected={selected ? \"\" : undefined}\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n tabIndex={isDraggable && !hasHandle ? 0 : undefined}\n aria-roledescription={isDraggable && !hasHandle ? \"draggable item\" : undefined}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented && !hasHandle && event.target === event.currentTarget) keyboard.onKeyDown(event)\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (!hasHandle && event.target === event.currentTarget) keyboard.onBlur()\n }}\n style={{ flexGrow: width, flexBasis: 0, ...style }}\n className={cn(\n \"group/canvas-node relative flex min-w-0 flex-col rounded-lg border bg-card p-3 text-sm shadow-xs transition-[opacity,box-shadow,border-color] outline-none hover:border-foreground/20 focus-visible:ring-3 focus-visible:ring-ring/50 data-dragging:opacity-40 data-grabbed:border-primary data-grabbed:ring-3 data-grabbed:ring-primary/30 data-selected:border-primary data-selected:ring-3 data-selected:ring-primary/20\",\n className\n )}\n {...props}\n >\n {children}\n {closestEdge && <CanvasDropIndicator edge={closestEdge} />}\n {percent !== null && <CanvasNodeOverlay>{percent}%</CanvasNodeOverlay>}\n </div>\n </NodeContext.Provider>\n )\n}\n\n/** Optional explicit drag handle; without it the whole node is the handle. */\nfunction CanvasNodeHandle({\n className,\n children,\n onKeyDown,\n onBlur,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button>) {\n const ctx = React.useContext(NodeContext)\n const keyboard = useCanvasKeyboard(ctx?.value ?? \"\")\n return (\n <ark.button\n asChild={asChild}\n ref={(el: HTMLElement | null) => {\n if (ctx) ctx.handleRef.current = el\n }}\n type=\"button\"\n data-slot=\"canvas-node-handle\"\n data-grabbed={keyboard.isGrabbed ? \"\" : undefined}\n aria-label=\"Drag\"\n aria-pressed={keyboard.isGrabbed}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (!event.defaultPrevented) {\n keyboard.onKeyDown(event)\n event.stopPropagation()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n keyboard.onBlur()\n }}\n className={cn(\n \"flex size-6 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:bg-primary data-grabbed:text-primary-foreground [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {asChild ? children : (children ?? <GripVerticalIcon />)}\n </ark.button>\n )\n}\n\nfunction CanvasNodeHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"canvas-node-header\" className={cn(\"mb-2 flex items-center gap-1\", className)} {...props} />\n}\n\nfunction CanvasNodeTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"canvas-node-title\"\n className={cn(\"min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/** A blurring overlay with a centered pill, e.g. the node's width share while resizing. */\nfunction CanvasNodeOverlay({ className, children, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"canvas-node-overlay\"\n aria-hidden\n className={cn(\n \"pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-background/50 backdrop-blur-xs\",\n className\n )}\n {...props}\n >\n <span\n data-slot=\"canvas-node-overlay-pill\"\n className=\"rounded-full bg-foreground px-2.5 py-1 text-xs font-semibold text-background tabular-nums shadow-md\"\n >\n {children}\n </span>\n </div>\n )\n}\n\n/** Actions revealed on hover/selection. */\nfunction CanvasNodeActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"canvas-node-actions\"\n className={cn(\n \"ms-auto flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/canvas-node:opacity-100 group-hover/canvas-node:opacity-100 group-data-selected/canvas-node:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Resize handle */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Place between two nodes in a `CanvasRow`. Pointer drags call `onResize`\n * with the horizontal movement as a fraction of the row width (signed).\n */\nfunction CanvasResizeHandle({\n onResize,\n onResizeStart,\n onResizeEnd,\n className,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onResize\"> & {\n onResize: (deltaFraction: number) => void\n onResizeStart?: () => void\n onResizeEnd?: () => void\n}) {\n const row = React.useContext(RowContext)\n const [active, setActive] = React.useState(false)\n const handlers = React.useRef({ onResize, onResizeStart, onResizeEnd })\n React.useEffect(() => {\n handlers.current = { onResize, onResizeStart, onResizeEnd }\n })\n\n const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n if (event.button !== 0) return\n const rowEl = row?.ref.current\n if (!rowEl) return\n event.preventDefault()\n const target = event.currentTarget\n try {\n target.setPointerCapture(event.pointerId)\n } catch {\n // Synthetic pointers can't be captured; moves still arrive via bubbling.\n }\n let lastX = event.clientX\n setActive(true)\n row.setResizing(true)\n handlers.current.onResizeStart?.()\n const move = (e: PointerEvent) => {\n const width = rowEl.getBoundingClientRect().width\n if (!width) return\n const delta = (e.clientX - lastX) / width\n lastX = e.clientX\n if (delta !== 0) handlers.current.onResize(delta)\n }\n const up = () => {\n target.removeEventListener(\"pointermove\", move)\n target.removeEventListener(\"pointerup\", up)\n target.removeEventListener(\"pointercancel\", up)\n setActive(false)\n row.setResizing(false)\n handlers.current.onResizeEnd?.()\n }\n target.addEventListener(\"pointermove\", move)\n target.addEventListener(\"pointerup\", up)\n target.addEventListener(\"pointercancel\", up)\n }\n\n return (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n data-slot=\"canvas-resize-handle\"\n data-active={active ? \"\" : undefined}\n onPointerDown={onPointerDown}\n className={cn(\n \"group/resize relative -mx-2 flex w-4 shrink-0 cursor-col-resize touch-none items-center justify-center select-none\",\n className\n )}\n {...props}\n >\n <span className=\"h-8 w-1 rounded-full bg-border transition-colors group-hover/resize:bg-primary/60 group-data-active/resize:bg-primary\" />\n </div>\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Drop indicator */\n/* -------------------------------------------------------------------------- */\n\nfunction CanvasDropIndicator({\n edge,\n gap = \"0.75rem\",\n className,\n style,\n ...props\n}: React.ComponentProps<\"div\"> & { edge: Edge; gap?: string }) {\n const horizontal = edge === \"top\" || edge === \"bottom\"\n return (\n <div\n data-slot=\"canvas-drop-indicator\"\n data-edge={edge}\n aria-hidden\n style={{ [\"--canvas-gap\" as string]: gap, ...style }}\n className={cn(\n \"pointer-events-none absolute z-10 rounded-full bg-primary\",\n \"before:absolute before:size-2 before:rounded-full before:border-2 before:border-primary before:bg-background\",\n horizontal\n ? \"inset-x-0 h-0.5 before:top-1/2 before:-left-1 before:-translate-y-1/2\"\n : \"inset-y-0 w-0.5 before:-top-1 before:left-1/2 before:-translate-x-1/2\",\n edge === \"top\" && \"top-[calc(-1*var(--canvas-gap)/2-1px)]\",\n edge === \"bottom\" && \"bottom-[calc(-1*var(--canvas-gap)/2-1px)]\",\n edge === \"left\" && \"left-[calc(-1*var(--canvas-gap)/2-1px)]\",\n edge === \"right\" && \"right-[calc(-1*var(--canvas-gap)/2-1px)]\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Canvas,\n CanvasArea,\n CanvasDropIndicator,\n CanvasEmpty,\n CanvasNode,\n CanvasNodeActions,\n CanvasNodeHandle,\n CanvasNodeHeader,\n CanvasNodeOverlay,\n CanvasNodeTitle,\n CanvasPalette,\n CanvasPaletteItem,\n CanvasResizeHandle,\n CanvasRow,\n useCanvas,\n type CanvasDropDetails,\n type CanvasSource,\n type CanvasTarget,\n}\n"
21
+ }
22
+ ],
23
+ "docs": "## Canvas\n`src/components/ui/canvas.tsx` is a compositional, data-agnostic drag-and-drop layout surface\non Pragmatic drag and drop. `Canvas` owns nothing but drag state; it reports every drop as an\nintent via `onDrop({ source, target })` where `source` is `{ type: \"palette\", data }` (from a\n`CanvasPaletteItem`, `data` passed back untouched) or `{ type: \"node\", id }`, and `target` is\n`{ type: \"node\", id, edge }` (closest of top/bottom/left/right) or `{ type: \"area\" }`. Parts:\n`CanvasPalette/PaletteItem`, `CanvasArea` (drop surface, `data-over`, auto-scroll),\n`CanvasEmpty`, `CanvasRow` (flex row; node `width` is a flex-grow weight), `CanvasNode`\n(`value`, `width`, `selected`, draggable via optional `CanvasNodeHandle`, four-edge drop\ntarget with `data-edge` + `CanvasDropIndicator`), `CanvasNodeHeader/Title/Actions`, and\n`CanvasResizeHandle` (pointer-driven; calls `onResize(deltaFraction)` relative to the row\nwidth; place it between siblings). While a handle is dragged, `CanvasRow` sets\n`data-resizing` and every node in that row (only that row) renders `CanvasNodeOverlay`: a\nblurring overlay with a pill showing its percentage share, computed from rendered widths. Stacks palette above the area below `lg`. Keyboard: nodes (or their handle) pick up with\nSpace/Enter; ArrowUp/Down emit a top/bottom-edge drop on the row's first/last node (or hop\nto the adjacent row when alone), ArrowLeft/Right emit a left/right-edge drop on the\nneighbour; palette items are focusable and Enter/Space drops them on the area. Same live\nregion + clone preview approach as Kanban.\n`src/demo/form-builder/` is the reference (route `/forms`), a composition only: `model.ts`\n(schema `{ title, description, rows: { fields }[] }`, field types text/email/url/phone/number/\ntextarea/date/select/multiselect/radio/checkbox/switch + heading/paragraph/divider, `name` key\nslugified from the label until edited, `options`, `validation` min/max/minLength/maxLength/\npattern, `width`, `condition: QueryGroup | null`; `applyDrop`, `resizeRow`, `updateField`,\n`removeField`, `duplicateField`, `findProblems`, `serializeSchema`/`parseSchema`,\n`conditionFields` mapping other fields to query-builder fields), `form-builder-page.tsx` (Canvas\nwith a grouped palette, node actions edit/duplicate/remove, a Drawer inspector with Basics /\nOptions editor / Validation (format presets or custom regex) / Visibility built from the\nQueryBuilder parts, Build|Preview `SegmentGroup`, undo/redo on `src/lib/history.ts`, Import\ndialog + Export/Copy JSON, problem badges) and `form-renderer.tsx` (renders a schema on the\ntoolkit controls with `useForm`, live conditions via the query-builder demo interpreter, hidden\nfields dropped from the submission). Nothing here is use-case specific in `components/ui`."
24
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "card",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "utils"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/card.tsx",
12
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({ className, size = \"default\", ...props }: React.ComponentProps<\"div\"> & { size?: \"default\" | \"sm\" }) {\n return (\n <div\n data-slot=\"card\"\n data-size={size}\n className={cn(\n \"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"font-heading text-base/snug font-medium group-data-[size=sm]/card:text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"card-description\" className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\"col-start-2 row-span-2 row-start-1 self-start justify-self-end\", className)}\n {...props}\n />\n )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"card-content\" className={cn(\"px-(--card-spacing)\", className)} {...props} />\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)\", className)}\n {...props}\n />\n )\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "carousel",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "embla-carousel-react": "^8.6.0",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/carousel.tsx",
16
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport useEmblaCarousel, { type UseEmblaCarouselType } from \"embla-carousel-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\ntype CarouselApi = UseEmblaCarouselType[1]\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>\ntype CarouselOptions = UseCarouselParameters[0]\ntype CarouselPlugin = UseCarouselParameters[1]\n\ntype CarouselProps = {\n opts?: CarouselOptions\n plugins?: CarouselPlugin\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof useEmblaCarousel>[0]\n api: ReturnType<typeof useEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: boolean\n canScrollNext: boolean\n} & CarouselProps\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null)\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context\n}\n\nfunction Carousel({\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n const [carouselRef, api] = useEmblaCarousel(\n {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n },\n plugins\n )\n const [canScrollPrev, setCanScrollPrev] = React.useState(false)\n const [canScrollNext, setCanScrollNext] = React.useState(false)\n\n const onSelect = React.useCallback((api: CarouselApi) => {\n if (!api) return\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }, [])\n\n const scrollPrev = React.useCallback(() => {\n api?.scrollPrev()\n }, [api])\n\n const scrollNext = React.useCallback(() => {\n api?.scrollNext()\n }, [api])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n },\n [scrollPrev, scrollNext]\n )\n\n React.useEffect(() => {\n if (!api || !setApi) return\n setApi(api)\n }, [api, setApi])\n\n React.useEffect(() => {\n if (!api) return\n onSelect(api)\n api.on(\"reInit\", onSelect)\n api.on(\"select\", onSelect)\n\n return () => {\n api?.off(\"select\", onSelect)\n }\n }, [api, onSelect])\n\n return (\n <CarouselContext.Provider\n value={{\n carouselRef,\n api: api,\n opts,\n orientation: orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext,\n }}\n >\n <div\n onKeyDownCapture={handleKeyDown}\n className={cn(\"relative\", className)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n data-slot=\"carousel\"\n {...props}\n >\n {children}\n </div>\n </CarouselContext.Provider>\n )\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div ref={carouselRef} className=\"overflow-hidden\" data-slot=\"carousel-content\">\n <div className={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", className)} {...props} />\n </div>\n )\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const { orientation } = useCarousel()\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n data-slot=\"carousel-item\"\n className={cn(\"min-w-0 shrink-0 grow-0 basis-full\", orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\", className)}\n {...props}\n />\n )\n}\n\nfunction CarouselPrevious({\n className,\n variant = \"outline\",\n size = \"icon-sm\",\n asChild,\n children,\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-previous\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute touch-manipulation rounded-full\",\n orientation === \"horizontal\" ? \"inset-y-0 -left-12 my-auto\" : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollPrev}\n onClick={scrollPrev}\n asChild={asChild}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <ChevronLeftIcon />\n <span className=\"sr-only\">Previous slide</span>\n </>\n ))}\n </Button>\n )\n}\n\nfunction CarouselNext({\n className,\n variant = \"outline\",\n size = \"icon-sm\",\n asChild,\n children,\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-next\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute touch-manipulation rounded-full\",\n orientation === \"horizontal\" ? \"inset-y-0 -right-12 my-auto\" : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollNext}\n onClick={scrollNext}\n asChild={asChild}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <ChevronRightIcon />\n <span className=\"sr-only\">Next slide</span>\n </>\n ))}\n </Button>\n )\n}\n\nexport { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, useCarousel }\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "cascader",
3
+ "type": "ui",
4
+ "description": "-------------------------------- helpers --------------------------------",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "controllable",
12
+ "input-group",
13
+ "popover",
14
+ "utils"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/cascader.tsx",
19
+ "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"
20
+ }
21
+ ],
22
+ "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."
23
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "chart",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "recharts": "^3.8.0"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/chart.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport * as RechartsPrimitive from \"recharts\"\nimport type { TooltipValueType } from \"recharts\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nconst INITIAL_DIMENSION = { width: 320, height: 200 } as const\ntype TooltipNameType = number | string\n\nexport type ChartConfig = Record<\n string,\n {\n label?: React.ReactNode\n icon?: React.ComponentType\n } & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })\n>\n\ntype ChartContextProps = {\n config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction useChart() {\n const context = React.useContext(ChartContext)\n\n if (!context) {\n throw new Error(\"useChart must be used within a <ChartContainer />\")\n }\n\n return context\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n initialDimension = INITIAL_DIMENSION,\n ...props\n}: React.ComponentProps<\"div\"> & {\n config: ChartConfig\n children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>[\"children\"]\n initialDimension?: {\n width: number\n height: number\n }\n}) {\n const uniqueId = React.useId()\n const chartId = `chart-${id ?? uniqueId.replace(/:/g, \"\")}`\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n )\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([, config]) => config.theme ?? config.color)\n\n if (!colorConfig.length) {\n return null\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? itemConfig.color\n return color ? ` --color-${key}: ${color};` : null\n })\n .join(\"\\n\")}\n}\n`\n )\n .join(\"\\n\"),\n }}\n />\n )\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nfunction ChartTooltipContent({\n active,\n payload,\n className,\n indicator = \"dot\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<\"div\"> & {\n hideLabel?: boolean\n hideIndicator?: boolean\n indicator?: \"line\" | \"dot\" | \"dashed\"\n nameKey?: string\n labelKey?: string\n } & Omit<RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>, \"accessibilityLayer\">) {\n const { config } = useChart()\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null\n }\n\n const [item] = payload\n const key = `${labelKey ?? item?.dataKey ?? item?.name ?? \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const value = !labelKey && typeof label === \"string\" ? (config[label]?.label ?? label) : itemConfig?.label\n\n if (labelFormatter) {\n return <div className={cn(\"font-medium\", labelClassName)}>{labelFormatter(value, payload)}</div>\n }\n\n if (!value) {\n return null\n }\n\n return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])\n\n if (!active || !payload?.length) {\n return null\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n return (\n <div\n className={cn(\n \"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n className\n )}\n >\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey ?? item.name ?? item.dataKey ?? \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const indicatorColor = color ?? item.payload?.fill ?? item.color\n\n return (\n <div\n key={index}\n className={cn(\n \"flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5 [&>svg]:text-muted-foreground\",\n indicator === \"dot\" && \"items-center\"\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn(\"shrink-0 rounded-xs border-border bg-(--color-bg)\", {\n \"size-2.5\": indicator === \"dot\",\n \"w-1\": indicator === \"line\",\n \"w-0 border-[1.5px] border-dashed bg-transparent\": indicator === \"dashed\",\n \"my-0.5\": nestLabel && indicator === \"dashed\",\n })}\n style={\n {\n \"--color-bg\": indicatorColor,\n \"--color-border\": indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n \"flex flex-1 justify-between leading-none\",\n nestLabel ? \"items-end\" : \"items-center\"\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">{itemConfig?.label ?? item.name}</span>\n </div>\n {item.value != null && (\n <span className=\"font-mono font-medium text-foreground tabular-nums\">\n {typeof item.value === \"number\" ? item.value.toLocaleString() : String(item.value)}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = \"bottom\",\n nameKey,\n}: React.ComponentProps<\"div\"> & {\n hideIcon?: boolean\n nameKey?: string\n} & RechartsPrimitive.DefaultLegendContentProps) {\n const { config } = useChart()\n\n if (!payload?.length) {\n return null\n }\n\n return (\n <div className={cn(\"flex items-center justify-center gap-4\", verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\", className)}>\n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey ?? item.dataKey ?? \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n return (\n <div key={index} className={cn(\"flex items-center gap-1.5 [&>svg]:size-3 [&>svg]:text-muted-foreground\")}>\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"size-2 shrink-0 rounded-xs\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n )\n })}\n </div>\n )\n}\n\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined\n }\n\n const payloadPayload =\n \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null\n ? payload.payload\n : undefined\n\n let configLabelKey: string = key\n\n if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n configLabelKey = payload[key as keyof typeof payload] as string\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n ) {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key]\n}\n\nexport { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "chat",
3
+ "type": "ui",
4
+ "description": "`src/components/ui/chat.tsx` is a compositional, data-agnostic chat layout primitive (sidebar with search and conversation list, panel with header/messages/composer, empty state).",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "class-variance-authority": "^0.7.1",
8
+ "lucide-react": "^1.41.0"
9
+ },
10
+ "registryDependencies": [
11
+ "button",
12
+ "controllable",
13
+ "scroll-area",
14
+ "utils"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/chat.tsx",
19
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { ArrowLeftIcon, MessagesSquareIcon, SearchIcon, SendIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\n\n/* -------------------------------------------------------------------------- */\n/* Root and context */\n/* -------------------------------------------------------------------------- */\n\ntype ChatContextValue = {\n /** Id of the selected conversation. */\n value: string | null\n setValue: (value: string | null) => void\n /** On small screens the panel overlays the list only while this is true. */\n mobileOpen: boolean\n setMobileOpen: (open: boolean) => void\n}\n\nconst ChatContext = React.createContext<ChatContextValue | null>(null)\n\nfunction useChat(component = \"Chat parts\") {\n const ctx = React.useContext(ChatContext)\n if (!ctx) throw new Error(`${component} must be used within <Chat>`)\n return ctx\n}\n\nfunction Chat({\n className,\n value: valueProp,\n defaultValue = null,\n onValueChange,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\"> & {\n value?: string | null\n defaultValue?: string | null\n onValueChange?: (value: string | null) => void\n}) {\n const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange)\n const [mobileOpen, setMobileOpen] = React.useState(value !== null)\n const ctx = React.useMemo(() => ({ value, setValue, mobileOpen, setMobileOpen }), [value, setValue, mobileOpen])\n return (\n <ChatContext.Provider value={ctx}>\n <div\n data-slot=\"chat\"\n data-selected={value !== null ? \"\" : undefined}\n className={cn(\"relative flex h-full gap-6\", className)}\n {...props}\n />\n </ChatContext.Provider>\n )\n}\n\nfunction ChatContextConsumer({ children }: { children: (ctx: ChatContextValue) => React.ReactNode }) {\n return children(useChat(\"ChatContext\"))\n}\n\n/* -------------------------------------------------------------------------- */\n/* Sidebar: header, search, conversation list */\n/* -------------------------------------------------------------------------- */\n\nfunction ChatSidebar({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-sidebar\"\n className={cn(\"flex w-full flex-col gap-2 sm:w-56 lg:w-72 2xl:w-80\", className)}\n {...props}\n />\n )\n}\n\nfunction ChatSidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-sidebar-header\"\n className={cn(\n \"sticky top-0 z-10 -mx-4 flex flex-col gap-2 bg-background px-4 pb-3 shadow-md sm:static sm:z-auto sm:mx-0 sm:p-0 sm:shadow-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ChatSidebarTitle({ className, children, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"chat-sidebar-title\" className={cn(\"flex items-center justify-between py-2\", className)} {...props}>\n {children}\n </div>\n )\n}\n\nfunction ChatSidebarHeading({ className, children, ...props }: React.ComponentProps<\"h1\">) {\n return (\n <h1\n data-slot=\"chat-sidebar-heading\"\n className={cn(\"flex items-center gap-2 text-2xl font-bold\", className)}\n {...props}\n >\n {children}\n <MessagesSquareIcon className=\"size-5\" />\n </h1>\n )\n}\n\nfunction ChatSearch({ className, ...props }: React.ComponentProps<\"input\">) {\n return (\n <label\n data-slot=\"chat-search\"\n className={cn(\n \"flex h-9 w-full items-center gap-2 rounded-lg border border-input bg-transparent px-2.5 text-sm transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 dark:bg-input/30\",\n className\n )}\n >\n <SearchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"sr-only\">Search</span>\n <input\n type=\"text\"\n data-slot=\"chat-search-input\"\n className=\"min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground\"\n {...props}\n />\n </label>\n )\n}\n\nfunction ChatConversationList({ className, children, ...props }: React.ComponentProps<typeof ScrollArea>) {\n return (\n <ScrollArea data-slot=\"chat-conversation-list\" className={cn(\"-mx-3 h-full flex-1\", className)} {...props}>\n <div role=\"list\" className=\"flex flex-col p-3 *:border-b *:border-border *:last:border-b-0\">\n {children}\n </div>\n </ScrollArea>\n )\n}\n\nfunction ChatConversationItem({\n className,\n value,\n onClick,\n asChild,\n ...props\n}: React.ComponentProps<typeof ark.button> & { value: string }) {\n const ctx = useChat(\"ChatConversationItem\")\n const selected = ctx.value === value\n return (\n <div role=\"listitem\" className=\"py-1\">\n <ark.button\n asChild={asChild}\n type=\"button\"\n data-slot=\"chat-conversation-item\"\n data-selected={selected ? \"\" : undefined}\n aria-current={selected ? \"true\" : undefined}\n className={cn(\n \"group/chat-conversation-item flex w-full gap-2 rounded-md p-2 text-start text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-selected:sm:bg-muted\",\n className\n )}\n onClick={(event) => {\n onClick?.(event)\n if (event.defaultPrevented) return\n ctx.setValue(value)\n ctx.setMobileOpen(true)\n }}\n {...props}\n />\n </div>\n )\n}\n\nfunction ChatConversationInfo({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-conversation-info\" className={cn(\"flex min-w-0 flex-col\", className)} {...props} />\n}\n\nfunction ChatConversationName({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"chat-conversation-name\" className={cn(\"font-medium\", className)} {...props} />\n}\n\nfunction ChatConversationPreview({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"chat-conversation-preview\"\n className={cn(\n \"line-clamp-2 text-muted-foreground group-hover/chat-conversation-item:text-accent-foreground/90\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Panel: header, messages, composer */\n/* -------------------------------------------------------------------------- */\n\nfunction ChatPanel({ className, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useChat(\"ChatPanel\")\n return (\n <div\n data-slot=\"chat-panel\"\n data-mobile-open={ctx.mobileOpen ? \"\" : undefined}\n className={cn(\n \"absolute inset-0 inset-s-full z-50 hidden w-full flex-1 flex-col border bg-background shadow-xs data-mobile-open:inset-s-0 data-mobile-open:flex sm:static sm:z-auto sm:flex sm:rounded-md\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ChatHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-header\"\n className={cn(\n \"mb-1 flex flex-none items-center justify-between gap-3 bg-card p-4 shadow-lg sm:rounded-t-md\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** Returns to the conversation list on small screens. Polymorphic via `asChild`. */\nfunction ChatBackTrigger({ className, onClick, children, asChild, ...props }: React.ComponentProps<typeof Button>) {\n const ctx = useChat(\"ChatBackTrigger\")\n return (\n <Button\n data-slot=\"chat-back-trigger\"\n size=\"icon\"\n variant=\"ghost\"\n asChild={asChild}\n className={cn(\"-ms-2 sm:hidden\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) ctx.setMobileOpen(false)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <ArrowLeftIcon className=\"rtl:rotate-180\" />\n <span className=\"sr-only\">Back</span>\n </>\n ))}\n </Button>\n )\n}\n\nfunction ChatHeaderInfo({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-header-info\"\n className={cn(\"flex min-w-0 items-center gap-2 lg:gap-4\", className)}\n {...props}\n />\n )\n}\n\nfunction ChatHeaderText({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-header-text\" className={cn(\"flex min-w-0 flex-col\", className)} {...props} />\n}\n\nfunction ChatHeaderTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n return <span data-slot=\"chat-header-title\" className={cn(\"text-sm font-medium lg:text-base\", className)} {...props} />\n}\n\nfunction ChatHeaderDescription({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"chat-header-description\"\n className={cn(\"line-clamp-1 max-w-32 text-xs text-muted-foreground lg:max-w-none lg:text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ChatHeaderActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-header-actions\"\n className={cn(\"-me-1 flex items-center gap-1 lg:gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction ChatBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-body\" className={cn(\"flex flex-1 flex-col gap-2 px-4 pt-0 pb-4\", className)} {...props} />\n}\n\n/**\n * Bottom-anchored message stream. Render messages newest-first: the column is\n * reversed so the latest message sits at the bottom and the scroll position\n * stays pinned there as messages arrive.\n */\nfunction ChatMessages({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"chat-messages\" className=\"relative -me-4 flex flex-1 flex-col overflow-y-hidden\">\n <div\n data-slot=\"chat-messages-viewport\"\n className={cn(\n \"flex h-40 w-full grow flex-col-reverse justify-start gap-4 overflow-y-auto py-2 pe-4 pb-4\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction ChatDateSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-date-separator\" className={cn(\"text-center text-xs\", className)} {...props} />\n}\n\nconst chatMessageVariants = cva(\"group/chat-message max-w-72 px-3 py-2 wrap-break-word shadow-lg\", {\n variants: {\n variant: {\n sent: \"self-end rounded-[16px_16px_0_16px] bg-primary/90 text-primary-foreground/75\",\n received: \"self-start rounded-[16px_16px_16px_0] bg-muted\",\n },\n },\n defaultVariants: { variant: \"received\" },\n})\n\nfunction ChatMessage({\n className,\n variant = \"received\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof chatMessageVariants>) {\n return (\n <div\n data-slot=\"chat-message\"\n data-variant={variant}\n className={cn(chatMessageVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction ChatMessageTime({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"chat-message-time\"\n className={cn(\n \"mt-1 block text-xs font-light text-foreground/75 italic group-data-[variant=sent]/chat-message:text-end group-data-[variant=sent]/chat-message:text-primary-foreground/85\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ChatComposer({ className, ...props }: React.ComponentProps<\"form\">) {\n return <form data-slot=\"chat-composer\" className={cn(\"flex w-full flex-none gap-2\", className)} {...props} />\n}\n\nfunction ChatComposerField({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-composer-field\"\n className={cn(\n \"flex flex-1 items-center gap-2 rounded-md border border-input bg-card px-2 py-1 transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 lg:gap-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ChatComposerActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-composer-actions\" className={cn(\"flex items-center gap-1\", className)} {...props} />\n}\n\nfunction ChatComposerInput({ className, ...props }: React.ComponentProps<\"input\">) {\n return (\n <label className=\"flex-1\">\n <span className=\"sr-only\">Message</span>\n <input\n type=\"text\"\n data-slot=\"chat-composer-input\"\n className={cn(\n \"h-8 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground\",\n className\n )}\n {...props}\n />\n </label>\n )\n}\n\n/** Submits the composer form. Polymorphic via `asChild`. */\nfunction ChatComposerSendTrigger({\n className,\n children,\n asChild,\n variant = \"ghost\",\n size = \"icon\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <Button\n data-slot=\"chat-composer-send-trigger\"\n type=\"submit\"\n variant={variant}\n size={size}\n asChild={asChild}\n className={className}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <SendIcon />\n <span className=\"sr-only\">Send</span>\n </>\n ))}\n </Button>\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Empty state */\n/* -------------------------------------------------------------------------- */\n\nfunction ChatEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-empty\"\n className={cn(\n \"absolute inset-0 inset-s-full z-50 hidden w-full flex-1 flex-col justify-center rounded-md border bg-card shadow-xs sm:static sm:z-auto sm:flex\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ChatEmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"chat-empty-content\" className={cn(\"flex flex-col items-center gap-6\", className)} {...props} />\n}\n\nfunction ChatEmptyIcon({ className, children, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"chat-empty-icon\"\n className={cn(\n \"flex size-16 items-center justify-center rounded-full border-2 border-border [&_svg]:size-8\",\n className\n )}\n {...props}\n >\n {children ?? <MessagesSquareIcon />}\n </div>\n )\n}\n\nfunction ChatEmptyTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n return <h2 data-slot=\"chat-empty-title\" className={cn(\"text-center text-xl font-semibold\", className)} {...props} />\n}\n\nfunction ChatEmptyDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"chat-empty-description\"\n className={cn(\"text-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* -------------------------------------------------------------------------- */\n/* Helpers */\n/* -------------------------------------------------------------------------- */\n\n/** Group items by a key while preserving first-seen order of the keys. */\nfunction groupBy<T>(items: T[], getKey: (item: T) => string): [string, T[]][] {\n const groups = new Map<string, T[]>()\n for (const item of items) {\n const key = getKey(item)\n const list = groups.get(key)\n if (list) list.push(item)\n else groups.set(key, [item])\n }\n return Array.from(groups.entries())\n}\n\n/** \"Jane Doe\" -> \"JD\"; single word -> first two letters; empty -> \"?\". */\nfunction getInitials(name: string) {\n const parts = name.trim().split(/\\s+/).filter(Boolean)\n if (parts.length === 0) return \"?\"\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()\n}\n\nexport {\n Chat,\n ChatBackTrigger,\n ChatBody,\n ChatComposer,\n ChatComposerActions,\n ChatComposerField,\n ChatComposerInput,\n ChatComposerSendTrigger,\n ChatContextConsumer as ChatContext,\n ChatConversationInfo,\n ChatConversationItem,\n ChatConversationList,\n ChatConversationName,\n ChatConversationPreview,\n ChatDateSeparator,\n ChatEmpty,\n ChatEmptyContent,\n ChatEmptyDescription,\n ChatEmptyIcon,\n ChatEmptyTitle,\n ChatHeader,\n ChatHeaderActions,\n ChatHeaderDescription,\n ChatHeaderInfo,\n ChatHeaderText,\n ChatHeaderTitle,\n ChatMessage,\n ChatMessageTime,\n ChatMessages,\n ChatPanel,\n ChatSearch,\n ChatSidebar,\n ChatSidebarHeader,\n ChatSidebarHeading,\n ChatSidebarTitle,\n chatMessageVariants,\n getInitials,\n groupBy,\n useChat,\n}\n"
20
+ }
21
+ ],
22
+ "docs": "## Chat\n`src/components/ui/chat.tsx` is a compositional, data-agnostic chat layout primitive\n(sidebar with search and conversation list, panel with header/messages/composer, empty\nstate). `Chat` owns only the selected conversation id (`value`/`onValueChange`) and the\nmobile list-vs-panel state; data, filtering, grouping, and sending are the consumer's.\nMessages render newest-first inside a reversed column so the stream stays pinned to the\nbottom. `src/demo/chat/` is the reference implementation (chats page from shadcn-admin).\n`src/app.tsx` mounts the chat demo at the `/inbox` hash route inside the app shell."
23
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "checkbox",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/checkbox.tsx",
15
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Checkbox as CheckboxPrimitive } from \"@ark-ui/react\"\nimport { CheckIcon, MinusIcon } from \"lucide-react\"\n\nconst checkboxControlClassName =\n \"relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 data-focus-visible:border-ring data-focus-visible:ring-3 data-focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-invalid:data-checked:border-primary dark:bg-input/30 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary\"\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction Checkbox({ className, children, ...props }: Omit<React.ComponentProps<typeof CheckboxPrimitive.Root>, \"id\">) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className=\"group/checkbox peer inline-flex items-center gap-2\"\n {...props}\n >\n <CheckboxControl className={className}>\n <CheckboxIndicator>\n <CheckIcon />\n </CheckboxIndicator>\n <CheckboxIndicator indeterminate>\n <MinusIcon />\n </CheckboxIndicator>\n </CheckboxControl>\n {children && <CheckboxLabel>{children}</CheckboxLabel>}\n <CheckboxHiddenInput />\n </CheckboxPrimitive.Root>\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction CheckboxRoot({ ...props }: Omit<React.ComponentProps<typeof CheckboxPrimitive.Root>, \"id\">) {\n return <CheckboxPrimitive.Root data-slot=\"checkbox\" {...props} />\n}\n\nfunction CheckboxContext({ ...props }: React.ComponentProps<typeof CheckboxPrimitive.Context>) {\n return <CheckboxPrimitive.Context {...props} />\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction CheckboxControl({ className, ...props }: Omit<React.ComponentProps<typeof CheckboxPrimitive.Control>, \"id\">) {\n return (\n <CheckboxPrimitive.Control\n data-slot=\"checkbox-control\"\n className={cn(checkboxControlClassName, className)}\n {...props}\n />\n )\n}\n\nfunction CheckboxIndicator({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Indicator>) {\n return (\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className={cn(\"grid place-content-center text-current transition-none [&>svg]:size-3.5\", className)}\n {...props}\n />\n )\n}\n\nfunction CheckboxLabel({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Label>) {\n return (\n <CheckboxPrimitive.Label\n data-slot=\"checkbox-label\"\n className={cn(\"text-sm leading-none select-none data-disabled:opacity-50\", className)}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction CheckboxHiddenInput({ ...props }: Omit<React.ComponentProps<typeof CheckboxPrimitive.HiddenInput>, \"id\">) {\n return <CheckboxPrimitive.HiddenInput {...props} />\n}\n\nfunction CheckboxGroup({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Group>) {\n return <CheckboxPrimitive.Group data-slot=\"checkbox-group\" className={cn(\"grid gap-2\", className)} {...props} />\n}\n\nfunction CheckboxGroupProvider({ ...props }: React.ComponentProps<typeof CheckboxPrimitive.GroupProvider>) {\n return <CheckboxPrimitive.GroupProvider {...props} />\n}\n\nexport {\n Checkbox,\n CheckboxContext,\n CheckboxControl,\n CheckboxGroup,\n CheckboxHiddenInput,\n CheckboxIndicator,\n CheckboxLabel,\n CheckboxRoot,\n CheckboxGroupProvider,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "clipboard",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/clipboard.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Clipboard as ClipboardPrimitive } from \"@ark-ui/react\"\nimport { CheckIcon, CopyIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction Clipboard({ className, ...props }: React.ComponentProps<typeof ClipboardPrimitive.Root>) {\n return (\n <ClipboardPrimitive.Root\n data-slot=\"clipboard\"\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction ClipboardContext({ ...props }: React.ComponentProps<typeof ClipboardPrimitive.Context>) {\n return <ClipboardPrimitive.Context {...props} />\n}\n\nfunction ClipboardLabel({ className, ...props }: React.ComponentProps<typeof ClipboardPrimitive.Label>) {\n return (\n <ClipboardPrimitive.Label\n data-slot=\"clipboard-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction ClipboardControl({ className, ...props }: React.ComponentProps<typeof ClipboardPrimitive.Control>) {\n return (\n <ClipboardPrimitive.Control\n data-slot=\"clipboard-control\"\n className={cn(\"flex items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction ClipboardInput({ className, ...props }: React.ComponentProps<typeof ClipboardPrimitive.Input>) {\n return (\n <ClipboardPrimitive.Input\n data-slot=\"clipboard-input\"\n className={cn(\n \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none read-only:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ClipboardTrigger({ className, children, ...props }: React.ComponentProps<typeof ClipboardPrimitive.Trigger>) {\n return (\n <ClipboardPrimitive.Trigger data-slot=\"clipboard-trigger\" className={cn(className)} asChild {...props}>\n <Button variant=\"outline\" size=\"icon\">\n {children ?? (\n <ClipboardIndicator copied={<CheckIcon />}>\n <CopyIcon />\n </ClipboardIndicator>\n )}\n </Button>\n </ClipboardPrimitive.Trigger>\n )\n}\n\nfunction ClipboardIndicator({ ...props }: React.ComponentProps<typeof ClipboardPrimitive.Indicator>) {\n return <ClipboardPrimitive.Indicator data-slot=\"clipboard-indicator\" {...props} />\n}\n\nfunction ClipboardValueText({ className, ...props }: React.ComponentProps<typeof ClipboardPrimitive.ValueText>) {\n return (\n <ClipboardPrimitive.ValueText\n data-slot=\"clipboard-value-text\"\n className={cn(\"font-mono text-sm\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Clipboard,\n ClipboardContext,\n ClipboardControl,\n ClipboardIndicator,\n ClipboardInput,\n ClipboardLabel,\n ClipboardTrigger,\n ClipboardValueText,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "collapsible",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/collapsible.tsx",
12
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Collapsible as CollapsiblePrimitive } from \"@ark-ui/react\"\n\nfunction Collapsible({\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n return (\n <CollapsiblePrimitive.Root data-slot=\"collapsible\" lazyMount={lazyMount} unmountOnExit={unmountOnExit} {...props} />\n )\n}\n\nfunction CollapsibleContext({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Context>) {\n return <CollapsiblePrimitive.Context {...props} />\n}\n\nfunction CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) {\n return <CollapsiblePrimitive.Trigger data-slot=\"collapsible-trigger\" {...props} />\n}\n\nfunction CollapsibleIndicator({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Indicator>) {\n return <CollapsiblePrimitive.Indicator data-slot=\"collapsible-indicator\" {...props} />\n}\n\nfunction CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Content>) {\n return <CollapsiblePrimitive.Content data-slot=\"collapsible-content\" {...props} />\n}\n\nexport { Collapsible, CollapsibleContent, CollapsibleContext, CollapsibleIndicator, CollapsibleTrigger }\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "color-picker",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/color-picker.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { ColorPicker as ColorPickerPrimitive, Portal as PortalPrimitive, parseColor } from \"@ark-ui/react\"\nimport { PipetteIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction ColorPicker({\n className,\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.Root>) {\n return (\n <ColorPickerPrimitive.Root\n data-slot=\"color-picker\"\n positioning={{ placement: \"bottom-start\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerContext({ ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Context>) {\n return <ColorPickerPrimitive.Context {...props} />\n}\n\nfunction ColorPickerLabel({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Label>) {\n return (\n <ColorPickerPrimitive.Label\n data-slot=\"color-picker-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerControl({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Control>) {\n return (\n <ColorPickerPrimitive.Control\n data-slot=\"color-picker-control\"\n className={cn(\"flex items-center gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerChannelInput({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelInput>) {\n return (\n <ColorPickerPrimitive.ChannelInput\n data-slot=\"color-picker-channel-input\"\n className={cn(\n \"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive dark:bg-input/30\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ColorPickerTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.Trigger>) {\n return (\n <ColorPickerPrimitive.Trigger\n data-slot=\"color-picker-trigger\"\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg border border-input bg-transparent p-1 outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 data-disabled:opacity-50\",\n className\n )}\n {...props}\n >\n {children ?? (\n <ColorPickerTransparencyGrid className=\"rounded-md\">\n <ColorPickerValueSwatch />\n </ColorPickerTransparencyGrid>\n )}\n </ColorPickerPrimitive.Trigger>\n )\n}\n\nfunction ColorPickerValueSwatch({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ValueSwatch>) {\n return (\n <ColorPickerPrimitive.ValueSwatch\n data-slot=\"color-picker-value-swatch\"\n className={cn(\"size-full rounded-[inherit]\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerValueText({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.ValueText>) {\n return (\n <ColorPickerPrimitive.ValueText\n data-slot=\"color-picker-value-text\"\n className={cn(\"font-mono text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerTransparencyGrid({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.TransparencyGrid>) {\n return (\n <ColorPickerPrimitive.TransparencyGrid\n data-slot=\"color-picker-transparency-grid\"\n className={cn(\"size-full overflow-hidden\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction ColorPickerPositioner({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Positioner>) {\n return (\n <ColorPickerPrimitive.Positioner\n data-slot=\"color-picker-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerContent({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Content>) {\n return (\n <ColorPickerPortal>\n <ColorPickerPositioner>\n <ColorPickerPrimitive.Content\n data-slot=\"color-picker-content\"\n className={cn(\n \"z-50 flex w-64 flex-col gap-3 rounded-lg bg-popover p-3 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className\n )}\n {...props}\n />\n </ColorPickerPositioner>\n </ColorPickerPortal>\n )\n}\n\nfunction ColorPickerArea({ className, children, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Area>) {\n return (\n <ColorPickerPrimitive.Area\n data-slot=\"color-picker-area\"\n className={cn(\"h-40 w-full overflow-hidden rounded-md\", className)}\n {...props}\n >\n {children ?? (\n <>\n <ColorPickerAreaBackground />\n <ColorPickerAreaThumb />\n </>\n )}\n </ColorPickerPrimitive.Area>\n )\n}\n\nfunction ColorPickerAreaBackground({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.AreaBackground>) {\n return (\n <ColorPickerPrimitive.AreaBackground\n data-slot=\"color-picker-area-background\"\n className={cn(\"size-full\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerAreaThumb({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.AreaThumb>) {\n return (\n <ColorPickerPrimitive.AreaThumb\n data-slot=\"color-picker-area-thumb\"\n className={cn(\n \"size-3 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.3)] outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ColorPickerChannelSlider({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelSlider>) {\n return (\n <ColorPickerPrimitive.ChannelSlider\n data-slot=\"color-picker-channel-slider\"\n className={cn(\"relative h-3 w-full\", className)}\n {...props}\n >\n {children ?? (\n <>\n <ColorPickerChannelSliderTrack />\n <ColorPickerChannelSliderThumb />\n </>\n )}\n </ColorPickerPrimitive.ChannelSlider>\n )\n}\n\nfunction ColorPickerChannelSliderTrack({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelSliderTrack>) {\n return (\n <ColorPickerPrimitive.ChannelSliderTrack\n data-slot=\"color-picker-channel-slider-track\"\n className={cn(\"size-full rounded-full\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerChannelSliderThumb({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelSliderThumb>) {\n return (\n <ColorPickerPrimitive.ChannelSliderThumb\n data-slot=\"color-picker-channel-slider-thumb\"\n className={cn(\n \"size-3 -translate-1/2 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.3)] outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ColorPickerChannelSliderLabel({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelSliderLabel>) {\n return (\n <ColorPickerPrimitive.ChannelSliderLabel\n data-slot=\"color-picker-channel-slider-label\"\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerChannelSliderValueText({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.ChannelSliderValueText>) {\n return (\n <ColorPickerPrimitive.ChannelSliderValueText\n data-slot=\"color-picker-channel-slider-value-text\"\n className={cn(\"text-xs tabular-nums\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerEyeDropperTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.EyeDropperTrigger>) {\n return (\n <ColorPickerPrimitive.EyeDropperTrigger\n data-slot=\"color-picker-eye-dropper-trigger\"\n className={cn(className)}\n asChild\n {...props}\n >\n <Button variant=\"outline\" size=\"icon-sm\">\n {children ?? <PipetteIcon />}\n <span className=\"sr-only\">Pick color</span>\n </Button>\n </ColorPickerPrimitive.EyeDropperTrigger>\n )\n}\n\nfunction ColorPickerFormatTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.FormatTrigger>) {\n return (\n <ColorPickerPrimitive.FormatTrigger\n data-slot=\"color-picker-format-trigger\"\n className={cn(className)}\n asChild\n {...props}\n >\n <Button variant=\"outline\" size=\"sm\">\n {children}\n </Button>\n </ColorPickerPrimitive.FormatTrigger>\n )\n}\n\nfunction ColorPickerFormatSelect({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.FormatSelect>) {\n return (\n <ColorPickerPrimitive.FormatSelect\n data-slot=\"color-picker-format-select\"\n className={cn(\n \"h-7 rounded-md border border-input bg-transparent px-1.5 text-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ColorPickerView({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.View>) {\n return (\n <ColorPickerPrimitive.View\n data-slot=\"color-picker-view\"\n className={cn(\"flex items-center gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerSwatchGroup({\n className,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.SwatchGroup>) {\n return (\n <ColorPickerPrimitive.SwatchGroup\n data-slot=\"color-picker-swatch-group\"\n className={cn(\"flex flex-wrap gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerSwatchTrigger({\n className,\n children,\n value,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.SwatchTrigger>) {\n return (\n <ColorPickerPrimitive.SwatchTrigger\n data-slot=\"color-picker-swatch-trigger\"\n value={value}\n className={cn(\"size-6 rounded-md outline-none focus-visible:ring-3 focus-visible:ring-ring/50\", className)}\n {...props}\n >\n {children ?? (\n <ColorPickerTransparencyGrid className=\"rounded-md\">\n <ColorPickerSwatch value={value}>\n <ColorPickerSwatchIndicator />\n </ColorPickerSwatch>\n </ColorPickerTransparencyGrid>\n )}\n </ColorPickerPrimitive.SwatchTrigger>\n )\n}\n\nfunction ColorPickerSwatch({ className, ...props }: React.ComponentProps<typeof ColorPickerPrimitive.Swatch>) {\n return (\n <ColorPickerPrimitive.Swatch\n data-slot=\"color-picker-swatch\"\n className={cn(\"flex size-full items-center justify-center rounded-[inherit]\", className)}\n {...props}\n />\n )\n}\n\nfunction ColorPickerSwatchIndicator({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ColorPickerPrimitive.SwatchIndicator>) {\n return (\n <ColorPickerPrimitive.SwatchIndicator\n data-slot=\"color-picker-swatch-indicator\"\n className={cn(\"size-2 rounded-full bg-white shadow-sm\", className)}\n {...props}\n >\n {children}\n </ColorPickerPrimitive.SwatchIndicator>\n )\n}\n\nfunction ColorPickerHiddenInput({ ...props }: React.ComponentProps<typeof ColorPickerPrimitive.HiddenInput>) {\n return <ColorPickerPrimitive.HiddenInput {...props} />\n}\n\nexport {\n ColorPicker,\n ColorPickerArea,\n ColorPickerAreaBackground,\n ColorPickerAreaThumb,\n ColorPickerChannelInput,\n ColorPickerChannelSlider,\n ColorPickerChannelSliderLabel,\n ColorPickerChannelSliderThumb,\n ColorPickerChannelSliderTrack,\n ColorPickerChannelSliderValueText,\n ColorPickerContent,\n ColorPickerContext,\n ColorPickerControl,\n ColorPickerEyeDropperTrigger,\n ColorPickerFormatSelect,\n ColorPickerFormatTrigger,\n ColorPickerHiddenInput,\n ColorPickerLabel,\n ColorPickerPortal,\n ColorPickerPositioner,\n ColorPickerSwatch,\n ColorPickerSwatchGroup,\n ColorPickerSwatchIndicator,\n ColorPickerSwatchTrigger,\n ColorPickerTransparencyGrid,\n ColorPickerTrigger,\n ColorPickerValueSwatch,\n ColorPickerValueText,\n ColorPickerView,\n parseColor,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "combobox",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "input-group",
12
+ "utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/combobox.tsx",
17
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n Combobox as ComboboxPrimitive,\n Portal as PortalPrimitive,\n createListCollection,\n useComboboxContext,\n useFilter,\n useListCollection,\n type CollectionItem,\n type ListCollection,\n} from \"@ark-ui/react\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { InputGroup, InputGroupAddon, InputGroupTrigger, InputGroupInput } from \"@/components/ui/input-group\"\nimport { ChevronDownIcon, XIcon, CheckIcon } from \"lucide-react\"\n\nfunction Combobox<T extends CollectionItem>({\n positioning,\n anchor,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof ComboboxPrimitive.Root<T>> & {\n /** Anchor the popup to another element, e.g. a `ComboboxChips` container. */\n anchor?: React.RefObject<HTMLElement | null>\n}) {\n return (\n <ComboboxPrimitive.Root\n data-slot=\"combobox\"\n positioning={{\n placement: \"bottom-start\",\n gutter: 6,\n sameWidth: true,\n ...(anchor ? { getAnchorElement: () => anchor.current } : {}),\n ...positioning,\n }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction ComboboxContext({ ...props }: React.ComponentProps<typeof ComboboxPrimitive.Context>) {\n return <ComboboxPrimitive.Context {...props} />\n}\n\nfunction ComboboxRootLabel({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.Label>) {\n return (\n <ComboboxPrimitive.Label\n data-slot=\"combobox-root-label\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction ComboboxControl({ ...props }: React.ComponentProps<typeof ComboboxPrimitive.Control>) {\n return <ComboboxPrimitive.Control data-slot=\"combobox-control\" {...props} />\n}\n\n/**\n * Ark has no value-text part for Combobox. Renders the selected items as\n * text, or `placeholder` when nothing is selected.\n */\nfunction ComboboxValue({\n placeholder,\n children,\n ...props\n}: React.ComponentProps<\"span\"> & { placeholder?: React.ReactNode }) {\n const combobox = useComboboxContext()\n return (\n <span data-slot=\"combobox-value\" data-placeholder-shown={combobox.hasSelectedItems ? undefined : \"\"} {...props}>\n {children ?? (combobox.hasSelectedItems ? combobox.valueAsString : placeholder)}\n </span>\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction ComboboxTrigger({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof ComboboxPrimitive.Trigger>, \"id\">) {\n return (\n <ComboboxPrimitive.Trigger\n data-slot=\"combobox-trigger\"\n className={cn(\"[&_svg:not([class*='size-'])]:size-4\", className)}\n {...props}\n >\n {children}\n <ChevronDownIcon className=\"pointer-events-none size-4 text-muted-foreground\" />\n </ComboboxPrimitive.Trigger>\n )\n}\n\nfunction ComboboxClear({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.ClearTrigger>) {\n const combobox = useComboboxContext()\n // Match Base UI: unmount when there is nothing to clear, so the trigger\n // button (hidden via `has-data-[slot=combobox-clear]`) shows again.\n if (!combobox.hasSelectedItems && !combobox.inputValue) return null\n return (\n <ComboboxPrimitive.ClearTrigger data-slot=\"combobox-clear\" className={cn(className)} asChild {...props}>\n <InputGroupTrigger variant=\"ghost\" size=\"icon-xs\">\n <XIcon className=\"pointer-events-none\" />\n </InputGroupTrigger>\n </ComboboxPrimitive.ClearTrigger>\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction ComboboxInput({\n className,\n children,\n disabled = false,\n showTrigger = true,\n showClear = false,\n ...props\n}: Omit<React.ComponentProps<typeof ComboboxPrimitive.Input>, \"id\"> & {\n showTrigger?: boolean\n showClear?: boolean\n}) {\n return (\n <ComboboxPrimitive.Control asChild>\n <InputGroup className={cn(\"w-auto\", className)}>\n <ComboboxPrimitive.Input asChild disabled={disabled} {...props}>\n <InputGroupInput />\n </ComboboxPrimitive.Input>\n <InputGroupAddon align=\"inline-end\">\n {showTrigger && (\n <InputGroupTrigger\n size=\"icon-xs\"\n variant=\"ghost\"\n asChild\n data-slot=\"input-group-trigger\"\n className=\"group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent\"\n disabled={disabled}\n >\n <ComboboxTrigger />\n </InputGroupTrigger>\n )}\n {showClear && <ComboboxClear disabled={disabled} />}\n </InputGroupAddon>\n {children}\n </InputGroup>\n </ComboboxPrimitive.Control>\n )\n}\n\nfunction ComboboxPositioner({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.Positioner>) {\n return (\n <ComboboxPrimitive.Positioner\n data-slot=\"combobox-positioner\"\n className={cn(\"isolate [--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction ComboboxContent({ className, ...props }: Omit<React.ComponentProps<typeof ComboboxPrimitive.Content>, \"id\">) {\n return (\n <PortalPrimitive>\n <ComboboxPositioner>\n <ComboboxPrimitive.Content\n data-slot=\"combobox-content\"\n className={cn(\n \"group/combobox-content relative z-50 max-h-(--available-height) w-(--reference-width) max-w-(--available-width) origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className\n )}\n {...props}\n />\n </ComboboxPositioner>\n </PortalPrimitive>\n )\n}\n\nfunction ComboboxList({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.List>) {\n return (\n <ComboboxPrimitive.List\n data-slot=\"combobox-list\"\n className={cn(\n \"no-scrollbar max-h-[min(calc(--spacing(72)-(--spacing(9))),calc(var(--available-height)-(--spacing(9))))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ComboboxItem({\n className,\n children,\n item,\n value,\n ...props\n}: Omit<React.ComponentProps<typeof ComboboxPrimitive.Item>, \"item\"> & {\n /** The collection item. Falls back to `value` for string collections. */\n item?: CollectionItem\n value?: string\n}) {\n return (\n <ComboboxPrimitive.Item\n data-slot=\"combobox-item\"\n item={item ?? value}\n className={cn(\n \"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground 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 {...props}\n >\n {children}\n <ComboboxItemIndicator>\n <CheckIcon className=\"pointer-events-none\" />\n </ComboboxItemIndicator>\n </ComboboxPrimitive.Item>\n )\n}\n\nfunction ComboboxItemText({ ...props }: React.ComponentProps<typeof ComboboxPrimitive.ItemText>) {\n return <ComboboxPrimitive.ItemText data-slot=\"combobox-item-text\" {...props} />\n}\n\nfunction ComboboxItemIndicator({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.ItemIndicator>) {\n return (\n <ComboboxPrimitive.ItemIndicator\n data-slot=\"combobox-item-indicator\"\n className={cn(\"pointer-events-none absolute right-2 flex size-4 items-center justify-center\", className)}\n {...props}\n />\n )\n}\n\nfunction ComboboxGroup({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.ItemGroup>) {\n return <ComboboxPrimitive.ItemGroup data-slot=\"combobox-group\" className={cn(className)} {...props} />\n}\n\nfunction ComboboxLabel({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.ItemGroupLabel>) {\n return (\n <ComboboxPrimitive.ItemGroupLabel\n data-slot=\"combobox-label\"\n className={cn(\"px-2 py-1.5 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/**\n * Renders every item in the root's collection through a render function.\n * Ark has no Collection part; this reads the collection from context.\n */\nfunction ComboboxCollection<T extends CollectionItem = CollectionItem>({\n children,\n}: {\n children: (item: T, index: number) => React.ReactNode\n}) {\n const combobox = useComboboxContext()\n const items = combobox.collection.items as T[]\n return (\n <React.Fragment>\n {items.map((item, index) => (\n <React.Fragment key={combobox.collection.getItemValue(item) ?? index}>{children(item, index)}</React.Fragment>\n ))}\n </React.Fragment>\n )\n}\n\nfunction ComboboxEmpty({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.Empty>) {\n return (\n <ComboboxPrimitive.Empty\n data-slot=\"combobox-empty\"\n className={cn(\"flex w-full justify-center py-2 text-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction ComboboxSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n role=\"separator\"\n aria-orientation=\"horizontal\"\n data-slot=\"combobox-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\n/**\n * Chips for `multiple` comboboxes. Ark has no chip parts; these read the\n * selected values from context and remove them via `clearValue`.\n */\nfunction ComboboxChips({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.Control>) {\n return (\n <ComboboxPrimitive.Control\n data-slot=\"combobox-chips\"\n className={cn(\n \"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ComboboxChip({\n className,\n children,\n value,\n showRemove = true,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The selected value this chip represents. */\n value: string\n showRemove?: boolean\n}) {\n const combobox = useComboboxContext()\n return (\n <div\n data-slot=\"combobox-chip\"\n data-value={value}\n className={cn(\n \"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0\",\n className\n )}\n {...props}\n >\n {children}\n {showRemove && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"combobox-chip-remove\"\n aria-label={`Remove ${value}`}\n disabled={combobox.disabled}\n className=\"-ml-1 opacity-50 hover:opacity-100\"\n onClick={() => combobox.clearValue(value)}\n >\n <XIcon className=\"pointer-events-none\" />\n </Button>\n )}\n </div>\n )\n}\n\nfunction ComboboxChipsInput({ className, ...props }: React.ComponentProps<typeof ComboboxPrimitive.Input>) {\n return (\n <ComboboxPrimitive.Input\n data-slot=\"combobox-chip-input\"\n className={cn(\"min-w-16 flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nfunction useComboboxAnchor() {\n return React.useRef<HTMLDivElement | null>(null)\n}\n\n/** Render-prop access to one item's state (`{ selected, highlighted, disabled, ... }`). */\nfunction ComboboxItemContext({ ...props }: React.ComponentProps<typeof ComboboxPrimitive.ItemContext>) {\n return <ComboboxPrimitive.ItemContext {...props} />\n}\n\nexport {\n Combobox,\n ComboboxChip,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxClear,\n ComboboxCollection,\n ComboboxContent,\n ComboboxContext,\n ComboboxControl,\n ComboboxEmpty,\n ComboboxGroup,\n ComboboxInput,\n ComboboxItem,\n ComboboxItemIndicator,\n ComboboxItemText,\n ComboboxLabel,\n ComboboxList,\n ComboboxPositioner,\n ComboboxRootLabel,\n ComboboxSeparator,\n ComboboxTrigger,\n ComboboxValue,\n createListCollection,\n useComboboxAnchor,\n useFilter,\n useListCollection,\n type CollectionItem,\n type ListCollection,\n ComboboxItemContext,\n}\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "command",
3
+ "type": "ui",
4
+ "description": "- **Feature parity** with the shadcn original: same exported names, same props surface where Ark supports it, same visual result, same variants. - **Ark's tree wins** where the structure differs (e.g.",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "dialog",
11
+ "input-group",
12
+ "utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/command.tsx",
17
+ "content": "import * as React from \"react\"\nimport {\n Listbox as CommandPrimitive,\n createListCollection,\n useFilter,\n useListCollection,\n type CollectionItem,\n type ListCollection,\n} from \"@ark-ui/react\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { InputGroup, InputGroupAddon } from \"@/components/ui/input-group\"\nimport { SearchIcon, CheckIcon } from \"lucide-react\"\n\n/**\n * Backed by Ark UI's Listbox. Ark has no built-in fuzzy filter: pass a\n * `collection` and filter it yourself (see `useListCollection` and\n * `useFilter`, re-exported below), wiring `CommandInput`'s `onValueChange`\n * to the collection's `filter`.\n */\n\ntype SelectHandler = (value: string) => void\n\nconst CommandSelectContext = React.createContext<{\n register: (value: string, handler: SelectHandler) => () => void\n} | null>(null)\n\nfunction Command<T extends CollectionItem>({\n className,\n selectionMode = \"single\",\n onSelect,\n defaultHighlightedValue,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Root<T>>) {\n // Like cmdk, highlight the first item on mount so Enter works immediately.\n const initialHighlight = defaultHighlightedValue ?? props.collection.firstValue\n const handlers = React.useRef(new Map<string, SelectHandler>())\n const register = React.useCallback((value: string, handler: SelectHandler) => {\n handlers.current.set(value, handler)\n return () => {\n handlers.current.delete(value)\n }\n }, [])\n const ctx = React.useMemo(() => ({ register }), [register])\n return (\n <CommandSelectContext.Provider value={ctx}>\n <CommandPrimitive.Root\n data-slot=\"command\"\n selectionMode={selectionMode}\n defaultHighlightedValue={initialHighlight}\n onSelect={(details) => {\n handlers.current.get(details.value)?.(details.value)\n onSelect?.(details)\n }}\n className={cn(\n \"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground\",\n className\n )}\n {...props}\n />\n </CommandSelectContext.Provider>\n )\n}\n\nfunction CommandContext({ ...props }: React.ComponentProps<typeof CommandPrimitive.Context>) {\n return <CommandPrimitive.Context {...props} />\n}\n\nfunction CommandDialog({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = false,\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n title?: string\n description?: string\n className?: string\n showCloseButton?: boolean\n}) {\n return (\n <Dialog {...props}>\n <DialogContent className={cn(\"overflow-hidden rounded-xl! p-0\", className)} showCloseButton={showCloseButton}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n {children}\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction CommandInput({\n className,\n autoHighlight = true,\n onValueChange,\n onChange,\n onFocus,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input> & {\n /** Called with the input text on every change. Wire it to your collection's `filter`. */\n onValueChange?: (value: string) => void\n}) {\n const settled = React.useRef(false)\n return (\n <div data-slot=\"command-input-wrapper\" className=\"p-1 pb-0\">\n <InputGroup className=\"h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!\">\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n autoHighlight={autoHighlight}\n onFocus={(event) => {\n onFocus?.(event)\n // When a Dialog focuses this input in the same tick it mounts, the\n // listbox machine has not entered its ready state yet and drops the\n // focus event, leaving the highlighted item invisible. Re-issue the\n // focus once, on the next tick, so the listbox registers it.\n if (settled.current) return\n settled.current = true\n const el = event.currentTarget\n setTimeout(() => {\n if (document.activeElement !== el) return\n el.blur()\n el.focus({ preventScroll: true })\n }, 0)\n }}\n className={cn(\"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\", className)}\n onChange={(event) => {\n onChange?.(event)\n onValueChange?.(event.currentTarget.value)\n }}\n {...props}\n />\n <InputGroupAddon>\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n </InputGroupAddon>\n </InputGroup>\n </div>\n )\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Content>) {\n return (\n <CommandPrimitive.Content\n data-slot=\"command-list\"\n className={cn(\"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandEmpty({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n data-slot=\"command-empty\"\n className={cn(\"py-6 text-center text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandGroup({\n className,\n heading,\n children,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.ItemGroup> & {\n heading?: React.ReactNode\n}) {\n return (\n <CommandPrimitive.ItemGroup\n data-slot=\"command-group\"\n className={cn(\"overflow-hidden p-1 text-foreground\", className)}\n {...props}\n >\n {heading != null && <CommandGroupLabel>{heading}</CommandGroupLabel>}\n {children}\n </CommandPrimitive.ItemGroup>\n )\n}\n\nfunction CommandGroupLabel({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.ItemGroupLabel>) {\n return (\n <CommandPrimitive.ItemGroupLabel\n data-slot=\"command-group-label\"\n className={cn(\"px-2 py-1.5 text-xs font-medium text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n role=\"separator\"\n aria-orientation=\"horizontal\"\n data-slot=\"command-separator\"\n className={cn(\"-mx-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandItem({\n className,\n children,\n item,\n value,\n onSelect,\n ...props\n}: Omit<React.ComponentProps<typeof CommandPrimitive.Item>, \"item\" | \"onSelect\"> & {\n /** The collection item. Falls back to `value` for string collections. */\n item?: CollectionItem\n value?: string\n /** Called with the item's value when it is selected by click or Enter. */\n onSelect?: SelectHandler\n}) {\n const ctx = React.useContext(CommandSelectContext)\n const resolvedItem = item ?? value\n const itemValue =\n typeof resolvedItem === \"string\" ? resolvedItem : ((resolvedItem as { value?: string } | undefined)?.value ?? value)\n React.useEffect(() => {\n if (!ctx || !onSelect || itemValue == null) return\n return ctx.register(itemValue, onSelect)\n }, [ctx, onSelect, itemValue])\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n item={resolvedItem}\n className={cn(\n \"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-highlighted:bg-muted data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-highlighted:*:[svg]:text-foreground\",\n className\n )}\n {...props}\n >\n {children}\n <CommandItemIndicator>\n <CheckIcon />\n </CommandItemIndicator>\n </CommandPrimitive.Item>\n )\n}\n\nfunction CommandItemText({ ...props }: React.ComponentProps<typeof CommandPrimitive.ItemText>) {\n return <CommandPrimitive.ItemText data-slot=\"command-item-text\" {...props} />\n}\n\nfunction CommandItemIndicator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.ItemIndicator>) {\n return (\n <CommandPrimitive.ItemIndicator\n data-slot=\"command-item-indicator\"\n className={cn(\"ml-auto group-has-data-[slot=command-shortcut]/command-item:hidden\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\n \"ml-auto text-xs tracking-widest text-muted-foreground group-data-highlighted/command-item:text-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Command,\n CommandContext,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandGroupLabel,\n CommandInput,\n CommandItem,\n CommandItemIndicator,\n CommandItemText,\n CommandList,\n CommandSeparator,\n CommandShortcut,\n createListCollection,\n useFilter,\n useListCollection,\n type CollectionItem,\n type ListCollection,\n}\n"
18
+ }
19
+ ],
20
+ "docs": "## Porting rules\n- **Feature parity** with the shadcn original: same exported names, same props surface\n where Ark supports it, same visual result, same variants.\n- **Ark's tree wins** where the structure differs (e.g. `Positioner` wrappers,\n `Control`/`HiddenInput` splits, `ItemGroup`/`ItemGroupLabel`, `Backdrop` vs `Overlay`).\n Export the extra Ark parts as their own composable pieces; do not fold them away.\n- **Stay compositional.** Every part is its own exported component. No collapsing\n parts into a single \"smart\" component, no dropping sub-parts to simplify.\n- **Styling:** `cn` from `@/lib/utils` and `cva` from `class-variance-authority`.\n Style state via Ark's data attributes (`data-state`, `data-open`, `data-disabled`,\n `data-highlighted`, `data-placement`, `data-orientation`, ...). Keep `data-slot`\n attributes on every part, as shadcn does.\n- `command.tsx` runs on Ark's Listbox and `drawer.tsx` on Ark's Drawer; `cmdk`, `vaul`,\n `radix-ui`, `@base-ui/react`, and every shadcn package (`shadcn` CLI, `@shadcn/react`,\n `cn`) are removed. Do not reintroduce them. `cn` is clsx + tailwind-merge in\n `src/lib/utils.ts`. Base variants, keyframes, and utilities live in `src/index.css`\n (keyframes at the end of the `@theme inline` block); there is no `components.json`.\n- `questionnaire.tsx` and `message-scroller.tsx` were removed because they depended on\n the `@shadcn/react` runtime. Rebuild from scratch if they are wanted.\n- Components with no Ark equivalent (calendar, carousel, chart, resizable, input-otp,\n sonner/toast) keep their current libraries. Separator, Label, AspectRatio, Direction\n are plain React."
21
+ }