@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,950 @@
1
+ import * as React from "react"
2
+ import {
3
+ addDays,
4
+ differenceInCalendarDays,
5
+ eachDayOfInterval,
6
+ eachMonthOfInterval,
7
+ eachWeekOfInterval,
8
+ endOfMonth,
9
+ endOfWeek,
10
+ format,
11
+ isSameDay,
12
+ isWeekend,
13
+ startOfDay,
14
+ startOfMonth,
15
+ startOfWeek,
16
+ } from "date-fns"
17
+ import { CalendarIcon, MinusIcon, PlusIcon } from "lucide-react"
18
+ import { Button } from "@/components/ui/button"
19
+ import { cn } from "@/lib/utils"
20
+ import { useControllable } from "@/lib/controllable"
21
+ import { LiveRegion, useLiveRegion } from "@/components/ui/live-region"
22
+
23
+ /**
24
+ * Gantt — a compositional, data-agnostic timeline. The consumer owns rows,
25
+ * bars and dependencies and renders them with the parts; the root owns only
26
+ * the time scale (`start`/`end`, `dayWidth`, header `scale`), the drag/resize
27
+ * interaction and keyboard moves, and reports `onBarChange({ id, start, end })`.
28
+ * Dates snap to whole days.
29
+ *
30
+ * Anatomy:
31
+ * Gantt { start, end, dayWidth, scale, onBarChange }
32
+ * GanttControls > GanttZoomOutTrigger / GanttZoomInTrigger / GanttTodayTrigger
33
+ * GanttViewport single scroll container (x and y)
34
+ * GanttHeader sticky two-tier date header (+ GanttHeaderCorner over the sidebar)
35
+ * GanttBody
36
+ * GanttRow value=… sidebar cell (GanttRowLabel, sticky) + track
37
+ * GanttBar value=… start end progress > GanttBarLabel, GanttBarResizeHandle side=…
38
+ * GanttMilestone value=… date
39
+ * GanttDependencies svg arrows between registered bars
40
+ * GanttToday vertical marker
41
+ * GanttEmpty
42
+ *
43
+ * Keyboard on a bar: Space/Enter picks up, ←/→ move a day, Shift+←/→ change
44
+ * the end, Alt+←/→ change the start, Space/Enter drops, Escape cancels.
45
+ */
46
+
47
+ type GanttScale = "day" | "week" | "month"
48
+ type GanttBarChange = { id: string; start: Date; end: Date }
49
+ type BarRegistration = { id: string; el: HTMLElement | null; start: Date; end: Date; milestone?: boolean }
50
+
51
+ /** Registered bars, outside React state so registering never re-renders the root. */
52
+ function createBarStore() {
53
+ const bars = new Map<string, BarRegistration>()
54
+ const listeners = new Set<() => void>()
55
+ let version = 0
56
+ const notify = () => {
57
+ version++
58
+ listeners.forEach((l) => l())
59
+ }
60
+ return {
61
+ bars,
62
+ subscribe: (l: () => void) => {
63
+ listeners.add(l)
64
+ return () => {
65
+ listeners.delete(l)
66
+ }
67
+ },
68
+ getVersion: () => version,
69
+ register(bar: BarRegistration) {
70
+ bars.set(bar.id, bar)
71
+ notify()
72
+ return () => {
73
+ if (bars.get(bar.id) === bar) bars.delete(bar.id)
74
+ notify()
75
+ }
76
+ },
77
+ }
78
+ }
79
+ type BarStore = ReturnType<typeof createBarStore>
80
+
81
+ type Interaction = {
82
+ id: string
83
+ type: "move" | "resize-start" | "resize-end"
84
+ start: Date
85
+ end: Date
86
+ }
87
+
88
+ type GanttContextValue = {
89
+ start: Date
90
+ end: Date
91
+ days: number
92
+ dayWidth: number
93
+ rowHeight: number
94
+ sidebarWidth: number
95
+ scale: GanttScale
96
+ editable: boolean
97
+ interaction: Interaction | null
98
+ grabbed: string | null
99
+ viewportRef: React.RefObject<HTMLDivElement | null>
100
+ xOf: (date: Date) => number
101
+ dateAt: (x: number) => Date
102
+ registerBar: (bar: BarRegistration) => () => void
103
+ barStore: BarStore
104
+ startInteraction: (bar: BarRegistration, type: Interaction["type"], event: React.PointerEvent) => void
105
+ grab: (id: string | null) => void
106
+ cancelGrab: () => void
107
+ nudge: (bar: BarRegistration, deltaDays: number, part: "both" | "start" | "end") => void
108
+ zoomIn: () => void
109
+ zoomOut: () => void
110
+ scrollToDate: (date: Date, options?: { behavior?: ScrollBehavior }) => void
111
+ announce: (message: string) => void
112
+ }
113
+
114
+ const GanttContext = React.createContext<GanttContextValue | null>(null)
115
+ const RowContext = React.createContext<{ value: string } | null>(null)
116
+
117
+ function useGantt() {
118
+ const ctx = React.useContext(GanttContext)
119
+ if (!ctx) throw new Error("Gantt parts must be used within <Gantt>")
120
+ return ctx
121
+ }
122
+ function useGanttRow() {
123
+ const ctx = React.useContext(RowContext)
124
+ if (!ctx) throw new Error("Row parts must be used within <GanttRow>")
125
+ return ctx
126
+ }
127
+
128
+ const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
129
+
130
+ /* ---------------------------------------------------------------------------
131
+ * Root
132
+ * ------------------------------------------------------------------------- */
133
+
134
+ function Gantt({
135
+ start: startProp,
136
+ end: endProp,
137
+ dayWidth: dayWidthProp,
138
+ defaultDayWidth = 40,
139
+ onDayWidthChange,
140
+ minDayWidth = 8,
141
+ maxDayWidth = 160,
142
+ scale: scaleProp,
143
+ rowHeight = 40,
144
+ sidebarWidth = 240,
145
+ editable = true,
146
+ onBarChange,
147
+ className,
148
+ children,
149
+ ...props
150
+ }: Omit<React.ComponentProps<"div">, "onChange"> & {
151
+ start: Date
152
+ end: Date
153
+ /** Pixels per day; controllable. Zooming changes it. */
154
+ dayWidth?: number
155
+ defaultDayWidth?: number
156
+ onDayWidthChange?: (dayWidth: number) => void
157
+ minDayWidth?: number
158
+ maxDayWidth?: number
159
+ /** Header tiers. Defaults from `dayWidth`: month below 16px, week below 32px, else day. */
160
+ scale?: GanttScale
161
+ rowHeight?: number
162
+ sidebarWidth?: number
163
+ editable?: boolean
164
+ onBarChange?: (change: GanttBarChange) => void
165
+ }) {
166
+ const viewportRef = React.useRef<HTMLDivElement>(null)
167
+ const start = React.useMemo(() => startOfDay(startProp), [startProp])
168
+ const end = React.useMemo(() => startOfDay(endProp), [endProp])
169
+ const days = Math.max(1, differenceInCalendarDays(end, start) + 1)
170
+
171
+ const [dayWidth, setDayWidthState] = useControllable(dayWidthProp, defaultDayWidth, onDayWidthChange)
172
+ const setDayWidth = React.useCallback(
173
+ (next: number) => setDayWidthState(clamp(next, minDayWidth, maxDayWidth)),
174
+ [setDayWidthState, minDayWidth, maxDayWidth]
175
+ )
176
+ const scale: GanttScale = scaleProp ?? (dayWidth < 16 ? "month" : dayWidth < 32 ? "week" : "day")
177
+
178
+ const xOf = React.useCallback(
179
+ (date: Date) => differenceInCalendarDays(startOfDay(date), start) * dayWidth,
180
+ [start, dayWidth]
181
+ )
182
+ const dateAt = React.useCallback((x: number) => addDays(start, Math.round(x / dayWidth)), [start, dayWidth])
183
+
184
+ /* Bar registry for dependency lines */
185
+ const barStore = React.useMemo(() => createBarStore(), [])
186
+ const registerBar = React.useCallback((bar: BarRegistration) => barStore.register(bar), [barStore])
187
+
188
+ /* Interaction */
189
+ const [interaction, setInteraction] = React.useState<Interaction | null>(null)
190
+ const [grabbed, setGrabbed] = React.useState<string | null>(null)
191
+ const { message: announcement, announce } = useLiveRegion()
192
+ const onBarChangeRef = React.useRef(onBarChange)
193
+ React.useEffect(() => {
194
+ onBarChangeRef.current = onBarChange
195
+ }, [onBarChange])
196
+ const grabOrigin = React.useRef<{ id: string; start: Date; end: Date } | null>(null)
197
+
198
+ const startInteraction = React.useCallback(
199
+ (bar: BarRegistration, type: Interaction["type"], event: React.PointerEvent) => {
200
+ if (!editable) return
201
+ event.preventDefault()
202
+ event.stopPropagation()
203
+ const origin = event.clientX
204
+ let current: Interaction = { id: bar.id, type, start: bar.start, end: bar.end }
205
+ setInteraction(current)
206
+ const move = (e: PointerEvent) => {
207
+ const deltaDays = Math.round((e.clientX - origin) / dayWidth)
208
+ const duration = differenceInCalendarDays(bar.end, bar.start)
209
+ if (type === "move") {
210
+ current = { id: bar.id, type, start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }
211
+ } else if (type === "resize-end") {
212
+ current = { id: bar.id, type, start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }
213
+ } else {
214
+ current = { id: bar.id, type, start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }
215
+ }
216
+ setInteraction(current)
217
+ }
218
+ const up = () => {
219
+ window.removeEventListener("pointermove", move)
220
+ window.removeEventListener("pointerup", up)
221
+ window.removeEventListener("pointercancel", up)
222
+ setInteraction(null)
223
+ if (!isSameDay(current.start, bar.start) || !isSameDay(current.end, bar.end)) {
224
+ onBarChangeRef.current?.({ id: bar.id, start: current.start, end: current.end })
225
+ }
226
+ }
227
+ window.addEventListener("pointermove", move)
228
+ window.addEventListener("pointerup", up)
229
+ window.addEventListener("pointercancel", up)
230
+ },
231
+ [editable, dayWidth]
232
+ )
233
+
234
+ const grab = React.useCallback(
235
+ (id: string | null) => {
236
+ if (id) {
237
+ const bar = barStore.bars.get(id)
238
+ grabOrigin.current = bar ? { id, start: bar.start, end: bar.end } : null
239
+ setGrabbed(id)
240
+ announce(
241
+ "Picked up. Arrow keys move by a day, Shift changes the end, Alt the start. Space drops, Escape cancels."
242
+ )
243
+ } else {
244
+ setGrabbed(null)
245
+ grabOrigin.current = null
246
+ announce("Dropped.")
247
+ }
248
+ },
249
+ [barStore, announce]
250
+ )
251
+ const cancelGrab = React.useCallback(() => {
252
+ const origin = grabOrigin.current
253
+ if (origin) onBarChangeRef.current?.(origin)
254
+ setGrabbed(null)
255
+ grabOrigin.current = null
256
+ announce("Cancelled.")
257
+ }, [announce])
258
+
259
+ const nudge = React.useCallback(
260
+ (bar: BarRegistration, deltaDays: number, part: "both" | "start" | "end") => {
261
+ const duration = differenceInCalendarDays(bar.end, bar.start)
262
+ const next =
263
+ part === "both"
264
+ ? { start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }
265
+ : part === "end"
266
+ ? { start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }
267
+ : { start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }
268
+ onBarChangeRef.current?.({ id: bar.id, ...next })
269
+ announce(`${format(next.start, "MMM d")} to ${format(next.end, "MMM d")}.`)
270
+ },
271
+ [announce]
272
+ )
273
+
274
+ const scrollToDate = React.useCallback(
275
+ (date: Date, options?: { behavior?: ScrollBehavior }) => {
276
+ const el = viewportRef.current
277
+ if (!el) return
278
+ const x = xOf(date)
279
+ el.scrollTo({
280
+ left: Math.max(0, x - (el.clientWidth - sidebarWidth) / 2),
281
+ behavior: options?.behavior ?? "smooth",
282
+ })
283
+ },
284
+ [xOf, sidebarWidth]
285
+ )
286
+
287
+ /** Zoom keeping the date at the viewport centre in place. */
288
+ const zoomBy = React.useCallback(
289
+ (factor: number) => {
290
+ const el = viewportRef.current
291
+ const centreX = el ? el.scrollLeft + (el.clientWidth - sidebarWidth) / 2 : 0
292
+ const centreDays = centreX / dayWidth
293
+ const next = clamp(dayWidth * factor, minDayWidth, maxDayWidth)
294
+ setDayWidth(next)
295
+ requestAnimationFrame(() => {
296
+ if (el) el.scrollLeft = Math.max(0, centreDays * next - (el.clientWidth - sidebarWidth) / 2)
297
+ })
298
+ },
299
+ [dayWidth, minDayWidth, maxDayWidth, setDayWidth, sidebarWidth]
300
+ )
301
+ const zoomIn = React.useCallback(() => zoomBy(1.5), [zoomBy])
302
+ const zoomOut = React.useCallback(() => zoomBy(1 / 1.5), [zoomBy])
303
+
304
+ const ctx = React.useMemo<GanttContextValue>(
305
+ () => ({
306
+ start,
307
+ end,
308
+ days,
309
+ dayWidth,
310
+ rowHeight,
311
+ sidebarWidth,
312
+ scale,
313
+ editable,
314
+ interaction,
315
+ grabbed,
316
+ viewportRef,
317
+ xOf,
318
+ dateAt,
319
+ registerBar,
320
+ barStore,
321
+ startInteraction,
322
+ grab,
323
+ cancelGrab,
324
+ nudge,
325
+ zoomIn,
326
+ zoomOut,
327
+ scrollToDate,
328
+ announce,
329
+ }),
330
+ [
331
+ start,
332
+ end,
333
+ days,
334
+ dayWidth,
335
+ rowHeight,
336
+ sidebarWidth,
337
+ scale,
338
+ editable,
339
+ interaction,
340
+ grabbed,
341
+ xOf,
342
+ dateAt,
343
+ registerBar,
344
+ barStore,
345
+ startInteraction,
346
+ grab,
347
+ cancelGrab,
348
+ nudge,
349
+ zoomIn,
350
+ zoomOut,
351
+ scrollToDate,
352
+ announce,
353
+ ]
354
+ )
355
+
356
+ return (
357
+ <GanttContext.Provider value={ctx}>
358
+ <div
359
+ data-slot="gantt"
360
+ data-scale={scale}
361
+ data-editable={editable ? "" : undefined}
362
+ data-interacting={interaction?.type}
363
+ className={cn("relative flex min-h-0 flex-1 flex-col", className)}
364
+ style={{ ["--gantt-sidebar" as string]: `${sidebarWidth}px`, ["--gantt-row" as string]: `${rowHeight}px` }}
365
+ {...props}
366
+ >
367
+ {children}
368
+ <LiveRegion data-slot="gantt-live-region" message={announcement} />
369
+ </div>
370
+ </GanttContext.Provider>
371
+ )
372
+ }
373
+
374
+ /* ---------------------------------------------------------------------------
375
+ * Viewport, header, body
376
+ * ------------------------------------------------------------------------- */
377
+
378
+ function GanttViewport({ className, ...props }: React.ComponentProps<"div">) {
379
+ const { viewportRef } = useGantt()
380
+ return (
381
+ <div
382
+ ref={viewportRef}
383
+ data-slot="gantt-viewport"
384
+ className={cn("relative min-h-0 flex-1 overflow-auto rounded-xl border bg-background text-sm", className)}
385
+ {...props}
386
+ />
387
+ )
388
+ }
389
+
390
+ /** Two-tier sticky header; the tiers depend on the scale. */
391
+ function GanttHeader({ className, ...props }: React.ComponentProps<"div">) {
392
+ const { start, end, days, dayWidth, scale, sidebarWidth } = useGantt()
393
+ const width = days * dayWidth
394
+ const tiers = React.useMemo(() => {
395
+ const clampX = (date: Date) => clamp(differenceInCalendarDays(date, start), 0, days)
396
+ const span = (from: Date, to: Date) => ({
397
+ left: clampX(from) * dayWidth,
398
+ width: (clampX(addDays(to, 1)) - clampX(from)) * dayWidth,
399
+ })
400
+ const months = eachMonthOfInterval({ start, end }).map((m) => ({
401
+ key: m.toISOString(),
402
+ label: format(m, scale === "month" ? "MMMM yyyy" : "MMM yyyy"),
403
+ ...span(startOfMonth(m), endOfMonth(m)),
404
+ }))
405
+ if (scale === "day") {
406
+ const dayCells = eachDayOfInterval({ start, end }).map((d) => ({
407
+ key: d.toISOString(),
408
+ label: dayWidth >= 40 ? format(d, "EEEEE d") : format(d, "d"),
409
+ weekend: isWeekend(d),
410
+ today: isSameDay(d, new Date()),
411
+ ...span(d, d),
412
+ }))
413
+ return { top: months, bottom: dayCells }
414
+ }
415
+ if (scale === "week") {
416
+ const weeks = eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((w) => ({
417
+ key: w.toISOString(),
418
+ label: format(w, "d MMM"),
419
+ weekend: false,
420
+ today: false,
421
+ ...span(startOfWeek(w, { weekStartsOn: 1 }), endOfWeek(w, { weekStartsOn: 1 })),
422
+ }))
423
+ return { top: months, bottom: weeks }
424
+ }
425
+ const years = Array.from(new Set(months.map((m) => m.label.slice(-4)))).map((y) => {
426
+ const first = new Date(Number(y), 0, 1)
427
+ const last = new Date(Number(y), 11, 31)
428
+ return { key: y, label: y, ...span(first, last) }
429
+ })
430
+ return {
431
+ top: years,
432
+ bottom: months.map((m) => ({ ...m, label: format(new Date(m.key), "MMM"), weekend: false, today: false })),
433
+ }
434
+ }, [start, end, days, dayWidth, scale])
435
+
436
+ return (
437
+ <div
438
+ data-slot="gantt-header"
439
+ className={cn("sticky top-0 z-30 flex bg-muted text-xs text-muted-foreground", className)}
440
+ style={{ width: sidebarWidth + width }}
441
+ {...props}
442
+ >
443
+ <GanttHeaderCorner />
444
+ <div className="relative shrink-0" style={{ width }}>
445
+ <div className="flex h-7 border-b">
446
+ {tiers.top.map((cell) => (
447
+ <div
448
+ key={cell.key}
449
+ className="absolute inset-y-0 flex items-center truncate border-e px-2 font-medium"
450
+ style={{ left: cell.left, width: cell.width }}
451
+ >
452
+ {cell.label}
453
+ </div>
454
+ ))}
455
+ </div>
456
+ <div className="flex h-7 border-b">
457
+ {tiers.bottom.map((cell) => (
458
+ <div
459
+ key={cell.key}
460
+ data-weekend={cell.weekend ? "" : undefined}
461
+ data-today={cell.today ? "" : undefined}
462
+ className="absolute flex h-7 items-center justify-center truncate border-e data-today:font-semibold data-today:text-foreground data-weekend:bg-foreground/3"
463
+ style={{ left: cell.left, width: cell.width, top: 28 }}
464
+ >
465
+ {cell.width >= 18 ? cell.label : ""}
466
+ </div>
467
+ ))}
468
+ </div>
469
+ </div>
470
+ </div>
471
+ )
472
+ }
473
+
474
+ /** The sticky cell above the sidebar; put a title or a search box in it. */
475
+ function GanttHeaderCorner({ className, ...props }: React.ComponentProps<"div">) {
476
+ return (
477
+ <div
478
+ data-slot="gantt-header-corner"
479
+ className={cn(
480
+ "sticky left-0 z-40 flex h-14 shrink-0 items-end border-e border-b bg-muted px-3 pb-2 font-medium",
481
+ className
482
+ )}
483
+ style={{ width: "var(--gantt-sidebar)" }}
484
+ {...props}
485
+ />
486
+ )
487
+ }
488
+
489
+ function GanttBody({ className, style, ...props }: React.ComponentProps<"div">) {
490
+ const { days, dayWidth, sidebarWidth } = useGantt()
491
+ return (
492
+ <div
493
+ data-slot="gantt-body"
494
+ className={cn("relative", className)}
495
+ style={{ width: sidebarWidth + days * dayWidth, ...style }}
496
+ {...props}
497
+ />
498
+ )
499
+ }
500
+
501
+ /** Vertical day/week lines and weekend shading behind the bars. */
502
+ function GanttGridLines({ className, ...props }: React.ComponentProps<"div">) {
503
+ const { start, end, dayWidth, scale, sidebarWidth } = useGantt()
504
+ const cells = React.useMemo(() => {
505
+ if (scale === "month")
506
+ return eachMonthOfInterval({ start, end }).map((d) => ({ date: startOfMonth(d), weekend: false }))
507
+ if (scale === "week")
508
+ return eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((d) => ({ date: d, weekend: false }))
509
+ return eachDayOfInterval({ start, end }).map((d) => ({ date: d, weekend: isWeekend(d) }))
510
+ }, [start, end, scale])
511
+ return (
512
+ <div
513
+ data-slot="gantt-grid-lines"
514
+ aria-hidden
515
+ className={cn("pointer-events-none absolute inset-y-0", className)}
516
+ style={{ left: sidebarWidth, right: 0 }}
517
+ {...props}
518
+ >
519
+ {cells.map((cell) => (
520
+ <div
521
+ key={cell.date.toISOString()}
522
+ data-weekend={cell.weekend ? "" : undefined}
523
+ className="absolute inset-y-0 border-e border-border/60 data-weekend:bg-foreground/2.5"
524
+ style={{
525
+ left: differenceInCalendarDays(cell.date, start) * dayWidth,
526
+ width: scale === "day" ? dayWidth : undefined,
527
+ }}
528
+ />
529
+ ))}
530
+ </div>
531
+ )
532
+ }
533
+
534
+ /* ---------------------------------------------------------------------------
535
+ * Rows
536
+ * ------------------------------------------------------------------------- */
537
+
538
+ /** Groups the rows; keep bars inside so dependency lines can find them. */
539
+ function GanttRows({ className, ...props }: React.ComponentProps<"div">) {
540
+ return <div data-slot="gantt-rows" className={cn("relative", className)} {...props} />
541
+ }
542
+
543
+ function GanttRow({ value, className, children, ...props }: React.ComponentProps<"div"> & { value: string }) {
544
+ const { rowHeight } = useGantt()
545
+ const ctx = React.useMemo(() => ({ value }), [value])
546
+ return (
547
+ <RowContext.Provider value={ctx}>
548
+ <div
549
+ data-slot="gantt-row"
550
+ data-value={value}
551
+ className={cn("group/row flex border-b hover:bg-muted/30", className)}
552
+ style={{ height: rowHeight }}
553
+ {...props}
554
+ >
555
+ {children}
556
+ </div>
557
+ </RowContext.Provider>
558
+ )
559
+ }
560
+
561
+ /** Sticky sidebar cell for the row. */
562
+ function GanttRowLabel({ className, ...props }: React.ComponentProps<"div">) {
563
+ return (
564
+ <div
565
+ data-slot="gantt-row-label"
566
+ className={cn(
567
+ "sticky left-0 z-20 flex shrink-0 items-center gap-2 border-e bg-background px-3 text-sm transition-colors",
568
+ "group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_30%,var(--color-background))]",
569
+ className
570
+ )}
571
+ style={{ width: "var(--gantt-sidebar)" }}
572
+ {...props}
573
+ />
574
+ )
575
+ }
576
+
577
+ /** The timeline part of a row; bars are positioned inside it. */
578
+ function GanttRowTrack({ className, ...props }: React.ComponentProps<"div">) {
579
+ const { days, dayWidth } = useGantt()
580
+ return (
581
+ <div
582
+ data-slot="gantt-row-track"
583
+ className={cn("relative shrink-0", className)}
584
+ style={{ width: days * dayWidth }}
585
+ {...props}
586
+ />
587
+ )
588
+ }
589
+
590
+ /* ---------------------------------------------------------------------------
591
+ * Bars
592
+ * ------------------------------------------------------------------------- */
593
+
594
+ const BarContext = React.createContext<BarRegistration | null>(null)
595
+
596
+ function GanttBar({
597
+ value,
598
+ start,
599
+ end,
600
+ progress,
601
+ className,
602
+ style,
603
+ children,
604
+ onPointerDown,
605
+ onKeyDown,
606
+ onBlur,
607
+ ...props
608
+ }: React.ComponentProps<"div"> & { value: string; start: Date; end: Date; progress?: number }) {
609
+ const gantt = useGantt()
610
+ useGanttRow()
611
+ const ref = React.useRef<HTMLDivElement>(null)
612
+ const live = gantt.interaction?.id === value ? gantt.interaction : null
613
+ // Key on timestamps: callers often pass fresh Date objects every render.
614
+ const startMs = (live ? live.start : startOfDay(start)).getTime()
615
+ const endMs = (live ? live.end : startOfDay(end)).getTime()
616
+ const registration = React.useMemo<BarRegistration>(
617
+ () => ({ id: value, el: null, start: new Date(startMs), end: new Date(endMs) }),
618
+ [value, startMs, endMs]
619
+ )
620
+ const shownStart = registration.start
621
+ const shownEnd = registration.end
622
+ const { registerBar } = gantt
623
+ React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])
624
+ const left = gantt.xOf(shownStart)
625
+ const width = Math.max(gantt.dayWidth, (differenceInCalendarDays(shownEnd, shownStart) + 1) * gantt.dayWidth)
626
+ const isGrabbed = gantt.grabbed === value
627
+
628
+ return (
629
+ <BarContext.Provider value={registration}>
630
+ <div
631
+ ref={ref}
632
+ data-slot="gantt-bar"
633
+ data-value={value}
634
+ data-dragging={live ? live.type : undefined}
635
+ data-grabbed={isGrabbed ? "" : undefined}
636
+ tabIndex={gantt.editable ? 0 : undefined}
637
+ aria-pressed={gantt.editable ? isGrabbed : undefined}
638
+ aria-label={`${format(shownStart, "MMM d")} to ${format(shownEnd, "MMM d")}`}
639
+ className={cn(
640
+ "group/bar absolute top-1/2 z-10 flex h-7 -translate-y-1/2 items-center overflow-hidden rounded-md border border-primary/30 bg-primary/15 text-xs text-foreground outline-none select-none",
641
+ gantt.editable && "cursor-grab active:cursor-grabbing",
642
+ "focus-visible:ring-2 focus-visible:ring-ring/50 data-dragging:z-20 data-dragging:shadow-md data-grabbed:ring-2 data-grabbed:ring-primary",
643
+ className
644
+ )}
645
+ style={{ left, width, ...style }}
646
+ onPointerDown={(event) => {
647
+ onPointerDown?.(event)
648
+ if (event.defaultPrevented || event.button !== 0) return
649
+ if ((event.target as HTMLElement).closest("[data-slot=gantt-bar-resize-handle], button, a, input")) return
650
+ gantt.startInteraction(registration, "move", event)
651
+ }}
652
+ onKeyDown={(event) => {
653
+ onKeyDown?.(event)
654
+ if (event.defaultPrevented || !gantt.editable || event.target !== event.currentTarget) return
655
+ if (event.key === " " || event.key === "Enter") {
656
+ event.preventDefault()
657
+ gantt.grab(isGrabbed ? null : value)
658
+ return
659
+ }
660
+ if (!isGrabbed) return
661
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
662
+ event.preventDefault()
663
+ const delta = event.key === "ArrowLeft" ? -1 : 1
664
+ gantt.nudge(registration, delta, event.shiftKey ? "end" : event.altKey ? "start" : "both")
665
+ } else if (event.key === "Escape") {
666
+ event.preventDefault()
667
+ gantt.cancelGrab()
668
+ }
669
+ }}
670
+ onBlur={(event) => {
671
+ onBlur?.(event)
672
+ if (isGrabbed && !event.currentTarget.contains(event.relatedTarget as Node | null)) gantt.grab(null)
673
+ }}
674
+ {...props}
675
+ >
676
+ {progress !== undefined && (
677
+ <div
678
+ data-slot="gantt-bar-progress"
679
+ aria-hidden
680
+ className="absolute inset-y-0 left-0 bg-primary/30"
681
+ style={{ width: `${clamp(progress, 0, 1) * 100}%` }}
682
+ />
683
+ )}
684
+ {children}
685
+ </div>
686
+ </BarContext.Provider>
687
+ )
688
+ }
689
+
690
+ function GanttBarLabel({ className, ...props }: React.ComponentProps<"span">) {
691
+ return (
692
+ <span data-slot="gantt-bar-label" className={cn("relative z-10 truncate px-2 font-medium", className)} {...props} />
693
+ )
694
+ }
695
+
696
+ function GanttBarResizeHandle({
697
+ side,
698
+ className,
699
+ onPointerDown,
700
+ ...props
701
+ }: React.ComponentProps<"div"> & { side: "start" | "end" }) {
702
+ const gantt = useGantt()
703
+ const bar = React.useContext(BarContext)
704
+ if (!gantt.editable || !bar) return null
705
+ return (
706
+ <div
707
+ data-slot="gantt-bar-resize-handle"
708
+ data-side={side}
709
+ role="separator"
710
+ aria-orientation="vertical"
711
+ className={cn(
712
+ "absolute inset-y-0 z-10 w-2 cursor-ew-resize touch-none opacity-0 transition-opacity group-hover/bar:opacity-100 group-focus-visible/bar:opacity-100",
713
+ side === "start" ? "left-0 rounded-l-md" : "right-0 rounded-r-md",
714
+ "after:absolute after:inset-y-1.5 after:w-0.5 after:rounded-full after:bg-primary/60",
715
+ side === "start" ? "after:left-0.5" : "after:right-0.5",
716
+ className
717
+ )}
718
+ onPointerDown={(event) => {
719
+ onPointerDown?.(event)
720
+ if (event.defaultPrevented || event.button !== 0) return
721
+ gantt.startInteraction(bar, side === "start" ? "resize-start" : "resize-end", event)
722
+ }}
723
+ {...props}
724
+ />
725
+ )
726
+ }
727
+
728
+ /** A single-day marker rendered as a diamond. */
729
+ function GanttMilestone({
730
+ value,
731
+ date,
732
+ className,
733
+ style,
734
+ children,
735
+ ...props
736
+ }: React.ComponentProps<"div"> & { value: string; date: Date }) {
737
+ const gantt = useGantt()
738
+ useGanttRow()
739
+ const ref = React.useRef<HTMLDivElement>(null)
740
+ const dayMs = startOfDay(date).getTime()
741
+ const registration = React.useMemo<BarRegistration>(
742
+ () => ({ id: value, el: null, start: new Date(dayMs), end: new Date(dayMs), milestone: true }),
743
+ [value, dayMs]
744
+ )
745
+ const day = registration.start
746
+ const { registerBar } = gantt
747
+ React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])
748
+ return (
749
+ <div
750
+ ref={ref}
751
+ data-slot="gantt-milestone"
752
+ data-value={value}
753
+ aria-label={format(day, "MMM d")}
754
+ className={cn("absolute top-1/2 z-10 flex -translate-y-1/2 items-center gap-2", className)}
755
+ style={{ left: gantt.xOf(day) + gantt.dayWidth / 2 - 8, ...style }}
756
+ {...props}
757
+ >
758
+ <span aria-hidden className="block size-4 rotate-45 rounded-sm bg-foreground" />
759
+ {children && <span className="text-xs font-medium whitespace-nowrap">{children}</span>}
760
+ </div>
761
+ )
762
+ }
763
+
764
+ /* ---------------------------------------------------------------------------
765
+ * Dependencies, today, empty
766
+ * ------------------------------------------------------------------------- */
767
+
768
+ /** Arrows from the end of `from` to the start of `to`, measured from the registered bar elements. */
769
+ function GanttDependencies({
770
+ links,
771
+ className,
772
+ ...props
773
+ }: React.ComponentProps<"svg"> & { links: { from: string; to: string }[] }) {
774
+ const { barStore, dayWidth, xOf, sidebarWidth, rowHeight, days } = useGantt()
775
+ React.useSyncExternalStore(barStore.subscribe, barStore.getVersion, barStore.getVersion)
776
+ const svgRef = React.useRef<SVGSVGElement>(null)
777
+ const id = React.useId()
778
+ // Row centres come from the DOM so collapsed groups and custom row order need no bookkeeping.
779
+ const origin = svgRef.current?.getBoundingClientRect()
780
+ const centreY = (el: HTMLElement | null) => {
781
+ if (!el || !origin) return null
782
+ const r = el.getBoundingClientRect()
783
+ return r.top + r.height / 2 - origin.top
784
+ }
785
+ const paths = links
786
+ .map((link) => {
787
+ const from = barStore.bars.get(link.from)
788
+ const to = barStore.bars.get(link.to)
789
+ if (!from || !to) return null
790
+ const y1 = centreY(from.el)
791
+ const y2 = centreY(to.el)
792
+ if (y1 === null || y2 === null) return null
793
+ const x1 = xOf(from.end) + (from.milestone ? dayWidth / 2 + 8 : dayWidth)
794
+ const x2 = xOf(to.start) + (to.milestone ? dayWidth / 2 - 8 : 0)
795
+ const gap = 10
796
+ const d =
797
+ x2 - gap > x1
798
+ ? `M ${x1} ${y1} H ${x1 + gap} V ${y2} H ${x2}`
799
+ : `M ${x1} ${y1} H ${x1 + gap} V ${y1 + rowHeight / 2 - 4} H ${x2 - gap} V ${y2} H ${x2}`
800
+ return { key: `${link.from}->${link.to}`, d }
801
+ })
802
+ .filter((p): p is { key: string; d: string } => !!p)
803
+ return (
804
+ <svg
805
+ ref={svgRef}
806
+ data-slot="gantt-dependencies"
807
+ aria-hidden
808
+ className={cn("pointer-events-none absolute top-0 z-5 overflow-hidden text-muted-foreground", className)}
809
+ style={{ left: sidebarWidth, width: days * dayWidth, height: "100%" }}
810
+ {...props}
811
+ >
812
+ <defs>
813
+ <marker id={`${id}-arrow`} viewBox="0 0 6 6" refX="5" refY="3" markerWidth="6" markerHeight="6" orient="auto">
814
+ <path d="M 0 0 L 6 3 L 0 6 z" fill="currentColor" />
815
+ </marker>
816
+ </defs>
817
+ {paths.map((p) => (
818
+ <path key={p.key} d={p.d} fill="none" stroke="currentColor" strokeWidth={1.5} markerEnd={`url(#${id}-arrow)`} />
819
+ ))}
820
+ </svg>
821
+ )
822
+ }
823
+
824
+ function GanttToday({ className, date = new Date(), ...props }: React.ComponentProps<"div"> & { date?: Date }) {
825
+ const { xOf, dayWidth, sidebarWidth, start, end } = useGantt()
826
+ const day = startOfDay(date)
827
+ if (day < start || day > end) return null
828
+ return (
829
+ <div
830
+ data-slot="gantt-today"
831
+ aria-hidden
832
+ className={cn("pointer-events-none absolute inset-y-0 w-px bg-destructive", className)}
833
+ style={{ left: sidebarWidth + xOf(day) + dayWidth / 2 }}
834
+ {...props}
835
+ >
836
+ <span className="absolute top-0 left-1 rounded-b bg-destructive px-1 text-[10px]/4 text-white">Today</span>
837
+ </div>
838
+ )
839
+ }
840
+
841
+ function GanttEmpty({ className, ...props }: React.ComponentProps<"div">) {
842
+ return (
843
+ <div
844
+ data-slot="gantt-empty"
845
+ className={cn("flex h-32 items-center justify-center text-sm text-muted-foreground", className)}
846
+ {...props}
847
+ />
848
+ )
849
+ }
850
+
851
+ /* ---------------------------------------------------------------------------
852
+ * Controls
853
+ * ------------------------------------------------------------------------- */
854
+
855
+ function GanttControls({ className, ...props }: React.ComponentProps<"div">) {
856
+ return <div data-slot="gantt-controls" className={cn("flex items-center gap-1", className)} {...props} />
857
+ }
858
+
859
+ function GanttZoomInTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
860
+ const { zoomIn } = useGantt()
861
+ return (
862
+ <Button
863
+ data-slot="gantt-zoom-in-trigger"
864
+ variant="outline"
865
+ size="icon-sm"
866
+ aria-label="Zoom in"
867
+ asChild={asChild}
868
+ onClick={(event) => {
869
+ onClick?.(event)
870
+ if (!event.defaultPrevented) zoomIn()
871
+ }}
872
+ {...props}
873
+ >
874
+ {asChild ? children : (children ?? <PlusIcon />)}
875
+ </Button>
876
+ )
877
+ }
878
+
879
+ function GanttZoomOutTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
880
+ const { zoomOut } = useGantt()
881
+ return (
882
+ <Button
883
+ data-slot="gantt-zoom-out-trigger"
884
+ variant="outline"
885
+ size="icon-sm"
886
+ aria-label="Zoom out"
887
+ asChild={asChild}
888
+ onClick={(event) => {
889
+ onClick?.(event)
890
+ if (!event.defaultPrevented) zoomOut()
891
+ }}
892
+ {...props}
893
+ >
894
+ {asChild ? children : (children ?? <MinusIcon />)}
895
+ </Button>
896
+ )
897
+ }
898
+
899
+ function GanttTodayTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
900
+ const { scrollToDate } = useGantt()
901
+ return (
902
+ <Button
903
+ data-slot="gantt-today-trigger"
904
+ variant="outline"
905
+ size="sm"
906
+ asChild={asChild}
907
+ onClick={(event) => {
908
+ onClick?.(event)
909
+ if (!event.defaultPrevented) scrollToDate(new Date())
910
+ }}
911
+ {...props}
912
+ >
913
+ {asChild
914
+ ? children
915
+ : (children ?? (
916
+ <>
917
+ <CalendarIcon /> Today
918
+ </>
919
+ ))}
920
+ </Button>
921
+ )
922
+ }
923
+
924
+ export {
925
+ Gantt,
926
+ GanttViewport,
927
+ GanttHeader,
928
+ GanttHeaderCorner,
929
+ GanttBody,
930
+ GanttGridLines,
931
+ GanttRows,
932
+ GanttRow,
933
+ GanttRowLabel,
934
+ GanttRowTrack,
935
+ GanttBar,
936
+ GanttBarLabel,
937
+ GanttBarResizeHandle,
938
+ GanttMilestone,
939
+ GanttDependencies,
940
+ GanttToday,
941
+ GanttEmpty,
942
+ GanttControls,
943
+ GanttZoomInTrigger,
944
+ GanttZoomOutTrigger,
945
+ GanttTodayTrigger,
946
+ useGantt,
947
+ useGanttRow,
948
+ type GanttScale,
949
+ type GanttBarChange,
950
+ }