@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,884 @@
1
+ import * as React from "react"
2
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
3
+ import { draggable, dropTargetForElements, monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
4
+ import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"
5
+ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"
6
+ import { attachClosestEdge, extractClosestEdge, type Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"
7
+ import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"
8
+ import { ark } from "@ark-ui/react"
9
+ import { GripVerticalIcon } from "lucide-react"
10
+ import { Badge } from "@/components/ui/badge"
11
+ import { Button } from "@/components/ui/button"
12
+ import { cloneDragPreview } from "@/lib/drag-preview"
13
+ import { cn } from "@/lib/utils"
14
+ import { LiveRegion, useLiveRegion } from "@/components/ui/live-region"
15
+
16
+ /**
17
+ * Kanban — a compositional, data-agnostic board on Pragmatic drag and drop.
18
+ * The consumer owns columns and cards and re-renders them from its own state;
19
+ * the primitive wires dragging and reports intents through `onCardMove` and
20
+ * `onColumnMove`. `moveCard`/`moveColumn` are optional helpers for the common
21
+ * `{ id, cards: { id }[] }[]` shape.
22
+ */
23
+
24
+ type KanbanCardMoveDetails = {
25
+ cardId: string
26
+ fromColumnId: string
27
+ toColumnId: string
28
+ fromIndex: number
29
+ toIndex: number
30
+ }
31
+
32
+ type KanbanColumnMoveDetails = {
33
+ columnId: string
34
+ fromIndex: number
35
+ toIndex: number
36
+ }
37
+
38
+ type DragData =
39
+ | { instanceId: symbol; type: "card"; cardId: string; columnId: string }
40
+ | { instanceId: symbol; type: "column"; columnId: string }
41
+
42
+ const CARD_TARGET = "kanban-card-target"
43
+ const COLUMN_TARGET = "kanban-column-target"
44
+
45
+ type KanbanDragging = { type: "card" | "column"; id: string; columnId?: string; height: number }
46
+ /** Where a dragged card would land: before `beforeCardId`, or at the end when null. */
47
+ type KanbanPreview = { columnId: string; beforeCardId: string | null }
48
+
49
+ type KanbanGrabbed = { type: "card" | "column"; id: string }
50
+
51
+ type KanbanContextValue = {
52
+ instanceId: symbol
53
+ dragging: KanbanDragging | null
54
+ preview: KanbanPreview | null
55
+ /** Keyboard "pick up" state; arrows move the grabbed card/column. */
56
+ grabbed: KanbanGrabbed | null
57
+ grab: (item: KanbanGrabbed | null) => void
58
+ moveByKey: (item: KanbanGrabbed, direction: "up" | "down" | "left" | "right") => void
59
+ }
60
+
61
+ const focusLater = (selector: string) =>
62
+ requestAnimationFrame(() => requestAnimationFrame(() => document.querySelector<HTMLElement>(selector)?.focus()))
63
+
64
+ const cssEscape = (value: string) => (typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value)
65
+
66
+ const KanbanContext = React.createContext<KanbanContextValue | null>(null)
67
+
68
+ function useKanban() {
69
+ const ctx = React.useContext(KanbanContext)
70
+ if (!ctx) throw new Error("Kanban parts must be used within <Kanban>")
71
+ return ctx
72
+ }
73
+
74
+ const isOwnData = (instanceId: symbol, data: Record<string | symbol, unknown>): data is DragData =>
75
+ data.instanceId === instanceId && (data.type === "card" || data.type === "column")
76
+
77
+ function cardsOf(columnEl: Element) {
78
+ return Array.from(columnEl.querySelectorAll<HTMLElement>(":scope [data-slot=kanban-card]"))
79
+ }
80
+
81
+ type DropTargetLike = { data: Record<string | symbol, unknown> }
82
+
83
+ /** Resolve a card drag against the current drop targets into a concrete destination. */
84
+ function resolveCardDrop(root: HTMLElement, data: Extract<DragData, { type: "card" }>, targets: DropTargetLike[]) {
85
+ const columnTarget = targets.find((t) => t.data[COLUMN_TARGET] === true)
86
+ if (!columnTarget) return null
87
+ const toColumnId = String(columnTarget.data.columnId)
88
+ const columnEls = Array.from(root.querySelectorAll<HTMLElement>("[data-slot=kanban-column]"))
89
+ const fromColumnEl = columnEls.find((el) => el.dataset.value === data.columnId)
90
+ const toColumnEl = columnEls.find((el) => el.dataset.value === toColumnId)
91
+ if (!fromColumnEl || !toColumnEl) return null
92
+ const fromIndex = cardsOf(fromColumnEl).findIndex((el) => el.dataset.value === data.cardId)
93
+ const toCards = cardsOf(toColumnEl)
94
+ const cardTarget = targets.find((t) => t.data[CARD_TARGET] === true)
95
+ const sameColumn = data.columnId === toColumnId
96
+
97
+ let toIndex: number
98
+ if (cardTarget && String(cardTarget.data.cardId) !== data.cardId) {
99
+ const indexOfTarget = toCards.findIndex((el) => el.dataset.value === String(cardTarget.data.cardId))
100
+ const edge = extractClosestEdge(cardTarget.data)
101
+ toIndex = sameColumn
102
+ ? getReorderDestinationIndex({
103
+ startIndex: fromIndex,
104
+ indexOfTarget,
105
+ closestEdgeOfTarget: edge,
106
+ axis: "vertical",
107
+ })
108
+ : edge === "bottom"
109
+ ? indexOfTarget + 1
110
+ : indexOfTarget
111
+ } else if (cardTarget) {
112
+ // Hovering the dragged card itself: no move.
113
+ toIndex = fromIndex
114
+ } else {
115
+ toIndex = sameColumn ? toCards.length - 1 : toCards.length
116
+ }
117
+ const noop = sameColumn && toIndex === fromIndex
118
+ const remaining = toCards.filter((el) => el.dataset.value !== data.cardId)
119
+ const beforeCardId = remaining[toIndex]?.dataset.value ?? null
120
+ return { toColumnId, toIndex, fromIndex, sameColumn, noop, beforeCardId }
121
+ }
122
+
123
+ function Kanban({
124
+ onCardMove,
125
+ onColumnMove,
126
+ className,
127
+ children,
128
+ ...props
129
+ }: React.ComponentProps<"div"> & {
130
+ onCardMove?: (details: KanbanCardMoveDetails) => void
131
+ onColumnMove?: (details: KanbanColumnMoveDetails) => void
132
+ }) {
133
+ const [instanceId] = React.useState(() => Symbol("kanban"))
134
+ const [dragging, setDragging] = React.useState<KanbanDragging | null>(null)
135
+ const [preview, setPreviewState] = React.useState<KanbanPreview | null>(null)
136
+ const [grabbed, setGrabbed] = React.useState<KanbanGrabbed | null>(null)
137
+ const { message: announcement, announce: setAnnouncement } = useLiveRegion()
138
+ const previewKey = React.useRef<string | null>(null)
139
+ const ref = React.useRef<HTMLDivElement>(null)
140
+ const callbacks = React.useRef({ onCardMove, onColumnMove })
141
+ callbacks.current = { onCardMove, onColumnMove }
142
+
143
+ const grab = React.useCallback(
144
+ (item: KanbanGrabbed | null) => {
145
+ setGrabbed(item)
146
+ setAnnouncement(
147
+ item
148
+ ? `Picked up ${item.type} ${item.id}. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.`
149
+ : "Dropped."
150
+ )
151
+ },
152
+ [setAnnouncement]
153
+ )
154
+
155
+ const moveByKey = React.useCallback(
156
+ (item: KanbanGrabbed, direction: "up" | "down" | "left" | "right") => {
157
+ const root = ref.current
158
+ if (!root) return
159
+ const columnEls = Array.from(root.querySelectorAll<HTMLElement>("[data-slot=kanban-column]"))
160
+ const columnIds = columnEls.map((el) => el.dataset.value ?? "")
161
+ const columnName = (el: HTMLElement) =>
162
+ el.querySelector("[data-slot=kanban-column-title]")?.textContent ?? el.dataset.value
163
+
164
+ if (item.type === "column") {
165
+ const fromIndex = columnIds.indexOf(item.id)
166
+ const toIndex = direction === "left" ? fromIndex - 1 : direction === "right" ? fromIndex + 1 : fromIndex
167
+ if (fromIndex < 0 || toIndex === fromIndex || toIndex < 0 || toIndex >= columnIds.length) return
168
+ callbacks.current.onColumnMove?.({ columnId: item.id, fromIndex, toIndex })
169
+ setAnnouncement(`Moved column to position ${toIndex + 1} of ${columnIds.length}.`)
170
+ focusLater(`[data-slot=kanban-column][data-value="${cssEscape(item.id)}"] [data-slot=kanban-column-handle]`)
171
+ return
172
+ }
173
+
174
+ const fromColumnEl = columnEls.find((el) => cardsOf(el).some((c) => c.dataset.value === item.id))
175
+ if (!fromColumnEl) return
176
+ const fromColumnId = fromColumnEl.dataset.value ?? ""
177
+ const fromCards = cardsOf(fromColumnEl)
178
+ const fromIndex = fromCards.findIndex((c) => c.dataset.value === item.id)
179
+ let toColumnId = fromColumnId
180
+ let toIndex = fromIndex
181
+ if (direction === "up" || direction === "down") {
182
+ toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1
183
+ if (toIndex < 0 || toIndex >= fromCards.length) return
184
+ } else {
185
+ const ci = columnIds.indexOf(fromColumnId)
186
+ const targetEl = columnEls[direction === "left" ? ci - 1 : ci + 1]
187
+ if (!targetEl) return
188
+ toColumnId = targetEl.dataset.value ?? ""
189
+ toIndex = Math.min(fromIndex, cardsOf(targetEl).length)
190
+ }
191
+ callbacks.current.onCardMove?.({ cardId: item.id, fromColumnId, toColumnId, fromIndex, toIndex })
192
+ const targetEl = columnEls[columnIds.indexOf(toColumnId)]
193
+ const count = toColumnId === fromColumnId ? fromCards.length : cardsOf(targetEl).length + 1
194
+ setAnnouncement(`Moved to ${columnName(targetEl)}, position ${toIndex + 1} of ${count}.`)
195
+ focusLater(`[data-slot=kanban-card][data-value="${cssEscape(item.id)}"]`)
196
+ },
197
+ [setAnnouncement]
198
+ )
199
+
200
+ React.useEffect(() => {
201
+ const root = ref.current
202
+ if (!root) return
203
+ const setPreview = (next: KanbanPreview | null) => {
204
+ const key = next ? `${next.columnId}::${next.beforeCardId ?? ""}` : null
205
+ if (key === previewKey.current) return
206
+ previewKey.current = key
207
+ setPreviewState(next)
208
+ }
209
+ const updatePreview = (data: DragData, targets: DropTargetLike[]) => {
210
+ if (data.type !== "card") return
211
+ const resolved = resolveCardDrop(root, data, targets)
212
+ setPreview(
213
+ resolved && !resolved.noop ? { columnId: resolved.toColumnId, beforeCardId: resolved.beforeCardId } : null
214
+ )
215
+ }
216
+ return monitorForElements({
217
+ canMonitor: ({ source }) => isOwnData(instanceId, source.data),
218
+ onDragStart: ({ source }) => {
219
+ const data = source.data as DragData
220
+ setDragging({
221
+ type: data.type,
222
+ id: data.type === "card" ? data.cardId : data.columnId,
223
+ columnId: data.type === "card" ? data.columnId : undefined,
224
+ height: source.element.getBoundingClientRect().height,
225
+ })
226
+ },
227
+ onDrag: ({ source, location }) => updatePreview(source.data as DragData, location.current.dropTargets),
228
+ onDropTargetChange: ({ source, location }) =>
229
+ updatePreview(source.data as DragData, location.current.dropTargets),
230
+ onDrop: ({ source, location }) => {
231
+ setDragging(null)
232
+ setPreview(null)
233
+ const data = source.data as DragData
234
+ const targets = location.current.dropTargets
235
+ const columnEls = Array.from(root.querySelectorAll<HTMLElement>("[data-slot=kanban-column]"))
236
+ const columnIds = columnEls.map((el) => el.dataset.value ?? "")
237
+
238
+ if (data.type === "column") {
239
+ const target = targets.find((t) => t.data[COLUMN_TARGET] === true)
240
+ if (!target) return
241
+ const targetId = String(target.data.columnId)
242
+ const fromIndex = columnIds.indexOf(data.columnId)
243
+ const indexOfTarget = columnIds.indexOf(targetId)
244
+ if (fromIndex < 0 || indexOfTarget < 0) return
245
+ const toIndex = getReorderDestinationIndex({
246
+ startIndex: fromIndex,
247
+ indexOfTarget,
248
+ closestEdgeOfTarget: extractClosestEdge(target.data),
249
+ axis: "horizontal",
250
+ })
251
+ if (toIndex === fromIndex) return
252
+ callbacks.current.onColumnMove?.({ columnId: data.columnId, fromIndex, toIndex })
253
+ return
254
+ }
255
+
256
+ const resolved = resolveCardDrop(root, data, targets)
257
+ if (!resolved || resolved.noop) return
258
+ callbacks.current.onCardMove?.({
259
+ cardId: data.cardId,
260
+ fromColumnId: data.columnId,
261
+ toColumnId: resolved.toColumnId,
262
+ fromIndex: resolved.fromIndex,
263
+ toIndex: resolved.toIndex,
264
+ })
265
+ },
266
+ })
267
+ }, [instanceId])
268
+
269
+ const ctx = React.useMemo(
270
+ () => ({ instanceId, dragging, preview, grabbed, grab, moveByKey }),
271
+ [instanceId, dragging, preview, grabbed, grab, moveByKey]
272
+ )
273
+ return (
274
+ <KanbanContext.Provider value={ctx}>
275
+ <div
276
+ ref={ref}
277
+ data-slot="kanban"
278
+ data-dragging={dragging ? dragging.type : undefined}
279
+ data-grabbed={grabbed ? grabbed.type : undefined}
280
+ className={cn("flex min-h-0 flex-1 flex-col", className)}
281
+ {...props}
282
+ >
283
+ {children}
284
+ <LiveRegion data-slot="kanban-live-region" message={announcement} />
285
+ </div>
286
+ </KanbanContext.Provider>
287
+ )
288
+ }
289
+
290
+ /** Keyboard handling shared by cards and column handles. */
291
+ function useKanbanKeyboard(item: KanbanGrabbed) {
292
+ const { grabbed, grab, moveByKey } = useKanban()
293
+ const isGrabbed = grabbed?.type === item.type && grabbed.id === item.id
294
+ const onKeyDown = (event: React.KeyboardEvent) => {
295
+ const key = event.key
296
+ if (key === " " || key === "Enter") {
297
+ event.preventDefault()
298
+ grab(isGrabbed ? null : item)
299
+ return
300
+ }
301
+ if (!isGrabbed) return
302
+ if (key === "Escape") {
303
+ event.preventDefault()
304
+ grab(null)
305
+ return
306
+ }
307
+ const direction = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" }[key] as
308
+ "up" | "down" | "left" | "right" | undefined
309
+ if (!direction) return
310
+ event.preventDefault()
311
+ moveByKey(item, direction)
312
+ }
313
+ return { isGrabbed, onKeyDown, onBlur: () => isGrabbed && grab(null) }
314
+ }
315
+
316
+ /* -------------------------------------------------------------------------- */
317
+ /* Board */
318
+ /* -------------------------------------------------------------------------- */
319
+
320
+ function KanbanBoard({ className, ...props }: React.ComponentProps<"div">) {
321
+ const { instanceId } = useKanban()
322
+ const ref = React.useRef<HTMLDivElement>(null)
323
+ React.useEffect(() => {
324
+ const el = ref.current
325
+ if (!el) return
326
+ return autoScrollForElements({
327
+ element: el,
328
+ canScroll: ({ source }) => isOwnData(instanceId, source.data),
329
+ })
330
+ }, [instanceId])
331
+ return (
332
+ <div
333
+ ref={ref}
334
+ data-slot="kanban-board"
335
+ className={cn("flex min-h-0 flex-1 items-start gap-4 overflow-x-auto p-1 pb-4", className)}
336
+ {...props}
337
+ />
338
+ )
339
+ }
340
+
341
+ /* -------------------------------------------------------------------------- */
342
+ /* Column */
343
+ /* -------------------------------------------------------------------------- */
344
+
345
+ type ColumnContextValue = {
346
+ value: string
347
+ handleRef: React.RefObject<HTMLElement | null>
348
+ headerRef: React.RefObject<HTMLElement | null>
349
+ isOver: boolean
350
+ isDragging: boolean
351
+ }
352
+
353
+ const ColumnContext = React.createContext<ColumnContextValue | null>(null)
354
+
355
+ function useKanbanColumn() {
356
+ const ctx = React.useContext(ColumnContext)
357
+ if (!ctx) throw new Error("Kanban column parts must be used within <KanbanColumn>")
358
+ return ctx
359
+ }
360
+
361
+ function KanbanColumn({
362
+ value,
363
+ draggable: isDraggable = true,
364
+ className,
365
+ children,
366
+ ...props
367
+ }: React.ComponentProps<"section"> & {
368
+ value: string
369
+ /** Allow the column itself to be reordered (drag by its handle or header). */
370
+ draggable?: boolean
371
+ }) {
372
+ const { instanceId, dragging, preview } = useKanban()
373
+ const ref = React.useRef<HTMLElement>(null)
374
+ const handleRef = React.useRef<HTMLElement | null>(null)
375
+ const headerRef = React.useRef<HTMLElement | null>(null)
376
+ const [isOver, setIsOver] = React.useState(false)
377
+ const [isDragging, setIsDragging] = React.useState(false)
378
+ const [closestEdge, setClosestEdge] = React.useState<Edge | null>(null)
379
+
380
+ React.useEffect(() => {
381
+ const el = ref.current
382
+ if (!el) return
383
+ const cleanups = [
384
+ dropTargetForElements({
385
+ element: el,
386
+ canDrop: ({ source }) => isOwnData(instanceId, source.data),
387
+ getIsSticky: () => true,
388
+ getData: ({ source, input, element }) => {
389
+ const base = { [COLUMN_TARGET]: true, columnId: value }
390
+ if ((source.data as DragData).type === "column") {
391
+ return attachClosestEdge(base, { element, input, allowedEdges: ["left", "right"] })
392
+ }
393
+ return base
394
+ },
395
+ onDragEnter: ({ source, self }) => {
396
+ const data = source.data as DragData
397
+ if (data.type === "card") setIsOver(true)
398
+ else if (data.columnId !== value) setClosestEdge(extractClosestEdge(self.data))
399
+ },
400
+ onDrag: ({ source, self }) => {
401
+ const data = source.data as DragData
402
+ if (data.type === "column" && data.columnId !== value) {
403
+ const edge = extractClosestEdge(self.data)
404
+ setClosestEdge((prev) => (prev === edge ? prev : edge))
405
+ }
406
+ },
407
+ onDragLeave: () => {
408
+ setIsOver(false)
409
+ setClosestEdge(null)
410
+ },
411
+ onDrop: () => {
412
+ setIsOver(false)
413
+ setClosestEdge(null)
414
+ },
415
+ }),
416
+ ]
417
+ if (isDraggable) {
418
+ cleanups.push(
419
+ draggable({
420
+ element: el,
421
+ dragHandle: handleRef.current ?? headerRef.current ?? undefined,
422
+ getInitialData: (): DragData => ({ instanceId, type: "column", columnId: value }),
423
+ onGenerateDragPreview: (args) => cloneDragPreview(args),
424
+ onDragStart: () => setIsDragging(true),
425
+ onDrop: () => setIsDragging(false),
426
+ })
427
+ )
428
+ }
429
+ return combine(...cleanups)
430
+ }, [instanceId, value, isDraggable])
431
+
432
+ const ctx = React.useMemo(() => ({ value, handleRef, headerRef, isOver, isDragging }), [value, isOver, isDragging])
433
+ return (
434
+ <ColumnContext.Provider value={ctx}>
435
+ <section
436
+ ref={ref}
437
+ data-slot="kanban-column"
438
+ data-value={value}
439
+ data-over={isOver && dragging?.type === "card" ? "" : undefined}
440
+ data-target={preview?.columnId === value ? "" : undefined}
441
+ data-dragging={isDragging ? "" : undefined}
442
+ data-edge={closestEdge ?? undefined}
443
+ className={cn(
444
+ "group/kanban-column relative flex max-h-full w-72 shrink-0 flex-col rounded-xl border bg-muted/40 transition-colors data-dragging:opacity-40 data-target:border-primary/30",
445
+ className
446
+ )}
447
+ {...props}
448
+ >
449
+ {children}
450
+ {closestEdge && <KanbanDropIndicator edge={closestEdge} />}
451
+ </section>
452
+ </ColumnContext.Provider>
453
+ )
454
+ }
455
+
456
+ function KanbanColumnHeader({ className, ...props }: React.ComponentProps<"header">) {
457
+ const { headerRef } = useKanbanColumn()
458
+ return (
459
+ <header
460
+ ref={headerRef as React.RefObject<HTMLElement>}
461
+ data-slot="kanban-column-header"
462
+ className={cn("flex items-center gap-2 px-3 py-2.5", className)}
463
+ {...props}
464
+ />
465
+ )
466
+ }
467
+
468
+ function KanbanColumnHandle({
469
+ className,
470
+ children,
471
+ onKeyDown,
472
+ onBlur,
473
+ asChild,
474
+ ...props
475
+ }: React.ComponentProps<typeof ark.button>) {
476
+ const { handleRef, value } = useKanbanColumn()
477
+ const keyboard = useKanbanKeyboard({ type: "column", id: value })
478
+ return (
479
+ <ark.button
480
+ asChild={asChild}
481
+ ref={handleRef as React.RefObject<HTMLButtonElement>}
482
+ type="button"
483
+ data-slot="kanban-column-handle"
484
+ data-grabbed={keyboard.isGrabbed ? "" : undefined}
485
+ aria-label="Drag column"
486
+ aria-pressed={keyboard.isGrabbed}
487
+ aria-describedby={undefined}
488
+ onKeyDown={(event) => {
489
+ onKeyDown?.(event)
490
+ if (!event.defaultPrevented) keyboard.onKeyDown(event)
491
+ }}
492
+ onBlur={(event) => {
493
+ onBlur?.(event)
494
+ keyboard.onBlur()
495
+ }}
496
+ className={cn(
497
+ "-ms-1 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",
498
+ className
499
+ )}
500
+ {...props}
501
+ >
502
+ {asChild ? children : (children ?? <GripVerticalIcon />)}
503
+ </ark.button>
504
+ )
505
+ }
506
+
507
+ function KanbanColumnTitle({ className, ...props }: React.ComponentProps<"h3">) {
508
+ return <h3 data-slot="kanban-column-title" className={cn("truncate text-sm font-semibold", className)} {...props} />
509
+ }
510
+
511
+ function KanbanColumnCount({ className, ...props }: React.ComponentProps<typeof Badge>) {
512
+ return (
513
+ <Badge
514
+ data-slot="kanban-column-count"
515
+ variant="secondary"
516
+ className={cn("h-5 min-w-5 justify-center px-1.5 tabular-nums", className)}
517
+ {...props}
518
+ />
519
+ )
520
+ }
521
+
522
+ function KanbanColumnActions({ className, ...props }: React.ComponentProps<"div">) {
523
+ return (
524
+ <div data-slot="kanban-column-actions" className={cn("ms-auto flex items-center gap-1", className)} {...props} />
525
+ )
526
+ }
527
+
528
+ /** The scrolling list of cards. Renders the landing slot at the end when a card would be appended. */
529
+ function KanbanColumnContent({ className, children, ...props }: React.ComponentProps<"div">) {
530
+ const { instanceId, dragging, preview } = useKanban()
531
+ const column = useKanbanColumn()
532
+ const ref = React.useRef<HTMLDivElement>(null)
533
+ React.useEffect(() => {
534
+ const el = ref.current
535
+ if (!el) return
536
+ return autoScrollForElements({
537
+ element: el,
538
+ canScroll: ({ source }) => isOwnData(instanceId, source.data),
539
+ })
540
+ }, [instanceId])
541
+ return (
542
+ <div
543
+ ref={ref}
544
+ data-slot="kanban-column-content"
545
+ className={cn(
546
+ "no-scrollbar flex min-h-16 flex-1 flex-col gap-(--kanban-gap) overflow-y-auto px-2 pb-2 [--kanban-gap:0.5rem]",
547
+ className
548
+ )}
549
+ {...props}
550
+ >
551
+ {children}
552
+ {dragging?.type === "card" && preview?.columnId === column.value && preview.beforeCardId === null && (
553
+ <KanbanDropSlot height={dragging.height} />
554
+ )}
555
+ </div>
556
+ )
557
+ }
558
+
559
+ function KanbanColumnFooter({ className, ...props }: React.ComponentProps<"footer">) {
560
+ return <footer data-slot="kanban-column-footer" className={cn("flex items-center px-2 pb-2", className)} {...props} />
561
+ }
562
+
563
+ /** A ghost-style full-width trigger for the footer, e.g. "Add card". Polymorphic via `asChild`. */
564
+ function KanbanAddTrigger({
565
+ className,
566
+ variant = "ghost",
567
+ size = "sm",
568
+ ...props
569
+ }: React.ComponentProps<typeof Button>) {
570
+ return (
571
+ <Button
572
+ data-slot="kanban-add-trigger"
573
+ variant={variant}
574
+ size={size}
575
+ className={cn("w-full justify-start text-muted-foreground hover:text-foreground", className)}
576
+ {...props}
577
+ />
578
+ )
579
+ }
580
+
581
+ function KanbanEmpty({ className, ...props }: React.ComponentProps<"div">) {
582
+ return (
583
+ <div
584
+ data-slot="kanban-empty"
585
+ className={cn(
586
+ "flex flex-1 items-center justify-center rounded-lg border border-dashed px-3 py-6 text-center text-xs text-muted-foreground",
587
+ className
588
+ )}
589
+ {...props}
590
+ />
591
+ )
592
+ }
593
+
594
+ /* -------------------------------------------------------------------------- */
595
+ /* Card */
596
+ /* -------------------------------------------------------------------------- */
597
+
598
+ type CardContextValue = { handleRef: React.RefObject<HTMLElement | null>; value: string }
599
+ const CardContext = React.createContext<CardContextValue | null>(null)
600
+
601
+ function KanbanCard({
602
+ value,
603
+ draggable: isDraggable = true,
604
+ className,
605
+ children,
606
+ onKeyDown,
607
+ onBlur,
608
+ ...props
609
+ }: React.ComponentProps<"article"> & { value: string; draggable?: boolean }) {
610
+ const { instanceId, dragging, preview } = useKanban()
611
+ const column = useKanbanColumn()
612
+ const keyboard = useKanbanKeyboard({ type: "card", id: value })
613
+ const ref = React.useRef<HTMLElement>(null)
614
+ const handleRef = React.useRef<HTMLElement | null>(null)
615
+ const [isDragging, setIsDragging] = React.useState(false)
616
+ const isSlotBefore = dragging?.type === "card" && preview?.columnId === column.value && preview.beforeCardId === value
617
+ // The dragged card stays as an outlined placeholder until a landing spot exists, then collapses.
618
+ const isCollapsed = isDragging && preview !== null
619
+
620
+ React.useEffect(() => {
621
+ const el = ref.current
622
+ if (!el) return
623
+ const cleanups = [
624
+ dropTargetForElements({
625
+ element: el,
626
+ canDrop: ({ source }) => isOwnData(instanceId, source.data) && (source.data as DragData).type === "card",
627
+ getIsSticky: () => true,
628
+ getData: ({ input, element }) =>
629
+ attachClosestEdge(
630
+ { [CARD_TARGET]: true, cardId: value, columnId: column.value },
631
+ {
632
+ element,
633
+ input,
634
+ allowedEdges: ["top", "bottom"],
635
+ }
636
+ ),
637
+ }),
638
+ ]
639
+ if (isDraggable) {
640
+ cleanups.push(
641
+ draggable({
642
+ element: el,
643
+ dragHandle: handleRef.current ?? undefined,
644
+ getInitialData: (): DragData => ({ instanceId, type: "card", cardId: value, columnId: column.value }),
645
+ onGenerateDragPreview: (args) => cloneDragPreview(args),
646
+ onDragStart: () => setIsDragging(true),
647
+ onDrop: () => setIsDragging(false),
648
+ })
649
+ )
650
+ }
651
+ return combine(...cleanups)
652
+ }, [instanceId, value, column.value, isDraggable])
653
+
654
+ const ctx = React.useMemo(() => ({ handleRef, value }), [value])
655
+ return (
656
+ <CardContext.Provider value={ctx}>
657
+ {isSlotBefore && dragging && <KanbanDropSlot height={dragging.height} />}
658
+ <article
659
+ ref={ref}
660
+ data-slot="kanban-card"
661
+ data-value={value}
662
+ data-dragging={isDragging ? "" : undefined}
663
+ data-collapsed={isCollapsed ? "" : undefined}
664
+ data-grabbed={keyboard.isGrabbed ? "" : undefined}
665
+ tabIndex={isDraggable ? 0 : undefined}
666
+ aria-roledescription={isDraggable ? "draggable card" : undefined}
667
+ onKeyDown={(event) => {
668
+ onKeyDown?.(event)
669
+ if (!event.defaultPrevented && event.target === event.currentTarget) keyboard.onKeyDown(event)
670
+ }}
671
+ onBlur={(event) => {
672
+ onBlur?.(event)
673
+ if (event.target === event.currentTarget) keyboard.onBlur()
674
+ }}
675
+ className={cn(
676
+ "group/kanban-card relative flex cursor-grab flex-col gap-2 rounded-lg border bg-card p-3 text-sm text-card-foreground shadow-xs transition-shadow outline-none hover:shadow-sm focus-visible:ring-3 focus-visible:ring-ring/50 active:cursor-grabbing data-grabbed:border-primary data-grabbed:ring-3 data-grabbed:ring-primary/30",
677
+ "data-dragging:border-dashed data-dragging:border-primary/40 data-dragging:bg-primary/5 data-dragging:shadow-none data-dragging:*:invisible",
678
+ // Collapse (not `display:none`): removing the dragged element from layout cancels a native drag in Chrome.
679
+ "data-collapsed:pointer-events-none data-collapsed:-mb-(--kanban-gap) data-collapsed:h-0 data-collapsed:min-h-0 data-collapsed:overflow-hidden data-collapsed:border-0 data-collapsed:p-0 data-collapsed:opacity-0",
680
+ className
681
+ )}
682
+ {...props}
683
+ >
684
+ {children}
685
+ </article>
686
+ </CardContext.Provider>
687
+ )
688
+ }
689
+
690
+ /** Optional explicit drag handle; without it the whole card is the handle. */
691
+ function KanbanCardHandle({
692
+ className,
693
+ children,
694
+ onKeyDown,
695
+ onBlur,
696
+ asChild,
697
+ ...props
698
+ }: React.ComponentProps<typeof ark.button>) {
699
+ const ctx = React.useContext(CardContext)
700
+ const keyboard = useKanbanKeyboard({ type: "card", id: ctx?.value ?? "" })
701
+ return (
702
+ <ark.button
703
+ asChild={asChild}
704
+ ref={(el: HTMLElement | null) => {
705
+ if (ctx) ctx.handleRef.current = el
706
+ }}
707
+ type="button"
708
+ data-slot="kanban-card-handle"
709
+ data-grabbed={keyboard.isGrabbed ? "" : undefined}
710
+ aria-label="Drag card"
711
+ aria-pressed={keyboard.isGrabbed}
712
+ onKeyDown={(event) => {
713
+ onKeyDown?.(event)
714
+ if (!event.defaultPrevented) {
715
+ keyboard.onKeyDown(event)
716
+ event.stopPropagation()
717
+ }
718
+ }}
719
+ onBlur={(event) => {
720
+ onBlur?.(event)
721
+ keyboard.onBlur()
722
+ }}
723
+ className={cn(
724
+ "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",
725
+ className
726
+ )}
727
+ {...props}
728
+ >
729
+ {asChild ? children : (children ?? <GripVerticalIcon />)}
730
+ </ark.button>
731
+ )
732
+ }
733
+
734
+ function KanbanCardHeader({ className, ...props }: React.ComponentProps<"div">) {
735
+ return (
736
+ <div
737
+ data-slot="kanban-card-header"
738
+ className={cn("flex items-start justify-between gap-2", className)}
739
+ {...props}
740
+ />
741
+ )
742
+ }
743
+
744
+ function KanbanCardTitle({ className, ...props }: React.ComponentProps<"p">) {
745
+ return <p data-slot="kanban-card-title" className={cn("leading-snug font-medium", className)} {...props} />
746
+ }
747
+
748
+ function KanbanCardDescription({ className, ...props }: React.ComponentProps<"p">) {
749
+ return (
750
+ <p
751
+ data-slot="kanban-card-description"
752
+ className={cn("line-clamp-3 text-xs text-muted-foreground", className)}
753
+ {...props}
754
+ />
755
+ )
756
+ }
757
+
758
+ function KanbanCardFooter({ className, ...props }: React.ComponentProps<"div">) {
759
+ return (
760
+ <div
761
+ data-slot="kanban-card-footer"
762
+ className={cn("flex items-center gap-2 text-xs text-muted-foreground", className)}
763
+ {...props}
764
+ />
765
+ )
766
+ }
767
+
768
+ /* -------------------------------------------------------------------------- */
769
+ /* Drop slot + indicator */
770
+ /* -------------------------------------------------------------------------- */
771
+
772
+ /** An outlined placeholder the size of the dragged card, marking where it will land. */
773
+ function KanbanDropSlot({ height, className, style, ...props }: React.ComponentProps<"div"> & { height?: number }) {
774
+ return (
775
+ <div
776
+ data-slot="kanban-drop-slot"
777
+ aria-hidden
778
+ style={{ height, ...style }}
779
+ className={cn(
780
+ "shrink-0 rounded-lg border-2 border-dashed border-primary/40 bg-primary/5",
781
+ height === undefined && "min-h-16",
782
+ className
783
+ )}
784
+ {...props}
785
+ />
786
+ )
787
+ }
788
+
789
+ /** A line on the given edge of its (relative) parent, with a terminal dot. */
790
+ function KanbanDropIndicator({
791
+ edge,
792
+ gap = "0.5rem",
793
+ className,
794
+ ...props
795
+ }: React.ComponentProps<"div"> & { edge: Edge; gap?: string }) {
796
+ const horizontal = edge === "top" || edge === "bottom"
797
+ return (
798
+ <div
799
+ data-slot="kanban-drop-indicator"
800
+ data-edge={edge}
801
+ aria-hidden
802
+ style={{ ["--kanban-gap" as string]: gap }}
803
+ className={cn(
804
+ "pointer-events-none absolute z-10 bg-primary",
805
+ "before:absolute before:size-2 before:rounded-full before:border-2 before:border-primary before:bg-background",
806
+ horizontal
807
+ ? "inset-x-0 h-0.5 before:top-1/2 before:-left-1 before:-translate-y-1/2"
808
+ : "inset-y-0 w-0.5 before:-top-1 before:left-1/2 before:-translate-x-1/2",
809
+ edge === "top" && "top-[calc(-1*var(--kanban-gap)/2-1px)]",
810
+ edge === "bottom" && "bottom-[calc(-1*var(--kanban-gap)/2-1px)]",
811
+ edge === "left" && "left-[calc(-1*var(--kanban-gap)/2-1px)]",
812
+ edge === "right" && "right-[calc(-1*var(--kanban-gap)/2-1px)]",
813
+ className
814
+ )}
815
+ {...props}
816
+ />
817
+ )
818
+ }
819
+
820
+ /* -------------------------------------------------------------------------- */
821
+ /* Helpers */
822
+ /* -------------------------------------------------------------------------- */
823
+
824
+ type KanbanColumnLike<TCard extends { id: string }> = { id: string; cards: TCard[] }
825
+
826
+ /** Apply an `onCardMove` intent to a `{ id, cards }[]` state shape. */
827
+ function moveCard<TCard extends { id: string }, TColumn extends KanbanColumnLike<TCard>>(
828
+ columns: TColumn[],
829
+ { cardId, fromColumnId, toColumnId, toIndex }: KanbanCardMoveDetails
830
+ ): TColumn[] {
831
+ const from = columns.find((c) => c.id === fromColumnId)
832
+ const card = from?.cards.find((c) => c.id === cardId)
833
+ if (!from || !card) return columns
834
+ return columns.map((column) => {
835
+ if (column.id === fromColumnId && column.id === toColumnId) {
836
+ const startIndex = column.cards.indexOf(card)
837
+ return { ...column, cards: reorder({ list: column.cards, startIndex, finishIndex: toIndex }) }
838
+ }
839
+ if (column.id === fromColumnId) return { ...column, cards: column.cards.filter((c) => c !== card) }
840
+ if (column.id === toColumnId) {
841
+ const cards = [...column.cards]
842
+ cards.splice(toIndex, 0, card)
843
+ return { ...column, cards }
844
+ }
845
+ return column
846
+ })
847
+ }
848
+
849
+ /** Apply an `onColumnMove` intent to a column array. */
850
+ function moveColumn<TColumn extends { id: string }>(
851
+ columns: TColumn[],
852
+ { fromIndex, toIndex }: KanbanColumnMoveDetails
853
+ ): TColumn[] {
854
+ return reorder({ list: columns, startIndex: fromIndex, finishIndex: toIndex })
855
+ }
856
+
857
+ export {
858
+ Kanban,
859
+ KanbanAddTrigger,
860
+ KanbanBoard,
861
+ KanbanCard,
862
+ KanbanCardDescription,
863
+ KanbanCardFooter,
864
+ KanbanCardHandle,
865
+ KanbanCardHeader,
866
+ KanbanCardTitle,
867
+ KanbanColumn,
868
+ KanbanColumnActions,
869
+ KanbanColumnContent,
870
+ KanbanColumnCount,
871
+ KanbanColumnFooter,
872
+ KanbanColumnHandle,
873
+ KanbanColumnHeader,
874
+ KanbanColumnTitle,
875
+ KanbanDropIndicator,
876
+ KanbanDropSlot,
877
+ KanbanEmpty,
878
+ moveCard,
879
+ moveColumn,
880
+ useKanban,
881
+ useKanbanColumn,
882
+ type KanbanCardMoveDetails,
883
+ type KanbanColumnMoveDetails,
884
+ }