@multicomma/arkcn 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/package.json +1 -1
  2. package/registry/index.json +1 -1
  3. package/registry/items/accordion.json +302 -0
  4. package/registry/items/activity-feed.json +408 -0
  5. package/registry/items/alert-dialog.json +676 -0
  6. package/registry/items/alert.json +113 -0
  7. package/registry/items/angle-slider.json +378 -0
  8. package/registry/items/app-shell.json +2654 -1
  9. package/registry/items/aspect-ratio.json +36 -0
  10. package/registry/items/attachment.json +299 -1
  11. package/registry/items/avatar.json +213 -0
  12. package/registry/items/badge.json +44 -0
  13. package/registry/items/breadcrumb.json +191 -0
  14. package/registry/items/bubble.json +146 -0
  15. package/registry/items/button-group.json +110 -0
  16. package/registry/items/button.json +52 -0
  17. package/registry/items/calendar.json +499 -1
  18. package/registry/items/canvas.json +446 -1
  19. package/registry/items/card.json +192 -0
  20. package/registry/items/carousel.json +208 -1
  21. package/registry/items/cascader.json +797 -1
  22. package/registry/items/chart.json +1764 -1
  23. package/registry/items/chat.json +993 -0
  24. package/registry/items/checkbox.json +503 -0
  25. package/registry/items/clipboard.json +308 -0
  26. package/registry/items/collapsible.json +226 -0
  27. package/registry/items/color-picker.json +1261 -0
  28. package/registry/items/combobox.json +1123 -1
  29. package/registry/items/command.json +837 -1
  30. package/registry/items/comment-thread.json +952 -1
  31. package/registry/items/context-menu.json +1344 -1
  32. package/registry/items/data-grid.json +449 -1
  33. package/registry/items/data-table.json +1310 -1
  34. package/registry/items/date-input.json +476 -0
  35. package/registry/items/dialog.json +592 -0
  36. package/registry/items/direction.json +29 -1
  37. package/registry/items/drawer.json +797 -0
  38. package/registry/items/dropdown-menu.json +1311 -1
  39. package/registry/items/editable.json +503 -0
  40. package/registry/items/empty.json +166 -0
  41. package/registry/items/field.json +286 -1
  42. package/registry/items/file-upload.json +638 -0
  43. package/registry/items/floating-panel.json +720 -0
  44. package/registry/items/floating-toolbar.json +43 -0
  45. package/registry/items/format.json +109 -0
  46. package/registry/items/gantt.json +773 -1
  47. package/registry/items/hooks/use-mobile.json +1 -0
  48. package/registry/items/hotkeys.json +99 -1
  49. package/registry/items/hover-card.json +357 -0
  50. package/registry/items/image-cropper.json +431 -0
  51. package/registry/items/input-group.json +189 -0
  52. package/registry/items/input-otp.json +181 -0
  53. package/registry/items/input.json +28 -0
  54. package/registry/items/item.json +308 -0
  55. package/registry/items/json-tree-view.json +336 -0
  56. package/registry/items/kanban.json +624 -1
  57. package/registry/items/kbd.json +54 -0
  58. package/registry/items/label.json +28 -0
  59. package/registry/items/lib/controllable.json +1 -0
  60. package/registry/items/lib/data-table-adapter.json +1 -0
  61. package/registry/items/lib/drag-preview.json +1 -0
  62. package/registry/items/lib/form.json +1 -0
  63. package/registry/items/lib/history.json +1 -0
  64. package/registry/items/lib/row-layout.json +1 -0
  65. package/registry/items/lib/table.json +1 -0
  66. package/registry/items/lib/time.json +1 -0
  67. package/registry/items/lib/utils.json +1 -0
  68. package/registry/items/live-region.json +43 -0
  69. package/registry/items/marker.json +96 -0
  70. package/registry/items/marquee.json +300 -0
  71. package/registry/items/menubar.json +1282 -1
  72. package/registry/items/message.json +166 -0
  73. package/registry/items/navigation-menu.json +561 -1
  74. package/registry/items/node-graph.json +1037 -1
  75. package/registry/items/number-input.json +519 -0
  76. package/registry/items/pagination.json +244 -1
  77. package/registry/items/password-input.json +311 -0
  78. package/registry/items/popconfirm.json +566 -0
  79. package/registry/items/popover.json +615 -0
  80. package/registry/items/progress.json +516 -0
  81. package/registry/items/qr-code.json +249 -0
  82. package/registry/items/query-builder.json +1368 -1
  83. package/registry/items/radio-group.json +362 -0
  84. package/registry/items/rating-group.json +304 -0
  85. package/registry/items/resizable.json +242 -1
  86. package/registry/items/rich-text-editor.json +934 -1
  87. package/registry/items/scheduler.json +1013 -1
  88. package/registry/items/scroll-area.json +227 -0
  89. package/registry/items/segment-group.json +363 -0
  90. package/registry/items/select.json +956 -0
  91. package/registry/items/separator.json +45 -1
  92. package/registry/items/sheet.json +592 -0
  93. package/registry/items/sidebar.json +785 -1
  94. package/registry/items/signature-pad.json +330 -0
  95. package/registry/items/skeleton.json +28 -0
  96. package/registry/items/slider.json +799 -0
  97. package/registry/items/sonner.json +135 -0
  98. package/registry/items/spinner.json +28 -0
  99. package/registry/items/steps.json +483 -0
  100. package/registry/items/swap.json +107 -0
  101. package/registry/items/switch.json +362 -0
  102. package/registry/items/table.json +210 -0
  103. package/registry/items/tabs.json +326 -0
  104. package/registry/items/tags-input.json +640 -0
  105. package/registry/items/textarea.json +28 -0
  106. package/registry/items/timer.json +289 -0
  107. package/registry/items/toc.json +372 -0
  108. package/registry/items/toggle-group.json +214 -1
  109. package/registry/items/toggle.json +124 -0
  110. package/registry/items/tooltip.json +417 -0
  111. package/registry/items/tour.json +498 -0
  112. package/registry/items/transfer-list.json +609 -1
  113. package/registry/items/tree-select.json +742 -1
  114. package/registry/items/tree-view.json +794 -0
  115. package/registry/items/virtual-list.json +214 -1
  116. package/src/components/ui/app-shell.tsx +11 -0
  117. package/src/components/ui/attachment.tsx +1 -0
  118. package/src/components/ui/calendar.tsx +1 -0
  119. package/src/components/ui/canvas.tsx +8 -0
  120. package/src/components/ui/carousel.tsx +3 -0
  121. package/src/components/ui/cascader.tsx +1 -0
  122. package/src/components/ui/chart.tsx +10 -0
  123. package/src/components/ui/combobox.tsx +3 -0
  124. package/src/components/ui/command.tsx +1 -0
  125. package/src/components/ui/comment-thread.tsx +11 -0
  126. package/src/components/ui/context-menu.tsx +6 -0
  127. package/src/components/ui/data-grid.tsx +8 -0
  128. package/src/components/ui/data-table.tsx +6 -0
  129. package/src/components/ui/direction.tsx +2 -0
  130. package/src/components/ui/dropdown-menu.tsx +6 -0
  131. package/src/components/ui/field.tsx +1 -0
  132. package/src/components/ui/gantt.tsx +15 -0
  133. package/src/components/ui/hotkeys.tsx +3 -0
  134. package/src/components/ui/kanban.tsx +3 -0
  135. package/src/components/ui/menubar.tsx +6 -0
  136. package/src/components/ui/navigation-menu.tsx +1 -0
  137. package/src/components/ui/node-graph.tsx +23 -0
  138. package/src/components/ui/pagination.tsx +1 -0
  139. package/src/components/ui/query-builder.tsx +7 -0
  140. package/src/components/ui/resizable.tsx +1 -0
  141. package/src/components/ui/rich-text-editor.tsx +54 -0
  142. package/src/components/ui/scheduler.tsx +26 -0
  143. package/src/components/ui/separator.tsx +1 -0
  144. package/src/components/ui/sidebar.tsx +5 -0
  145. package/src/components/ui/toggle-group.tsx +3 -0
  146. package/src/components/ui/transfer-list.tsx +11 -0
  147. package/src/components/ui/tree-select.tsx +5 -0
  148. package/src/components/ui/virtual-list.tsx +7 -0
@@ -27,6 +27,778 @@
27
27
  "useGantt",
28
28
  "useGanttRow"
29
29
  ],
30
+ "parts": [
31
+ {
32
+ "name": "Gantt",
33
+ "props": [
34
+ {
35
+ "name": "end",
36
+ "type": "Date",
37
+ "required": true,
38
+ "description": "Last date on the time scale.",
39
+ "from": "arkcn"
40
+ },
41
+ {
42
+ "name": "start",
43
+ "type": "Date",
44
+ "required": true,
45
+ "description": "First date on the time scale.",
46
+ "from": "arkcn"
47
+ },
48
+ {
49
+ "name": "dayWidth",
50
+ "type": "number",
51
+ "required": false,
52
+ "description": "Pixels per day; controllable. Zooming changes it.",
53
+ "from": "arkcn"
54
+ },
55
+ {
56
+ "name": "defaultDayWidth",
57
+ "type": "number",
58
+ "required": false,
59
+ "default": "40",
60
+ "description": "Initial pixels per day when uncontrolled.",
61
+ "from": "arkcn"
62
+ },
63
+ {
64
+ "name": "editable",
65
+ "type": "boolean",
66
+ "required": false,
67
+ "default": "true",
68
+ "description": "Allow editing.",
69
+ "from": "arkcn"
70
+ },
71
+ {
72
+ "name": "maxDayWidth",
73
+ "type": "number",
74
+ "required": false,
75
+ "default": "160",
76
+ "description": "Upper bound for zooming in.",
77
+ "from": "arkcn"
78
+ },
79
+ {
80
+ "name": "minDayWidth",
81
+ "type": "number",
82
+ "required": false,
83
+ "default": "8",
84
+ "description": "Lower bound for zooming out.",
85
+ "from": "arkcn"
86
+ },
87
+ {
88
+ "name": "onBarChange",
89
+ "type": "((change: GanttBarChange) => void)",
90
+ "required": false,
91
+ "description": "Called with `{ id, start, end }` snapped to whole days after a move, resize, or keyboard change.",
92
+ "from": "arkcn"
93
+ },
94
+ {
95
+ "name": "onDayWidthChange",
96
+ "type": "((dayWidth: number) => void)",
97
+ "required": false,
98
+ "description": "Called when the zoom changes.",
99
+ "from": "arkcn"
100
+ },
101
+ {
102
+ "name": "rowHeight",
103
+ "type": "number",
104
+ "required": false,
105
+ "default": "40",
106
+ "description": "Height of every row in px.",
107
+ "from": "arkcn"
108
+ },
109
+ {
110
+ "name": "scale",
111
+ "type": "GanttScale",
112
+ "required": false,
113
+ "description": "Header tiers. Defaults from `dayWidth`: month below 16px, week below 32px, else day.",
114
+ "from": "arkcn"
115
+ },
116
+ {
117
+ "name": "sidebarWidth",
118
+ "type": "number",
119
+ "required": false,
120
+ "default": "240",
121
+ "description": "Width of the sticky label column in px.",
122
+ "from": "arkcn"
123
+ },
124
+ {
125
+ "name": "children",
126
+ "type": "ReactNode",
127
+ "required": false,
128
+ "description": "Content rendered inside the part.",
129
+ "from": "@types/react"
130
+ },
131
+ {
132
+ "name": "className",
133
+ "type": "string",
134
+ "required": false,
135
+ "description": "Extra classes merged into the part's own classes with `cn`.",
136
+ "from": "@types/react"
137
+ },
138
+ {
139
+ "name": "style",
140
+ "type": "CSSProperties",
141
+ "required": false,
142
+ "description": "Inline styles applied to the rendered element.",
143
+ "from": "@types/react"
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ "name": "GanttViewport",
149
+ "props": [
150
+ {
151
+ "name": "children",
152
+ "type": "ReactNode",
153
+ "required": false,
154
+ "description": "Content rendered inside the part.",
155
+ "from": "@types/react"
156
+ },
157
+ {
158
+ "name": "className",
159
+ "type": "string",
160
+ "required": false,
161
+ "description": "Extra classes merged into the part's own classes with `cn`.",
162
+ "from": "@types/react"
163
+ },
164
+ {
165
+ "name": "style",
166
+ "type": "CSSProperties",
167
+ "required": false,
168
+ "description": "Inline styles applied to the rendered element.",
169
+ "from": "@types/react"
170
+ }
171
+ ]
172
+ },
173
+ {
174
+ "name": "GanttHeader",
175
+ "props": [
176
+ {
177
+ "name": "children",
178
+ "type": "ReactNode",
179
+ "required": false,
180
+ "description": "Content rendered inside the part.",
181
+ "from": "@types/react"
182
+ },
183
+ {
184
+ "name": "className",
185
+ "type": "string",
186
+ "required": false,
187
+ "description": "Extra classes merged into the part's own classes with `cn`.",
188
+ "from": "@types/react"
189
+ },
190
+ {
191
+ "name": "style",
192
+ "type": "CSSProperties",
193
+ "required": false,
194
+ "description": "Inline styles applied to the rendered element.",
195
+ "from": "@types/react"
196
+ }
197
+ ]
198
+ },
199
+ {
200
+ "name": "GanttHeaderCorner",
201
+ "props": [
202
+ {
203
+ "name": "children",
204
+ "type": "ReactNode",
205
+ "required": false,
206
+ "description": "Content rendered inside the part.",
207
+ "from": "@types/react"
208
+ },
209
+ {
210
+ "name": "className",
211
+ "type": "string",
212
+ "required": false,
213
+ "description": "Extra classes merged into the part's own classes with `cn`.",
214
+ "from": "@types/react"
215
+ },
216
+ {
217
+ "name": "style",
218
+ "type": "CSSProperties",
219
+ "required": false,
220
+ "description": "Inline styles applied to the rendered element.",
221
+ "from": "@types/react"
222
+ }
223
+ ]
224
+ },
225
+ {
226
+ "name": "GanttBody",
227
+ "props": [
228
+ {
229
+ "name": "children",
230
+ "type": "ReactNode",
231
+ "required": false,
232
+ "description": "Content rendered inside the part.",
233
+ "from": "@types/react"
234
+ },
235
+ {
236
+ "name": "className",
237
+ "type": "string",
238
+ "required": false,
239
+ "description": "Extra classes merged into the part's own classes with `cn`.",
240
+ "from": "@types/react"
241
+ },
242
+ {
243
+ "name": "style",
244
+ "type": "CSSProperties",
245
+ "required": false,
246
+ "description": "Inline styles applied to the rendered element.",
247
+ "from": "@types/react"
248
+ }
249
+ ]
250
+ },
251
+ {
252
+ "name": "GanttGridLines",
253
+ "props": [
254
+ {
255
+ "name": "children",
256
+ "type": "ReactNode",
257
+ "required": false,
258
+ "description": "Content rendered inside the part.",
259
+ "from": "@types/react"
260
+ },
261
+ {
262
+ "name": "className",
263
+ "type": "string",
264
+ "required": false,
265
+ "description": "Extra classes merged into the part's own classes with `cn`.",
266
+ "from": "@types/react"
267
+ },
268
+ {
269
+ "name": "style",
270
+ "type": "CSSProperties",
271
+ "required": false,
272
+ "description": "Inline styles applied to the rendered element.",
273
+ "from": "@types/react"
274
+ }
275
+ ]
276
+ },
277
+ {
278
+ "name": "GanttRows",
279
+ "props": [
280
+ {
281
+ "name": "children",
282
+ "type": "ReactNode",
283
+ "required": false,
284
+ "description": "Content rendered inside the part.",
285
+ "from": "@types/react"
286
+ },
287
+ {
288
+ "name": "className",
289
+ "type": "string",
290
+ "required": false,
291
+ "description": "Extra classes merged into the part's own classes with `cn`.",
292
+ "from": "@types/react"
293
+ },
294
+ {
295
+ "name": "style",
296
+ "type": "CSSProperties",
297
+ "required": false,
298
+ "description": "Inline styles applied to the rendered element.",
299
+ "from": "@types/react"
300
+ }
301
+ ]
302
+ },
303
+ {
304
+ "name": "GanttRow",
305
+ "props": [
306
+ {
307
+ "name": "value",
308
+ "type": "string",
309
+ "required": true,
310
+ "description": "The controlled value.",
311
+ "from": "arkcn"
312
+ },
313
+ {
314
+ "name": "children",
315
+ "type": "ReactNode",
316
+ "required": false,
317
+ "description": "Content rendered inside the part.",
318
+ "from": "@types/react"
319
+ },
320
+ {
321
+ "name": "className",
322
+ "type": "string",
323
+ "required": false,
324
+ "description": "Extra classes merged into the part's own classes with `cn`.",
325
+ "from": "@types/react"
326
+ },
327
+ {
328
+ "name": "style",
329
+ "type": "CSSProperties",
330
+ "required": false,
331
+ "description": "Inline styles applied to the rendered element.",
332
+ "from": "@types/react"
333
+ }
334
+ ]
335
+ },
336
+ {
337
+ "name": "GanttRowLabel",
338
+ "props": [
339
+ {
340
+ "name": "children",
341
+ "type": "ReactNode",
342
+ "required": false,
343
+ "description": "Content rendered inside the part.",
344
+ "from": "@types/react"
345
+ },
346
+ {
347
+ "name": "className",
348
+ "type": "string",
349
+ "required": false,
350
+ "description": "Extra classes merged into the part's own classes with `cn`.",
351
+ "from": "@types/react"
352
+ },
353
+ {
354
+ "name": "style",
355
+ "type": "CSSProperties",
356
+ "required": false,
357
+ "description": "Inline styles applied to the rendered element.",
358
+ "from": "@types/react"
359
+ }
360
+ ]
361
+ },
362
+ {
363
+ "name": "GanttRowTrack",
364
+ "props": [
365
+ {
366
+ "name": "children",
367
+ "type": "ReactNode",
368
+ "required": false,
369
+ "description": "Content rendered inside the part.",
370
+ "from": "@types/react"
371
+ },
372
+ {
373
+ "name": "className",
374
+ "type": "string",
375
+ "required": false,
376
+ "description": "Extra classes merged into the part's own classes with `cn`.",
377
+ "from": "@types/react"
378
+ },
379
+ {
380
+ "name": "style",
381
+ "type": "CSSProperties",
382
+ "required": false,
383
+ "description": "Inline styles applied to the rendered element.",
384
+ "from": "@types/react"
385
+ }
386
+ ]
387
+ },
388
+ {
389
+ "name": "GanttBar",
390
+ "props": [
391
+ {
392
+ "name": "end",
393
+ "type": "Date",
394
+ "required": true,
395
+ "description": "Bar end (a whole day).",
396
+ "from": "arkcn"
397
+ },
398
+ {
399
+ "name": "start",
400
+ "type": "Date",
401
+ "required": true,
402
+ "description": "Bar start (a whole day).",
403
+ "from": "arkcn"
404
+ },
405
+ {
406
+ "name": "value",
407
+ "type": "string",
408
+ "required": true,
409
+ "description": "The controlled value.",
410
+ "from": "arkcn"
411
+ },
412
+ {
413
+ "name": "progress",
414
+ "type": "number",
415
+ "required": false,
416
+ "description": "Completion from 0 to 100, drawn as the filled part of the bar.",
417
+ "from": "arkcn"
418
+ },
419
+ {
420
+ "name": "children",
421
+ "type": "ReactNode",
422
+ "required": false,
423
+ "description": "Content rendered inside the part.",
424
+ "from": "@types/react"
425
+ },
426
+ {
427
+ "name": "className",
428
+ "type": "string",
429
+ "required": false,
430
+ "description": "Extra classes merged into the part's own classes with `cn`.",
431
+ "from": "@types/react"
432
+ },
433
+ {
434
+ "name": "style",
435
+ "type": "CSSProperties",
436
+ "required": false,
437
+ "description": "Inline styles applied to the rendered element.",
438
+ "from": "@types/react"
439
+ }
440
+ ]
441
+ },
442
+ {
443
+ "name": "GanttBarLabel",
444
+ "props": [
445
+ {
446
+ "name": "children",
447
+ "type": "ReactNode",
448
+ "required": false,
449
+ "description": "Content rendered inside the part.",
450
+ "from": "@types/react"
451
+ },
452
+ {
453
+ "name": "className",
454
+ "type": "string",
455
+ "required": false,
456
+ "description": "Extra classes merged into the part's own classes with `cn`.",
457
+ "from": "@types/react"
458
+ },
459
+ {
460
+ "name": "style",
461
+ "type": "CSSProperties",
462
+ "required": false,
463
+ "description": "Inline styles applied to the rendered element.",
464
+ "from": "@types/react"
465
+ }
466
+ ]
467
+ },
468
+ {
469
+ "name": "GanttBarResizeHandle",
470
+ "props": [
471
+ {
472
+ "name": "side",
473
+ "type": "\"end\" | \"start\"",
474
+ "required": true,
475
+ "description": "Which side of the anchor or container the part sits on.",
476
+ "from": "arkcn"
477
+ },
478
+ {
479
+ "name": "children",
480
+ "type": "ReactNode",
481
+ "required": false,
482
+ "description": "Content rendered inside the part.",
483
+ "from": "@types/react"
484
+ },
485
+ {
486
+ "name": "className",
487
+ "type": "string",
488
+ "required": false,
489
+ "description": "Extra classes merged into the part's own classes with `cn`.",
490
+ "from": "@types/react"
491
+ },
492
+ {
493
+ "name": "style",
494
+ "type": "CSSProperties",
495
+ "required": false,
496
+ "description": "Inline styles applied to the rendered element.",
497
+ "from": "@types/react"
498
+ }
499
+ ]
500
+ },
501
+ {
502
+ "name": "GanttMilestone",
503
+ "props": [
504
+ {
505
+ "name": "date",
506
+ "type": "Date",
507
+ "required": true,
508
+ "description": "The date this part represents.",
509
+ "from": "arkcn"
510
+ },
511
+ {
512
+ "name": "value",
513
+ "type": "string",
514
+ "required": true,
515
+ "description": "The controlled value.",
516
+ "from": "arkcn"
517
+ },
518
+ {
519
+ "name": "children",
520
+ "type": "ReactNode",
521
+ "required": false,
522
+ "description": "Content rendered inside the part.",
523
+ "from": "@types/react"
524
+ },
525
+ {
526
+ "name": "className",
527
+ "type": "string",
528
+ "required": false,
529
+ "description": "Extra classes merged into the part's own classes with `cn`.",
530
+ "from": "@types/react"
531
+ },
532
+ {
533
+ "name": "style",
534
+ "type": "CSSProperties",
535
+ "required": false,
536
+ "description": "Inline styles applied to the rendered element.",
537
+ "from": "@types/react"
538
+ }
539
+ ]
540
+ },
541
+ {
542
+ "name": "GanttDependencies",
543
+ "props": [
544
+ {
545
+ "name": "links",
546
+ "type": "{ from: string; to: string; }[]",
547
+ "required": true,
548
+ "description": "Dependencies as `{ from, to }` bar ids, drawn as arrows.",
549
+ "from": "arkcn"
550
+ },
551
+ {
552
+ "name": "children",
553
+ "type": "ReactNode",
554
+ "required": false,
555
+ "description": "Content rendered inside the part.",
556
+ "from": "@types/react"
557
+ },
558
+ {
559
+ "name": "className",
560
+ "type": "string",
561
+ "required": false,
562
+ "description": "Extra classes merged into the part's own classes with `cn`.",
563
+ "from": "@types/react"
564
+ },
565
+ {
566
+ "name": "style",
567
+ "type": "CSSProperties",
568
+ "required": false,
569
+ "description": "Inline styles applied to the rendered element.",
570
+ "from": "@types/react"
571
+ }
572
+ ]
573
+ },
574
+ {
575
+ "name": "GanttToday",
576
+ "props": [
577
+ {
578
+ "name": "date",
579
+ "type": "Date",
580
+ "required": false,
581
+ "default": "new Date()",
582
+ "description": "The date this part represents.",
583
+ "from": "arkcn"
584
+ },
585
+ {
586
+ "name": "children",
587
+ "type": "ReactNode",
588
+ "required": false,
589
+ "description": "Content rendered inside the part.",
590
+ "from": "@types/react"
591
+ },
592
+ {
593
+ "name": "className",
594
+ "type": "string",
595
+ "required": false,
596
+ "description": "Extra classes merged into the part's own classes with `cn`.",
597
+ "from": "@types/react"
598
+ },
599
+ {
600
+ "name": "style",
601
+ "type": "CSSProperties",
602
+ "required": false,
603
+ "description": "Inline styles applied to the rendered element.",
604
+ "from": "@types/react"
605
+ }
606
+ ]
607
+ },
608
+ {
609
+ "name": "GanttEmpty",
610
+ "props": [
611
+ {
612
+ "name": "children",
613
+ "type": "ReactNode",
614
+ "required": false,
615
+ "description": "Content rendered inside the part.",
616
+ "from": "@types/react"
617
+ },
618
+ {
619
+ "name": "className",
620
+ "type": "string",
621
+ "required": false,
622
+ "description": "Extra classes merged into the part's own classes with `cn`.",
623
+ "from": "@types/react"
624
+ },
625
+ {
626
+ "name": "style",
627
+ "type": "CSSProperties",
628
+ "required": false,
629
+ "description": "Inline styles applied to the rendered element.",
630
+ "from": "@types/react"
631
+ }
632
+ ]
633
+ },
634
+ {
635
+ "name": "GanttControls",
636
+ "props": [
637
+ {
638
+ "name": "children",
639
+ "type": "ReactNode",
640
+ "required": false,
641
+ "description": "Content rendered inside the part.",
642
+ "from": "@types/react"
643
+ },
644
+ {
645
+ "name": "className",
646
+ "type": "string",
647
+ "required": false,
648
+ "description": "Extra classes merged into the part's own classes with `cn`.",
649
+ "from": "@types/react"
650
+ },
651
+ {
652
+ "name": "style",
653
+ "type": "CSSProperties",
654
+ "required": false,
655
+ "description": "Inline styles applied to the rendered element.",
656
+ "from": "@types/react"
657
+ }
658
+ ]
659
+ },
660
+ {
661
+ "name": "GanttZoomInTrigger",
662
+ "props": [
663
+ {
664
+ "name": "asChild",
665
+ "type": "boolean",
666
+ "required": false,
667
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
668
+ "from": "arkcn"
669
+ },
670
+ {
671
+ "name": "size",
672
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
673
+ "required": false,
674
+ "description": "Size variant; sets a `data-size` attribute.",
675
+ "from": "arkcn"
676
+ },
677
+ {
678
+ "name": "variant",
679
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
680
+ "required": false,
681
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
682
+ "from": "arkcn"
683
+ },
684
+ {
685
+ "name": "children",
686
+ "type": "ReactNode",
687
+ "required": false,
688
+ "description": "Content rendered inside the part.",
689
+ "from": "@types/react"
690
+ },
691
+ {
692
+ "name": "className",
693
+ "type": "string",
694
+ "required": false,
695
+ "description": "Extra classes merged into the part's own classes with `cn`.",
696
+ "from": "@types/react"
697
+ },
698
+ {
699
+ "name": "style",
700
+ "type": "CSSProperties",
701
+ "required": false,
702
+ "description": "Inline styles applied to the rendered element.",
703
+ "from": "@types/react"
704
+ }
705
+ ]
706
+ },
707
+ {
708
+ "name": "GanttZoomOutTrigger",
709
+ "props": [
710
+ {
711
+ "name": "asChild",
712
+ "type": "boolean",
713
+ "required": false,
714
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
715
+ "from": "arkcn"
716
+ },
717
+ {
718
+ "name": "size",
719
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
720
+ "required": false,
721
+ "description": "Size variant; sets a `data-size` attribute.",
722
+ "from": "arkcn"
723
+ },
724
+ {
725
+ "name": "variant",
726
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
727
+ "required": false,
728
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
729
+ "from": "arkcn"
730
+ },
731
+ {
732
+ "name": "children",
733
+ "type": "ReactNode",
734
+ "required": false,
735
+ "description": "Content rendered inside the part.",
736
+ "from": "@types/react"
737
+ },
738
+ {
739
+ "name": "className",
740
+ "type": "string",
741
+ "required": false,
742
+ "description": "Extra classes merged into the part's own classes with `cn`.",
743
+ "from": "@types/react"
744
+ },
745
+ {
746
+ "name": "style",
747
+ "type": "CSSProperties",
748
+ "required": false,
749
+ "description": "Inline styles applied to the rendered element.",
750
+ "from": "@types/react"
751
+ }
752
+ ]
753
+ },
754
+ {
755
+ "name": "GanttTodayTrigger",
756
+ "props": [
757
+ {
758
+ "name": "asChild",
759
+ "type": "boolean",
760
+ "required": false,
761
+ "description": "Render the child element instead of the default one, merging props and behaviour onto it.",
762
+ "from": "arkcn"
763
+ },
764
+ {
765
+ "name": "size",
766
+ "type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
767
+ "required": false,
768
+ "description": "Size variant; sets a `data-size` attribute.",
769
+ "from": "arkcn"
770
+ },
771
+ {
772
+ "name": "variant",
773
+ "type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
774
+ "required": false,
775
+ "description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
776
+ "from": "arkcn"
777
+ },
778
+ {
779
+ "name": "children",
780
+ "type": "ReactNode",
781
+ "required": false,
782
+ "description": "Content rendered inside the part.",
783
+ "from": "@types/react"
784
+ },
785
+ {
786
+ "name": "className",
787
+ "type": "string",
788
+ "required": false,
789
+ "description": "Extra classes merged into the part's own classes with `cn`.",
790
+ "from": "@types/react"
791
+ },
792
+ {
793
+ "name": "style",
794
+ "type": "CSSProperties",
795
+ "required": false,
796
+ "description": "Inline styles applied to the rendered element.",
797
+ "from": "@types/react"
798
+ }
799
+ ]
800
+ }
801
+ ],
30
802
  "dependencies": {
31
803
  "date-fns": "^4.4.0",
32
804
  "lucide-react": "^1.41.0"
@@ -40,7 +812,7 @@
40
812
  "files": [
41
813
  {
42
814
  "path": "components/ui/gantt.tsx",
43
- "content": "import * as React from \"react\"\nimport {\n addDays,\n differenceInCalendarDays,\n eachDayOfInterval,\n eachMonthOfInterval,\n eachWeekOfInterval,\n endOfMonth,\n endOfWeek,\n format,\n isSameDay,\n isWeekend,\n startOfDay,\n startOfMonth,\n startOfWeek,\n} from \"date-fns\"\nimport { CalendarIcon, MinusIcon, PlusIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Gantt — a compositional, data-agnostic timeline. The consumer owns rows,\n * bars and dependencies and renders them with the parts; the root owns only\n * the time scale (`start`/`end`, `dayWidth`, header `scale`), the drag/resize\n * interaction and keyboard moves, and reports `onBarChange({ id, start, end })`.\n * Dates snap to whole days.\n *\n * Anatomy:\n * Gantt { start, end, dayWidth, scale, onBarChange }\n * GanttControls > GanttZoomOutTrigger / GanttZoomInTrigger / GanttTodayTrigger\n * GanttViewport single scroll container (x and y)\n * GanttHeader sticky two-tier date header (+ GanttHeaderCorner over the sidebar)\n * GanttBody\n * GanttRow value=… sidebar cell (GanttRowLabel, sticky) + track\n * GanttBar value=… start end progress > GanttBarLabel, GanttBarResizeHandle side=…\n * GanttMilestone value=… date\n * GanttDependencies svg arrows between registered bars\n * GanttToday vertical marker\n * GanttEmpty\n *\n * Keyboard on a bar: Space/Enter picks up, ←/→ move a day, Shift+←/→ change\n * the end, Alt+←/→ change the start, Space/Enter drops, Escape cancels.\n */\n\ntype GanttScale = \"day\" | \"week\" | \"month\"\ntype GanttBarChange = { id: string; start: Date; end: Date }\ntype BarRegistration = { id: string; el: HTMLElement | null; start: Date; end: Date; milestone?: boolean }\n\n/** Registered bars, outside React state so registering never re-renders the root. */\nfunction createBarStore() {\n const bars = new Map<string, BarRegistration>()\n const listeners = new Set<() => void>()\n let version = 0\n const notify = () => {\n version++\n listeners.forEach((l) => l())\n }\n return {\n bars,\n subscribe: (l: () => void) => {\n listeners.add(l)\n return () => {\n listeners.delete(l)\n }\n },\n getVersion: () => version,\n register(bar: BarRegistration) {\n bars.set(bar.id, bar)\n notify()\n return () => {\n if (bars.get(bar.id) === bar) bars.delete(bar.id)\n notify()\n }\n },\n }\n}\ntype BarStore = ReturnType<typeof createBarStore>\n\ntype Interaction = {\n id: string\n type: \"move\" | \"resize-start\" | \"resize-end\"\n start: Date\n end: Date\n}\n\ntype GanttContextValue = {\n start: Date\n end: Date\n days: number\n dayWidth: number\n rowHeight: number\n sidebarWidth: number\n scale: GanttScale\n editable: boolean\n interaction: Interaction | null\n grabbed: string | null\n viewportRef: React.RefObject<HTMLDivElement | null>\n xOf: (date: Date) => number\n dateAt: (x: number) => Date\n registerBar: (bar: BarRegistration) => () => void\n barStore: BarStore\n startInteraction: (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => void\n grab: (id: string | null) => void\n cancelGrab: () => void\n nudge: (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => void\n zoomIn: () => void\n zoomOut: () => void\n scrollToDate: (date: Date, options?: { behavior?: ScrollBehavior }) => void\n announce: (message: string) => void\n}\n\nconst GanttContext = React.createContext<GanttContextValue | null>(null)\nconst RowContext = React.createContext<{ value: string } | null>(null)\n\nfunction useGantt() {\n const ctx = React.useContext(GanttContext)\n if (!ctx) throw new Error(\"Gantt parts must be used within <Gantt>\")\n return ctx\n}\nfunction useGanttRow() {\n const ctx = React.useContext(RowContext)\n if (!ctx) throw new Error(\"Row parts must be used within <GanttRow>\")\n return ctx\n}\n\nconst clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction Gantt({\n start: startProp,\n end: endProp,\n dayWidth: dayWidthProp,\n defaultDayWidth = 40,\n onDayWidthChange,\n minDayWidth = 8,\n maxDayWidth = 160,\n scale: scaleProp,\n rowHeight = 40,\n sidebarWidth = 240,\n editable = true,\n onBarChange,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n start: Date\n end: Date\n /** Pixels per day; controllable. Zooming changes it. */\n dayWidth?: number\n defaultDayWidth?: number\n onDayWidthChange?: (dayWidth: number) => void\n minDayWidth?: number\n maxDayWidth?: number\n /** Header tiers. Defaults from `dayWidth`: month below 16px, week below 32px, else day. */\n scale?: GanttScale\n rowHeight?: number\n sidebarWidth?: number\n editable?: boolean\n onBarChange?: (change: GanttBarChange) => void\n}) {\n const viewportRef = React.useRef<HTMLDivElement>(null)\n const start = React.useMemo(() => startOfDay(startProp), [startProp])\n const end = React.useMemo(() => startOfDay(endProp), [endProp])\n const days = Math.max(1, differenceInCalendarDays(end, start) + 1)\n\n const [dayWidth, setDayWidthState] = useControllable(dayWidthProp, defaultDayWidth, onDayWidthChange)\n const setDayWidth = React.useCallback(\n (next: number) => setDayWidthState(clamp(next, minDayWidth, maxDayWidth)),\n [setDayWidthState, minDayWidth, maxDayWidth]\n )\n const scale: GanttScale = scaleProp ?? (dayWidth < 16 ? \"month\" : dayWidth < 32 ? \"week\" : \"day\")\n\n const xOf = React.useCallback(\n (date: Date) => differenceInCalendarDays(startOfDay(date), start) * dayWidth,\n [start, dayWidth]\n )\n const dateAt = React.useCallback((x: number) => addDays(start, Math.round(x / dayWidth)), [start, dayWidth])\n\n /* Bar registry for dependency lines */\n const barStore = React.useMemo(() => createBarStore(), [])\n const registerBar = React.useCallback((bar: BarRegistration) => barStore.register(bar), [barStore])\n\n /* Interaction */\n const [interaction, setInteraction] = React.useState<Interaction | null>(null)\n const [grabbed, setGrabbed] = React.useState<string | null>(null)\n const { message: announcement, announce } = useLiveRegion()\n const onBarChangeRef = React.useRef(onBarChange)\n React.useEffect(() => {\n onBarChangeRef.current = onBarChange\n }, [onBarChange])\n const grabOrigin = React.useRef<{ id: string; start: Date; end: Date } | null>(null)\n\n const startInteraction = React.useCallback(\n (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => {\n if (!editable) return\n event.preventDefault()\n event.stopPropagation()\n const origin = event.clientX\n let current: Interaction = { id: bar.id, type, start: bar.start, end: bar.end }\n setInteraction(current)\n const move = (e: PointerEvent) => {\n const deltaDays = Math.round((e.clientX - origin) / dayWidth)\n const duration = differenceInCalendarDays(bar.end, bar.start)\n if (type === \"move\") {\n current = { id: bar.id, type, start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n } else if (type === \"resize-end\") {\n current = { id: bar.id, type, start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n } else {\n current = { id: bar.id, type, start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n }\n setInteraction(current)\n }\n const up = () => {\n window.removeEventListener(\"pointermove\", move)\n window.removeEventListener(\"pointerup\", up)\n window.removeEventListener(\"pointercancel\", up)\n setInteraction(null)\n if (!isSameDay(current.start, bar.start) || !isSameDay(current.end, bar.end)) {\n onBarChangeRef.current?.({ id: bar.id, start: current.start, end: current.end })\n }\n }\n window.addEventListener(\"pointermove\", move)\n window.addEventListener(\"pointerup\", up)\n window.addEventListener(\"pointercancel\", up)\n },\n [editable, dayWidth]\n )\n\n const grab = React.useCallback(\n (id: string | null) => {\n if (id) {\n const bar = barStore.bars.get(id)\n grabOrigin.current = bar ? { id, start: bar.start, end: bar.end } : null\n setGrabbed(id)\n announce(\n \"Picked up. Arrow keys move by a day, Shift changes the end, Alt the start. Space drops, Escape cancels.\"\n )\n } else {\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Dropped.\")\n }\n },\n [barStore, announce]\n )\n const cancelGrab = React.useCallback(() => {\n const origin = grabOrigin.current\n if (origin) onBarChangeRef.current?.(origin)\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Cancelled.\")\n }, [announce])\n\n const nudge = React.useCallback(\n (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => {\n const duration = differenceInCalendarDays(bar.end, bar.start)\n const next =\n part === \"both\"\n ? { start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n : part === \"end\"\n ? { start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n : { start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n onBarChangeRef.current?.({ id: bar.id, ...next })\n announce(`${format(next.start, \"MMM d\")} to ${format(next.end, \"MMM d\")}.`)\n },\n [announce]\n )\n\n const scrollToDate = React.useCallback(\n (date: Date, options?: { behavior?: ScrollBehavior }) => {\n const el = viewportRef.current\n if (!el) return\n const x = xOf(date)\n el.scrollTo({\n left: Math.max(0, x - (el.clientWidth - sidebarWidth) / 2),\n behavior: options?.behavior ?? \"smooth\",\n })\n },\n [xOf, sidebarWidth]\n )\n\n /** Zoom keeping the date at the viewport centre in place. */\n const zoomBy = React.useCallback(\n (factor: number) => {\n const el = viewportRef.current\n const centreX = el ? el.scrollLeft + (el.clientWidth - sidebarWidth) / 2 : 0\n const centreDays = centreX / dayWidth\n const next = clamp(dayWidth * factor, minDayWidth, maxDayWidth)\n setDayWidth(next)\n requestAnimationFrame(() => {\n if (el) el.scrollLeft = Math.max(0, centreDays * next - (el.clientWidth - sidebarWidth) / 2)\n })\n },\n [dayWidth, minDayWidth, maxDayWidth, setDayWidth, sidebarWidth]\n )\n const zoomIn = React.useCallback(() => zoomBy(1.5), [zoomBy])\n const zoomOut = React.useCallback(() => zoomBy(1 / 1.5), [zoomBy])\n\n const ctx = React.useMemo<GanttContextValue>(\n () => ({\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n viewportRef,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n }),\n [\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n ]\n )\n\n return (\n <GanttContext.Provider value={ctx}>\n <div\n data-slot=\"gantt\"\n data-scale={scale}\n data-editable={editable ? \"\" : undefined}\n data-interacting={interaction?.type}\n className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}\n style={{ [\"--gantt-sidebar\" as string]: `${sidebarWidth}px`, [\"--gantt-row\" as string]: `${rowHeight}px` }}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"gantt-live-region\" message={announcement} />\n </div>\n </GanttContext.Provider>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Viewport, header, body\n * ------------------------------------------------------------------------- */\n\nfunction GanttViewport({ className, ...props }: React.ComponentProps<\"div\">) {\n const { viewportRef } = useGantt()\n return (\n <div\n ref={viewportRef}\n data-slot=\"gantt-viewport\"\n className={cn(\"relative min-h-0 flex-1 overflow-auto rounded-xl border bg-background text-sm\", className)}\n {...props}\n />\n )\n}\n\n/** Two-tier sticky header; the tiers depend on the scale. */\nfunction GanttHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, days, dayWidth, scale, sidebarWidth } = useGantt()\n const width = days * dayWidth\n const tiers = React.useMemo(() => {\n const clampX = (date: Date) => clamp(differenceInCalendarDays(date, start), 0, days)\n const span = (from: Date, to: Date) => ({\n left: clampX(from) * dayWidth,\n width: (clampX(addDays(to, 1)) - clampX(from)) * dayWidth,\n })\n const months = eachMonthOfInterval({ start, end }).map((m) => ({\n key: m.toISOString(),\n label: format(m, scale === \"month\" ? \"MMMM yyyy\" : \"MMM yyyy\"),\n ...span(startOfMonth(m), endOfMonth(m)),\n }))\n if (scale === \"day\") {\n const dayCells = eachDayOfInterval({ start, end }).map((d) => ({\n key: d.toISOString(),\n label: dayWidth >= 40 ? format(d, \"EEEEE d\") : format(d, \"d\"),\n weekend: isWeekend(d),\n today: isSameDay(d, new Date()),\n ...span(d, d),\n }))\n return { top: months, bottom: dayCells }\n }\n if (scale === \"week\") {\n const weeks = eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((w) => ({\n key: w.toISOString(),\n label: format(w, \"d MMM\"),\n weekend: false,\n today: false,\n ...span(startOfWeek(w, { weekStartsOn: 1 }), endOfWeek(w, { weekStartsOn: 1 })),\n }))\n return { top: months, bottom: weeks }\n }\n const years = Array.from(new Set(months.map((m) => m.label.slice(-4)))).map((y) => {\n const first = new Date(Number(y), 0, 1)\n const last = new Date(Number(y), 11, 31)\n return { key: y, label: y, ...span(first, last) }\n })\n return {\n top: years,\n bottom: months.map((m) => ({ ...m, label: format(new Date(m.key), \"MMM\"), weekend: false, today: false })),\n }\n }, [start, end, days, dayWidth, scale])\n\n return (\n <div\n data-slot=\"gantt-header\"\n className={cn(\"sticky top-0 z-30 flex bg-muted text-xs text-muted-foreground\", className)}\n style={{ width: sidebarWidth + width }}\n {...props}\n >\n <GanttHeaderCorner />\n <div className=\"relative shrink-0\" style={{ width }}>\n <div className=\"flex h-7 border-b\">\n {tiers.top.map((cell) => (\n <div\n key={cell.key}\n className=\"absolute inset-y-0 flex items-center truncate border-e px-2 font-medium\"\n style={{ left: cell.left, width: cell.width }}\n >\n {cell.label}\n </div>\n ))}\n </div>\n <div className=\"flex h-7 border-b\">\n {tiers.bottom.map((cell) => (\n <div\n key={cell.key}\n data-weekend={cell.weekend ? \"\" : undefined}\n data-today={cell.today ? \"\" : undefined}\n 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\"\n style={{ left: cell.left, width: cell.width, top: 28 }}\n >\n {cell.width >= 18 ? cell.label : \"\"}\n </div>\n ))}\n </div>\n </div>\n </div>\n )\n}\n\n/** The sticky cell above the sidebar; put a title or a search box in it. */\nfunction GanttHeaderCorner({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-header-corner\"\n className={cn(\n \"sticky left-0 z-40 flex h-14 shrink-0 items-end border-e border-b bg-muted px-3 pb-2 font-medium\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\nfunction GanttBody({ className, style, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth, sidebarWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-body\"\n className={cn(\"relative\", className)}\n style={{ width: sidebarWidth + days * dayWidth, ...style }}\n {...props}\n />\n )\n}\n\n/** Vertical day/week lines and weekend shading behind the bars. */\nfunction GanttGridLines({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, dayWidth, scale, sidebarWidth } = useGantt()\n const cells = React.useMemo(() => {\n if (scale === \"month\")\n return eachMonthOfInterval({ start, end }).map((d) => ({ date: startOfMonth(d), weekend: false }))\n if (scale === \"week\")\n return eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((d) => ({ date: d, weekend: false }))\n return eachDayOfInterval({ start, end }).map((d) => ({ date: d, weekend: isWeekend(d) }))\n }, [start, end, scale])\n return (\n <div\n data-slot=\"gantt-grid-lines\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0\", className)}\n style={{ left: sidebarWidth, right: 0 }}\n {...props}\n >\n {cells.map((cell) => (\n <div\n key={cell.date.toISOString()}\n data-weekend={cell.weekend ? \"\" : undefined}\n className=\"absolute inset-y-0 border-e border-border/60 data-weekend:bg-foreground/2.5\"\n style={{\n left: differenceInCalendarDays(cell.date, start) * dayWidth,\n width: scale === \"day\" ? dayWidth : undefined,\n }}\n />\n ))}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Rows\n * ------------------------------------------------------------------------- */\n\n/** Groups the rows; keep bars inside so dependency lines can find them. */\nfunction GanttRows({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-rows\" className={cn(\"relative\", className)} {...props} />\n}\n\nfunction GanttRow({ value, className, children, ...props }: React.ComponentProps<\"div\"> & { value: string }) {\n const { rowHeight } = useGantt()\n const ctx = React.useMemo(() => ({ value }), [value])\n return (\n <RowContext.Provider value={ctx}>\n <div\n data-slot=\"gantt-row\"\n data-value={value}\n className={cn(\"group/row flex border-b hover:bg-muted/30\", className)}\n style={{ height: rowHeight }}\n {...props}\n >\n {children}\n </div>\n </RowContext.Provider>\n )\n}\n\n/** Sticky sidebar cell for the row. */\nfunction GanttRowLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-row-label\"\n className={cn(\n \"sticky left-0 z-20 flex shrink-0 items-center gap-2 border-e bg-background px-3 text-sm transition-colors\",\n \"group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_30%,var(--color-background))]\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\n/** The timeline part of a row; bars are positioned inside it. */\nfunction GanttRowTrack({ className, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-row-track\"\n className={cn(\"relative shrink-0\", className)}\n style={{ width: days * dayWidth }}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Bars\n * ------------------------------------------------------------------------- */\n\nconst BarContext = React.createContext<BarRegistration | null>(null)\n\nfunction GanttBar({\n value,\n start,\n end,\n progress,\n className,\n style,\n children,\n onPointerDown,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; start: Date; end: Date; progress?: number }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const live = gantt.interaction?.id === value ? gantt.interaction : null\n // Key on timestamps: callers often pass fresh Date objects every render.\n const startMs = (live ? live.start : startOfDay(start)).getTime()\n const endMs = (live ? live.end : startOfDay(end)).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(startMs), end: new Date(endMs) }),\n [value, startMs, endMs]\n )\n const shownStart = registration.start\n const shownEnd = registration.end\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n const left = gantt.xOf(shownStart)\n const width = Math.max(gantt.dayWidth, (differenceInCalendarDays(shownEnd, shownStart) + 1) * gantt.dayWidth)\n const isGrabbed = gantt.grabbed === value\n\n return (\n <BarContext.Provider value={registration}>\n <div\n ref={ref}\n data-slot=\"gantt-bar\"\n data-value={value}\n data-dragging={live ? live.type : undefined}\n data-grabbed={isGrabbed ? \"\" : undefined}\n tabIndex={gantt.editable ? 0 : undefined}\n aria-pressed={gantt.editable ? isGrabbed : undefined}\n aria-label={`${format(shownStart, \"MMM d\")} to ${format(shownEnd, \"MMM d\")}`}\n className={cn(\n \"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\",\n gantt.editable && \"cursor-grab active:cursor-grabbing\",\n \"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\",\n className\n )}\n style={{ left, width, ...style }}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n if ((event.target as HTMLElement).closest(\"[data-slot=gantt-bar-resize-handle], button, a, input\")) return\n gantt.startInteraction(registration, \"move\", event)\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || !gantt.editable || event.target !== event.currentTarget) return\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault()\n gantt.grab(isGrabbed ? null : value)\n return\n }\n if (!isGrabbed) return\n if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n event.preventDefault()\n const delta = event.key === \"ArrowLeft\" ? -1 : 1\n gantt.nudge(registration, delta, event.shiftKey ? \"end\" : event.altKey ? \"start\" : \"both\")\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n gantt.cancelGrab()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (isGrabbed && !event.currentTarget.contains(event.relatedTarget as Node | null)) gantt.grab(null)\n }}\n {...props}\n >\n {progress !== undefined && (\n <div\n data-slot=\"gantt-bar-progress\"\n aria-hidden\n className=\"absolute inset-y-0 left-0 bg-primary/30\"\n style={{ width: `${clamp(progress, 0, 1) * 100}%` }}\n />\n )}\n {children}\n </div>\n </BarContext.Provider>\n )\n}\n\nfunction GanttBarLabel({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"gantt-bar-label\" className={cn(\"relative z-10 truncate px-2 font-medium\", className)} {...props} />\n )\n}\n\nfunction GanttBarResizeHandle({\n side,\n className,\n onPointerDown,\n ...props\n}: React.ComponentProps<\"div\"> & { side: \"start\" | \"end\" }) {\n const gantt = useGantt()\n const bar = React.useContext(BarContext)\n if (!gantt.editable || !bar) return null\n return (\n <div\n data-slot=\"gantt-bar-resize-handle\"\n data-side={side}\n role=\"separator\"\n aria-orientation=\"vertical\"\n className={cn(\n \"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\",\n side === \"start\" ? \"left-0 rounded-l-md\" : \"right-0 rounded-r-md\",\n \"after:absolute after:inset-y-1.5 after:w-0.5 after:rounded-full after:bg-primary/60\",\n side === \"start\" ? \"after:left-0.5\" : \"after:right-0.5\",\n className\n )}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n gantt.startInteraction(bar, side === \"start\" ? \"resize-start\" : \"resize-end\", event)\n }}\n {...props}\n />\n )\n}\n\n/** A single-day marker rendered as a diamond. */\nfunction GanttMilestone({\n value,\n date,\n className,\n style,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; date: Date }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const dayMs = startOfDay(date).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(dayMs), end: new Date(dayMs), milestone: true }),\n [value, dayMs]\n )\n const day = registration.start\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n return (\n <div\n ref={ref}\n data-slot=\"gantt-milestone\"\n data-value={value}\n aria-label={format(day, \"MMM d\")}\n className={cn(\"absolute top-1/2 z-10 flex -translate-y-1/2 items-center gap-2\", className)}\n style={{ left: gantt.xOf(day) + gantt.dayWidth / 2 - 8, ...style }}\n {...props}\n >\n <span aria-hidden className=\"block size-4 rotate-45 rounded-sm bg-foreground\" />\n {children && <span className=\"text-xs font-medium whitespace-nowrap\">{children}</span>}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Dependencies, today, empty\n * ------------------------------------------------------------------------- */\n\n/** Arrows from the end of `from` to the start of `to`, measured from the registered bar elements. */\nfunction GanttDependencies({\n links,\n className,\n ...props\n}: React.ComponentProps<\"svg\"> & { links: { from: string; to: string }[] }) {\n const { barStore, dayWidth, xOf, sidebarWidth, rowHeight, days } = useGantt()\n React.useSyncExternalStore(barStore.subscribe, barStore.getVersion, barStore.getVersion)\n const svgRef = React.useRef<SVGSVGElement>(null)\n const id = React.useId()\n // Row centres come from the DOM so collapsed groups and custom row order need no bookkeeping.\n const origin = svgRef.current?.getBoundingClientRect()\n const centreY = (el: HTMLElement | null) => {\n if (!el || !origin) return null\n const r = el.getBoundingClientRect()\n return r.top + r.height / 2 - origin.top\n }\n const paths = links\n .map((link) => {\n const from = barStore.bars.get(link.from)\n const to = barStore.bars.get(link.to)\n if (!from || !to) return null\n const y1 = centreY(from.el)\n const y2 = centreY(to.el)\n if (y1 === null || y2 === null) return null\n const x1 = xOf(from.end) + (from.milestone ? dayWidth / 2 + 8 : dayWidth)\n const x2 = xOf(to.start) + (to.milestone ? dayWidth / 2 - 8 : 0)\n const gap = 10\n const d =\n x2 - gap > x1\n ? `M ${x1} ${y1} H ${x1 + gap} V ${y2} H ${x2}`\n : `M ${x1} ${y1} H ${x1 + gap} V ${y1 + rowHeight / 2 - 4} H ${x2 - gap} V ${y2} H ${x2}`\n return { key: `${link.from}->${link.to}`, d }\n })\n .filter((p): p is { key: string; d: string } => !!p)\n return (\n <svg\n ref={svgRef}\n data-slot=\"gantt-dependencies\"\n aria-hidden\n className={cn(\"pointer-events-none absolute top-0 z-5 overflow-hidden text-muted-foreground\", className)}\n style={{ left: sidebarWidth, width: days * dayWidth, height: \"100%\" }}\n {...props}\n >\n <defs>\n <marker id={`${id}-arrow`} viewBox=\"0 0 6 6\" refX=\"5\" refY=\"3\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto\">\n <path d=\"M 0 0 L 6 3 L 0 6 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n {paths.map((p) => (\n <path key={p.key} d={p.d} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} markerEnd={`url(#${id}-arrow)`} />\n ))}\n </svg>\n )\n}\n\nfunction GanttToday({ className, date = new Date(), ...props }: React.ComponentProps<\"div\"> & { date?: Date }) {\n const { xOf, dayWidth, sidebarWidth, start, end } = useGantt()\n const day = startOfDay(date)\n if (day < start || day > end) return null\n return (\n <div\n data-slot=\"gantt-today\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0 w-px bg-destructive\", className)}\n style={{ left: sidebarWidth + xOf(day) + dayWidth / 2 }}\n {...props}\n >\n <span className=\"absolute top-0 left-1 rounded-b bg-destructive px-1 text-[10px]/4 text-white\">Today</span>\n </div>\n )\n}\n\nfunction GanttEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-empty\"\n className={cn(\"flex h-32 items-center justify-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Controls\n * ------------------------------------------------------------------------- */\n\nfunction GanttControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-controls\" className={cn(\"flex items-center gap-1\", className)} {...props} />\n}\n\nfunction GanttZoomInTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomIn } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-in-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom in\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomIn()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <PlusIcon />)}\n </Button>\n )\n}\n\nfunction GanttZoomOutTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomOut } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-out-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom out\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomOut()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <MinusIcon />)}\n </Button>\n )\n}\n\nfunction GanttTodayTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { scrollToDate } = useGantt()\n return (\n <Button\n data-slot=\"gantt-today-trigger\"\n variant=\"outline\"\n size=\"sm\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) scrollToDate(new Date())\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <CalendarIcon /> Today\n </>\n ))}\n </Button>\n )\n}\n\nexport {\n Gantt,\n GanttViewport,\n GanttHeader,\n GanttHeaderCorner,\n GanttBody,\n GanttGridLines,\n GanttRows,\n GanttRow,\n GanttRowLabel,\n GanttRowTrack,\n GanttBar,\n GanttBarLabel,\n GanttBarResizeHandle,\n GanttMilestone,\n GanttDependencies,\n GanttToday,\n GanttEmpty,\n GanttControls,\n GanttZoomInTrigger,\n GanttZoomOutTrigger,\n GanttTodayTrigger,\n useGantt,\n useGanttRow,\n type GanttScale,\n type GanttBarChange,\n}\n"
815
+ "content": "import * as React from \"react\"\nimport {\n addDays,\n differenceInCalendarDays,\n eachDayOfInterval,\n eachMonthOfInterval,\n eachWeekOfInterval,\n endOfMonth,\n endOfWeek,\n format,\n isSameDay,\n isWeekend,\n startOfDay,\n startOfMonth,\n startOfWeek,\n} from \"date-fns\"\nimport { CalendarIcon, MinusIcon, PlusIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Gantt — a compositional, data-agnostic timeline. The consumer owns rows,\n * bars and dependencies and renders them with the parts; the root owns only\n * the time scale (`start`/`end`, `dayWidth`, header `scale`), the drag/resize\n * interaction and keyboard moves, and reports `onBarChange({ id, start, end })`.\n * Dates snap to whole days.\n *\n * Anatomy:\n * Gantt { start, end, dayWidth, scale, onBarChange }\n * GanttControls > GanttZoomOutTrigger / GanttZoomInTrigger / GanttTodayTrigger\n * GanttViewport single scroll container (x and y)\n * GanttHeader sticky two-tier date header (+ GanttHeaderCorner over the sidebar)\n * GanttBody\n * GanttRow value=… sidebar cell (GanttRowLabel, sticky) + track\n * GanttBar value=… start end progress > GanttBarLabel, GanttBarResizeHandle side=…\n * GanttMilestone value=… date\n * GanttDependencies svg arrows between registered bars\n * GanttToday vertical marker\n * GanttEmpty\n *\n * Keyboard on a bar: Space/Enter picks up, ←/→ move a day, Shift+←/→ change\n * the end, Alt+←/→ change the start, Space/Enter drops, Escape cancels.\n */\n\ntype GanttScale = \"day\" | \"week\" | \"month\"\ntype GanttBarChange = { id: string; start: Date; end: Date }\ntype BarRegistration = { id: string; el: HTMLElement | null; start: Date; end: Date; milestone?: boolean }\n\n/** Registered bars, outside React state so registering never re-renders the root. */\nfunction createBarStore() {\n const bars = new Map<string, BarRegistration>()\n const listeners = new Set<() => void>()\n let version = 0\n const notify = () => {\n version++\n listeners.forEach((l) => l())\n }\n return {\n bars,\n subscribe: (l: () => void) => {\n listeners.add(l)\n return () => {\n listeners.delete(l)\n }\n },\n getVersion: () => version,\n register(bar: BarRegistration) {\n bars.set(bar.id, bar)\n notify()\n return () => {\n if (bars.get(bar.id) === bar) bars.delete(bar.id)\n notify()\n }\n },\n }\n}\ntype BarStore = ReturnType<typeof createBarStore>\n\ntype Interaction = {\n id: string\n type: \"move\" | \"resize-start\" | \"resize-end\"\n /** First date on the time scale. */\n start: Date\n /** Last date on the time scale. */\n end: Date\n}\n\ntype GanttContextValue = {\n /** First date on the time scale. */\n start: Date\n /** Last date on the time scale. */\n end: Date\n days: number\n dayWidth: number\n /** Height of every row in px. */\n rowHeight: number\n /** Width of the sticky label column in px. */\n sidebarWidth: number\n scale: GanttScale\n editable: boolean\n interaction: Interaction | null\n grabbed: string | null\n viewportRef: React.RefObject<HTMLDivElement | null>\n xOf: (date: Date) => number\n dateAt: (x: number) => Date\n registerBar: (bar: BarRegistration) => () => void\n barStore: BarStore\n startInteraction: (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => void\n grab: (id: string | null) => void\n cancelGrab: () => void\n nudge: (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => void\n zoomIn: () => void\n zoomOut: () => void\n scrollToDate: (date: Date, options?: { behavior?: ScrollBehavior }) => void\n announce: (message: string) => void\n}\n\nconst GanttContext = React.createContext<GanttContextValue | null>(null)\nconst RowContext = React.createContext<{ value: string } | null>(null)\n\nfunction useGantt() {\n const ctx = React.useContext(GanttContext)\n if (!ctx) throw new Error(\"Gantt parts must be used within <Gantt>\")\n return ctx\n}\nfunction useGanttRow() {\n const ctx = React.useContext(RowContext)\n if (!ctx) throw new Error(\"Row parts must be used within <GanttRow>\")\n return ctx\n}\n\nconst clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction Gantt({\n start: startProp,\n end: endProp,\n dayWidth: dayWidthProp,\n defaultDayWidth = 40,\n onDayWidthChange,\n minDayWidth = 8,\n maxDayWidth = 160,\n scale: scaleProp,\n rowHeight = 40,\n sidebarWidth = 240,\n editable = true,\n onBarChange,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n /** First date on the time scale. */\n start: Date\n /** Last date on the time scale. */\n end: Date\n /** Pixels per day; controllable. Zooming changes it. */\n dayWidth?: number\n /** Initial pixels per day when uncontrolled. */\n defaultDayWidth?: number\n /** Called when the zoom changes. */\n onDayWidthChange?: (dayWidth: number) => void\n /** Lower bound for zooming out. */\n minDayWidth?: number\n /** Upper bound for zooming in. */\n maxDayWidth?: number\n /** Header tiers. Defaults from `dayWidth`: month below 16px, week below 32px, else day. */\n scale?: GanttScale\n /** Height of every row in px. */\n rowHeight?: number\n /** Width of the sticky label column in px. */\n sidebarWidth?: number\n editable?: boolean\n /** Called with `{ id, start, end }` snapped to whole days after a move, resize, or keyboard change. */\n onBarChange?: (change: GanttBarChange) => void\n}) {\n const viewportRef = React.useRef<HTMLDivElement>(null)\n const start = React.useMemo(() => startOfDay(startProp), [startProp])\n const end = React.useMemo(() => startOfDay(endProp), [endProp])\n const days = Math.max(1, differenceInCalendarDays(end, start) + 1)\n\n const [dayWidth, setDayWidthState] = useControllable(dayWidthProp, defaultDayWidth, onDayWidthChange)\n const setDayWidth = React.useCallback(\n (next: number) => setDayWidthState(clamp(next, minDayWidth, maxDayWidth)),\n [setDayWidthState, minDayWidth, maxDayWidth]\n )\n const scale: GanttScale = scaleProp ?? (dayWidth < 16 ? \"month\" : dayWidth < 32 ? \"week\" : \"day\")\n\n const xOf = React.useCallback(\n (date: Date) => differenceInCalendarDays(startOfDay(date), start) * dayWidth,\n [start, dayWidth]\n )\n const dateAt = React.useCallback((x: number) => addDays(start, Math.round(x / dayWidth)), [start, dayWidth])\n\n /* Bar registry for dependency lines */\n const barStore = React.useMemo(() => createBarStore(), [])\n const registerBar = React.useCallback((bar: BarRegistration) => barStore.register(bar), [barStore])\n\n /* Interaction */\n const [interaction, setInteraction] = React.useState<Interaction | null>(null)\n const [grabbed, setGrabbed] = React.useState<string | null>(null)\n const { message: announcement, announce } = useLiveRegion()\n const onBarChangeRef = React.useRef(onBarChange)\n React.useEffect(() => {\n onBarChangeRef.current = onBarChange\n }, [onBarChange])\n const grabOrigin = React.useRef<{ id: string; start: Date; end: Date } | null>(null)\n\n const startInteraction = React.useCallback(\n (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => {\n if (!editable) return\n event.preventDefault()\n event.stopPropagation()\n const origin = event.clientX\n let current: Interaction = { id: bar.id, type, start: bar.start, end: bar.end }\n setInteraction(current)\n const move = (e: PointerEvent) => {\n const deltaDays = Math.round((e.clientX - origin) / dayWidth)\n const duration = differenceInCalendarDays(bar.end, bar.start)\n if (type === \"move\") {\n current = { id: bar.id, type, start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n } else if (type === \"resize-end\") {\n current = { id: bar.id, type, start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n } else {\n current = { id: bar.id, type, start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n }\n setInteraction(current)\n }\n const up = () => {\n window.removeEventListener(\"pointermove\", move)\n window.removeEventListener(\"pointerup\", up)\n window.removeEventListener(\"pointercancel\", up)\n setInteraction(null)\n if (!isSameDay(current.start, bar.start) || !isSameDay(current.end, bar.end)) {\n onBarChangeRef.current?.({ id: bar.id, start: current.start, end: current.end })\n }\n }\n window.addEventListener(\"pointermove\", move)\n window.addEventListener(\"pointerup\", up)\n window.addEventListener(\"pointercancel\", up)\n },\n [editable, dayWidth]\n )\n\n const grab = React.useCallback(\n (id: string | null) => {\n if (id) {\n const bar = barStore.bars.get(id)\n grabOrigin.current = bar ? { id, start: bar.start, end: bar.end } : null\n setGrabbed(id)\n announce(\n \"Picked up. Arrow keys move by a day, Shift changes the end, Alt the start. Space drops, Escape cancels.\"\n )\n } else {\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Dropped.\")\n }\n },\n [barStore, announce]\n )\n const cancelGrab = React.useCallback(() => {\n const origin = grabOrigin.current\n if (origin) onBarChangeRef.current?.(origin)\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Cancelled.\")\n }, [announce])\n\n const nudge = React.useCallback(\n (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => {\n const duration = differenceInCalendarDays(bar.end, bar.start)\n const next =\n part === \"both\"\n ? { start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n : part === \"end\"\n ? { start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n : { start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n onBarChangeRef.current?.({ id: bar.id, ...next })\n announce(`${format(next.start, \"MMM d\")} to ${format(next.end, \"MMM d\")}.`)\n },\n [announce]\n )\n\n const scrollToDate = React.useCallback(\n (date: Date, options?: { behavior?: ScrollBehavior }) => {\n const el = viewportRef.current\n if (!el) return\n const x = xOf(date)\n el.scrollTo({\n left: Math.max(0, x - (el.clientWidth - sidebarWidth) / 2),\n behavior: options?.behavior ?? \"smooth\",\n })\n },\n [xOf, sidebarWidth]\n )\n\n /** Zoom keeping the date at the viewport centre in place. */\n const zoomBy = React.useCallback(\n (factor: number) => {\n const el = viewportRef.current\n const centreX = el ? el.scrollLeft + (el.clientWidth - sidebarWidth) / 2 : 0\n const centreDays = centreX / dayWidth\n const next = clamp(dayWidth * factor, minDayWidth, maxDayWidth)\n setDayWidth(next)\n requestAnimationFrame(() => {\n if (el) el.scrollLeft = Math.max(0, centreDays * next - (el.clientWidth - sidebarWidth) / 2)\n })\n },\n [dayWidth, minDayWidth, maxDayWidth, setDayWidth, sidebarWidth]\n )\n const zoomIn = React.useCallback(() => zoomBy(1.5), [zoomBy])\n const zoomOut = React.useCallback(() => zoomBy(1 / 1.5), [zoomBy])\n\n const ctx = React.useMemo<GanttContextValue>(\n () => ({\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n viewportRef,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n }),\n [\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n ]\n )\n\n return (\n <GanttContext.Provider value={ctx}>\n <div\n data-slot=\"gantt\"\n data-scale={scale}\n data-editable={editable ? \"\" : undefined}\n data-interacting={interaction?.type}\n className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}\n style={{ [\"--gantt-sidebar\" as string]: `${sidebarWidth}px`, [\"--gantt-row\" as string]: `${rowHeight}px` }}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"gantt-live-region\" message={announcement} />\n </div>\n </GanttContext.Provider>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Viewport, header, body\n * ------------------------------------------------------------------------- */\n\nfunction GanttViewport({ className, ...props }: React.ComponentProps<\"div\">) {\n const { viewportRef } = useGantt()\n return (\n <div\n ref={viewportRef}\n data-slot=\"gantt-viewport\"\n className={cn(\"relative min-h-0 flex-1 overflow-auto rounded-xl border bg-background text-sm\", className)}\n {...props}\n />\n )\n}\n\n/** Two-tier sticky header; the tiers depend on the scale. */\nfunction GanttHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, days, dayWidth, scale, sidebarWidth } = useGantt()\n const width = days * dayWidth\n const tiers = React.useMemo(() => {\n const clampX = (date: Date) => clamp(differenceInCalendarDays(date, start), 0, days)\n const span = (from: Date, to: Date) => ({\n left: clampX(from) * dayWidth,\n width: (clampX(addDays(to, 1)) - clampX(from)) * dayWidth,\n })\n const months = eachMonthOfInterval({ start, end }).map((m) => ({\n key: m.toISOString(),\n label: format(m, scale === \"month\" ? \"MMMM yyyy\" : \"MMM yyyy\"),\n ...span(startOfMonth(m), endOfMonth(m)),\n }))\n if (scale === \"day\") {\n const dayCells = eachDayOfInterval({ start, end }).map((d) => ({\n key: d.toISOString(),\n label: dayWidth >= 40 ? format(d, \"EEEEE d\") : format(d, \"d\"),\n weekend: isWeekend(d),\n today: isSameDay(d, new Date()),\n ...span(d, d),\n }))\n return { top: months, bottom: dayCells }\n }\n if (scale === \"week\") {\n const weeks = eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((w) => ({\n key: w.toISOString(),\n label: format(w, \"d MMM\"),\n weekend: false,\n today: false,\n ...span(startOfWeek(w, { weekStartsOn: 1 }), endOfWeek(w, { weekStartsOn: 1 })),\n }))\n return { top: months, bottom: weeks }\n }\n const years = Array.from(new Set(months.map((m) => m.label.slice(-4)))).map((y) => {\n const first = new Date(Number(y), 0, 1)\n const last = new Date(Number(y), 11, 31)\n return { key: y, label: y, ...span(first, last) }\n })\n return {\n top: years,\n bottom: months.map((m) => ({ ...m, label: format(new Date(m.key), \"MMM\"), weekend: false, today: false })),\n }\n }, [start, end, days, dayWidth, scale])\n\n return (\n <div\n data-slot=\"gantt-header\"\n className={cn(\"sticky top-0 z-30 flex bg-muted text-xs text-muted-foreground\", className)}\n style={{ width: sidebarWidth + width }}\n {...props}\n >\n <GanttHeaderCorner />\n <div className=\"relative shrink-0\" style={{ width }}>\n <div className=\"flex h-7 border-b\">\n {tiers.top.map((cell) => (\n <div\n key={cell.key}\n className=\"absolute inset-y-0 flex items-center truncate border-e px-2 font-medium\"\n style={{ left: cell.left, width: cell.width }}\n >\n {cell.label}\n </div>\n ))}\n </div>\n <div className=\"flex h-7 border-b\">\n {tiers.bottom.map((cell) => (\n <div\n key={cell.key}\n data-weekend={cell.weekend ? \"\" : undefined}\n data-today={cell.today ? \"\" : undefined}\n 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\"\n style={{ left: cell.left, width: cell.width, top: 28 }}\n >\n {cell.width >= 18 ? cell.label : \"\"}\n </div>\n ))}\n </div>\n </div>\n </div>\n )\n}\n\n/** The sticky cell above the sidebar; put a title or a search box in it. */\nfunction GanttHeaderCorner({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-header-corner\"\n className={cn(\n \"sticky left-0 z-40 flex h-14 shrink-0 items-end border-e border-b bg-muted px-3 pb-2 font-medium\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\nfunction GanttBody({ className, style, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth, sidebarWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-body\"\n className={cn(\"relative\", className)}\n style={{ width: sidebarWidth + days * dayWidth, ...style }}\n {...props}\n />\n )\n}\n\n/** Vertical day/week lines and weekend shading behind the bars. */\nfunction GanttGridLines({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, dayWidth, scale, sidebarWidth } = useGantt()\n const cells = React.useMemo(() => {\n if (scale === \"month\")\n return eachMonthOfInterval({ start, end }).map((d) => ({ date: startOfMonth(d), weekend: false }))\n if (scale === \"week\")\n return eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((d) => ({ date: d, weekend: false }))\n return eachDayOfInterval({ start, end }).map((d) => ({ date: d, weekend: isWeekend(d) }))\n }, [start, end, scale])\n return (\n <div\n data-slot=\"gantt-grid-lines\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0\", className)}\n style={{ left: sidebarWidth, right: 0 }}\n {...props}\n >\n {cells.map((cell) => (\n <div\n key={cell.date.toISOString()}\n data-weekend={cell.weekend ? \"\" : undefined}\n className=\"absolute inset-y-0 border-e border-border/60 data-weekend:bg-foreground/2.5\"\n style={{\n left: differenceInCalendarDays(cell.date, start) * dayWidth,\n width: scale === \"day\" ? dayWidth : undefined,\n }}\n />\n ))}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Rows\n * ------------------------------------------------------------------------- */\n\n/** Groups the rows; keep bars inside so dependency lines can find them. */\nfunction GanttRows({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-rows\" className={cn(\"relative\", className)} {...props} />\n}\n\nfunction GanttRow({ value, className, children, ...props }: React.ComponentProps<\"div\"> & { value: string }) {\n const { rowHeight } = useGantt()\n const ctx = React.useMemo(() => ({ value }), [value])\n return (\n <RowContext.Provider value={ctx}>\n <div\n data-slot=\"gantt-row\"\n data-value={value}\n className={cn(\"group/row flex border-b hover:bg-muted/30\", className)}\n style={{ height: rowHeight }}\n {...props}\n >\n {children}\n </div>\n </RowContext.Provider>\n )\n}\n\n/** Sticky sidebar cell for the row. */\nfunction GanttRowLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-row-label\"\n className={cn(\n \"sticky left-0 z-20 flex shrink-0 items-center gap-2 border-e bg-background px-3 text-sm transition-colors\",\n \"group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_30%,var(--color-background))]\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\n/** The timeline part of a row; bars are positioned inside it. */\nfunction GanttRowTrack({ className, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-row-track\"\n className={cn(\"relative shrink-0\", className)}\n style={{ width: days * dayWidth }}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Bars\n * ------------------------------------------------------------------------- */\n\nconst BarContext = React.createContext<BarRegistration | null>(null)\n\nfunction GanttBar({\n value,\n start,\n end,\n progress,\n className,\n style,\n children,\n onPointerDown,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; start: Date; end: Date; progress?: number }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const live = gantt.interaction?.id === value ? gantt.interaction : null\n // Key on timestamps: callers often pass fresh Date objects every render.\n const startMs = (live ? live.start : startOfDay(start)).getTime()\n const endMs = (live ? live.end : startOfDay(end)).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(startMs), end: new Date(endMs) }),\n [value, startMs, endMs]\n )\n const shownStart = registration.start\n const shownEnd = registration.end\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n const left = gantt.xOf(shownStart)\n const width = Math.max(gantt.dayWidth, (differenceInCalendarDays(shownEnd, shownStart) + 1) * gantt.dayWidth)\n const isGrabbed = gantt.grabbed === value\n\n return (\n <BarContext.Provider value={registration}>\n <div\n ref={ref}\n data-slot=\"gantt-bar\"\n data-value={value}\n data-dragging={live ? live.type : undefined}\n data-grabbed={isGrabbed ? \"\" : undefined}\n tabIndex={gantt.editable ? 0 : undefined}\n aria-pressed={gantt.editable ? isGrabbed : undefined}\n aria-label={`${format(shownStart, \"MMM d\")} to ${format(shownEnd, \"MMM d\")}`}\n className={cn(\n \"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\",\n gantt.editable && \"cursor-grab active:cursor-grabbing\",\n \"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\",\n className\n )}\n style={{ left, width, ...style }}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n if ((event.target as HTMLElement).closest(\"[data-slot=gantt-bar-resize-handle], button, a, input\")) return\n gantt.startInteraction(registration, \"move\", event)\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || !gantt.editable || event.target !== event.currentTarget) return\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault()\n gantt.grab(isGrabbed ? null : value)\n return\n }\n if (!isGrabbed) return\n if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n event.preventDefault()\n const delta = event.key === \"ArrowLeft\" ? -1 : 1\n gantt.nudge(registration, delta, event.shiftKey ? \"end\" : event.altKey ? \"start\" : \"both\")\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n gantt.cancelGrab()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (isGrabbed && !event.currentTarget.contains(event.relatedTarget as Node | null)) gantt.grab(null)\n }}\n {...props}\n >\n {progress !== undefined && (\n <div\n data-slot=\"gantt-bar-progress\"\n aria-hidden\n className=\"absolute inset-y-0 left-0 bg-primary/30\"\n style={{ width: `${clamp(progress, 0, 1) * 100}%` }}\n />\n )}\n {children}\n </div>\n </BarContext.Provider>\n )\n}\n\nfunction GanttBarLabel({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"gantt-bar-label\" className={cn(\"relative z-10 truncate px-2 font-medium\", className)} {...props} />\n )\n}\n\nfunction GanttBarResizeHandle({\n side,\n className,\n onPointerDown,\n ...props\n}: React.ComponentProps<\"div\"> & { side: \"start\" | \"end\" }) {\n const gantt = useGantt()\n const bar = React.useContext(BarContext)\n if (!gantt.editable || !bar) return null\n return (\n <div\n data-slot=\"gantt-bar-resize-handle\"\n data-side={side}\n role=\"separator\"\n aria-orientation=\"vertical\"\n className={cn(\n \"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\",\n side === \"start\" ? \"left-0 rounded-l-md\" : \"right-0 rounded-r-md\",\n \"after:absolute after:inset-y-1.5 after:w-0.5 after:rounded-full after:bg-primary/60\",\n side === \"start\" ? \"after:left-0.5\" : \"after:right-0.5\",\n className\n )}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n gantt.startInteraction(bar, side === \"start\" ? \"resize-start\" : \"resize-end\", event)\n }}\n {...props}\n />\n )\n}\n\n/** A single-day marker rendered as a diamond. */\nfunction GanttMilestone({\n value,\n date,\n className,\n style,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; date: Date }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const dayMs = startOfDay(date).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(dayMs), end: new Date(dayMs), milestone: true }),\n [value, dayMs]\n )\n const day = registration.start\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n return (\n <div\n ref={ref}\n data-slot=\"gantt-milestone\"\n data-value={value}\n aria-label={format(day, \"MMM d\")}\n className={cn(\"absolute top-1/2 z-10 flex -translate-y-1/2 items-center gap-2\", className)}\n style={{ left: gantt.xOf(day) + gantt.dayWidth / 2 - 8, ...style }}\n {...props}\n >\n <span aria-hidden className=\"block size-4 rotate-45 rounded-sm bg-foreground\" />\n {children && <span className=\"text-xs font-medium whitespace-nowrap\">{children}</span>}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Dependencies, today, empty\n * ------------------------------------------------------------------------- */\n\n/** Arrows from the end of `from` to the start of `to`, measured from the registered bar elements. */\nfunction GanttDependencies({\n links,\n className,\n ...props\n}: React.ComponentProps<\"svg\"> & { links: { from: string; to: string }[] }) {\n const { barStore, dayWidth, xOf, sidebarWidth, rowHeight, days } = useGantt()\n React.useSyncExternalStore(barStore.subscribe, barStore.getVersion, barStore.getVersion)\n const svgRef = React.useRef<SVGSVGElement>(null)\n const id = React.useId()\n // Row centres come from the DOM so collapsed groups and custom row order need no bookkeeping.\n const origin = svgRef.current?.getBoundingClientRect()\n const centreY = (el: HTMLElement | null) => {\n if (!el || !origin) return null\n const r = el.getBoundingClientRect()\n return r.top + r.height / 2 - origin.top\n }\n const paths = links\n .map((link) => {\n const from = barStore.bars.get(link.from)\n const to = barStore.bars.get(link.to)\n if (!from || !to) return null\n const y1 = centreY(from.el)\n const y2 = centreY(to.el)\n if (y1 === null || y2 === null) return null\n const x1 = xOf(from.end) + (from.milestone ? dayWidth / 2 + 8 : dayWidth)\n const x2 = xOf(to.start) + (to.milestone ? dayWidth / 2 - 8 : 0)\n const gap = 10\n const d =\n x2 - gap > x1\n ? `M ${x1} ${y1} H ${x1 + gap} V ${y2} H ${x2}`\n : `M ${x1} ${y1} H ${x1 + gap} V ${y1 + rowHeight / 2 - 4} H ${x2 - gap} V ${y2} H ${x2}`\n return { key: `${link.from}->${link.to}`, d }\n })\n .filter((p): p is { key: string; d: string } => !!p)\n return (\n <svg\n ref={svgRef}\n data-slot=\"gantt-dependencies\"\n aria-hidden\n className={cn(\"pointer-events-none absolute top-0 z-5 overflow-hidden text-muted-foreground\", className)}\n style={{ left: sidebarWidth, width: days * dayWidth, height: \"100%\" }}\n {...props}\n >\n <defs>\n <marker id={`${id}-arrow`} viewBox=\"0 0 6 6\" refX=\"5\" refY=\"3\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto\">\n <path d=\"M 0 0 L 6 3 L 0 6 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n {paths.map((p) => (\n <path key={p.key} d={p.d} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} markerEnd={`url(#${id}-arrow)`} />\n ))}\n </svg>\n )\n}\n\nfunction GanttToday({ className, date = new Date(), ...props }: React.ComponentProps<\"div\"> & { date?: Date }) {\n const { xOf, dayWidth, sidebarWidth, start, end } = useGantt()\n const day = startOfDay(date)\n if (day < start || day > end) return null\n return (\n <div\n data-slot=\"gantt-today\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0 w-px bg-destructive\", className)}\n style={{ left: sidebarWidth + xOf(day) + dayWidth / 2 }}\n {...props}\n >\n <span className=\"absolute top-0 left-1 rounded-b bg-destructive px-1 text-[10px]/4 text-white\">Today</span>\n </div>\n )\n}\n\nfunction GanttEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-empty\"\n className={cn(\"flex h-32 items-center justify-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Controls\n * ------------------------------------------------------------------------- */\n\nfunction GanttControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-controls\" className={cn(\"flex items-center gap-1\", className)} {...props} />\n}\n\nfunction GanttZoomInTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomIn } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-in-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom in\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomIn()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <PlusIcon />)}\n </Button>\n )\n}\n\nfunction GanttZoomOutTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomOut } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-out-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom out\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomOut()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <MinusIcon />)}\n </Button>\n )\n}\n\nfunction GanttTodayTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { scrollToDate } = useGantt()\n return (\n <Button\n data-slot=\"gantt-today-trigger\"\n variant=\"outline\"\n size=\"sm\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) scrollToDate(new Date())\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <CalendarIcon /> Today\n </>\n ))}\n </Button>\n )\n}\n\nexport {\n Gantt,\n GanttViewport,\n GanttHeader,\n GanttHeaderCorner,\n GanttBody,\n GanttGridLines,\n GanttRows,\n GanttRow,\n GanttRowLabel,\n GanttRowTrack,\n GanttBar,\n GanttBarLabel,\n GanttBarResizeHandle,\n GanttMilestone,\n GanttDependencies,\n GanttToday,\n GanttEmpty,\n GanttControls,\n GanttZoomInTrigger,\n GanttZoomOutTrigger,\n GanttTodayTrigger,\n useGantt,\n useGanttRow,\n type GanttScale,\n type GanttBarChange,\n}\n"
44
816
  }
45
817
  ],
46
818
  "docs": "## Gantt\n`src/components/ui/gantt.tsx` is a compositional, data-agnostic timeline. The consumer owns rows,\nbars and dependency links; the root owns the time scale (`start`/`end` dates, `dayWidth`\ncontrollable + `onDayWidthChange`, `scale` day|week|month derived from `dayWidth` unless set,\n`rowHeight`, `sidebarWidth`, `editable`), the drag/resize interaction, keyboard moves, and\nreports `onBarChange({ id, start, end })` snapped to whole days. Parts: `GanttViewport` (one\nscroll container for both axes) > `GanttHeader` (sticky two-tier date header, tiers by scale;\n`GanttHeaderCorner` is the sticky cell over the sidebar) + `GanttBody` (`GanttGridLines` with\nweekend shading, `GanttToday`, `GanttRows` > `GanttRow value=` > `GanttRowLabel` (sticky sidebar\ncell) + `GanttRowTrack` > `GanttBar value= start end progress` (draggable, focusable; `data-dragging`\n= move|resize-start|resize-end, `data-grabbed`; children: `GanttBarLabel`, `GanttBarResizeHandle\nside=start|end`) or `GanttMilestone value= date` (diamond), `GanttDependencies links=[{ from, to }]`\n(orthogonal arrows measured from the registered bar elements through a small external store, so\ncollapsed groups and custom row order need no bookkeeping), `GanttEmpty`), `GanttControls` >\n`GanttZoomIn/ZoomOutTrigger` (zoom keeps the centre date) + `GanttTodayTrigger`, and `useGantt()`\n(`xOf`, `dateAt`, `scrollToDate`, ...). Keyboard on a bar: Space/Enter picks up, ←/→ move a day,\nShift+←/→ change the end, Alt+←/→ change the start, Escape cancels; a live region announces.\nGotcha: bars register with a memo keyed on timestamps; never key registrations on `Date` identity.\n`src/demo/gantt/` is the reference (`/projects/timeline`, \"Timeline\"): phases with collapsible groups and\nsummary spans, milestones, dependency arrows toggle, day/week/month `SegmentGroup`, undo/redo on\n`useHistory`, editable toggle, scroll to today on mount."