@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,1164 @@
1
+ import * as React from "react"
2
+ import {
3
+ addDays,
4
+ addMinutes,
5
+ addMonths,
6
+ addWeeks,
7
+ differenceInCalendarDays,
8
+ differenceInMinutes,
9
+ eachDayOfInterval,
10
+ endOfMonth,
11
+ endOfWeek,
12
+ format,
13
+ isSameDay,
14
+ isSameMonth,
15
+ isToday,
16
+ setHours,
17
+ setMinutes,
18
+ startOfDay,
19
+ startOfMonth,
20
+ startOfWeek,
21
+ } from "date-fns"
22
+ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
23
+ import { Button } from "@/components/ui/button"
24
+ import { SegmentGroup, SegmentGroupIndicator, SegmentGroupItem } from "@/components/ui/segment-group"
25
+ import { cn } from "@/lib/utils"
26
+ import { useControllable } from "@/lib/controllable"
27
+ import { LiveRegion, useLiveRegion } from "@/components/ui/live-region"
28
+
29
+ /**
30
+ * Scheduler — a compositional, data-agnostic calendar. The consumer owns the
31
+ * events and renders them with the parts; the root owns the anchor `date`,
32
+ * the `view` (day/week/month), the time-grid geometry and the interactions:
33
+ * drag to move, drag the bottom edge to resize, drag on empty time to create,
34
+ * keyboard moves. Everything is reported as intents (`onEventChange`,
35
+ * `onCreate`) snapped to `slotMinutes`.
36
+ *
37
+ * Anatomy:
38
+ * Scheduler { date, view, onEventChange, onCreate }
39
+ * SchedulerToolbar > SchedulerPrevTrigger / SchedulerTodayTrigger / SchedulerNextTrigger / SchedulerTitle / SchedulerViewSelect
40
+ * SchedulerTimeGrid (day, week)
41
+ * SchedulerTimeGridHeader > SchedulerDayHeading date=… (+ SchedulerAllDayRow)
42
+ * SchedulerTimeGridBody > SchedulerTimeGutter + SchedulerDayColumn date= events=
43
+ * children: (event) => <SchedulerEvent value= start= end=> … <SchedulerEventResizeHandle/> </SchedulerEvent>
44
+ * SchedulerNowIndicator, SchedulerCreatePreview
45
+ * SchedulerMonthGrid > SchedulerMonthHeader + SchedulerMonthBody > SchedulerMonthCell date= > SchedulerMonthEvent value=…
46
+ *
47
+ * Keyboard on an event: Space/Enter picks up, ↑/↓ move by a slot, ←/→ move a
48
+ * day, Shift+↑/↓ change the end, Escape cancels.
49
+ */
50
+
51
+ type SchedulerView = "day" | "week" | "month"
52
+ type EventLike = { id: string; start: Date; end: Date; allDay?: boolean }
53
+ type SchedulerEventChange = { id: string; start: Date; end: Date }
54
+ type Interaction = { id: string; type: "move" | "resize"; start: Date; end: Date }
55
+ type CreateState = { start: Date; end: Date } | null
56
+
57
+ type SchedulerContextValue = {
58
+ date: Date
59
+ setDate: (date: Date) => void
60
+ view: SchedulerView
61
+ setView: (view: SchedulerView) => void
62
+ range: { start: Date; end: Date }
63
+ days: Date[]
64
+ weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6
65
+ hourHeight: number
66
+ slotMinutes: number
67
+ minHour: number
68
+ maxHour: number
69
+ editable: boolean
70
+ interaction: Interaction | null
71
+ creating: CreateState
72
+ grabbed: string | null
73
+ timeToY: (date: Date) => number
74
+ yToMinutes: (y: number) => number
75
+ bodyRef: React.RefObject<HTMLDivElement | null>
76
+ startMove: (event: EventLike, pointer: React.PointerEvent, columnDates: Date[]) => void
77
+ startResize: (event: EventLike, pointer: React.PointerEvent) => void
78
+ startCreate: (day: Date, pointer: React.PointerEvent, columnDates: Date[]) => void
79
+ startMonthMove: (event: EventLike, pointer: React.PointerEvent) => void
80
+ grab: (id: string | null) => void
81
+ cancelGrab: () => void
82
+ nudge: (event: EventLike, deltaMinutes: number, deltaDays: number, part: "both" | "end") => void
83
+ announce: (message: string) => void
84
+ }
85
+
86
+ const SchedulerContext = React.createContext<SchedulerContextValue | null>(null)
87
+ function useScheduler() {
88
+ const ctx = React.useContext(SchedulerContext)
89
+ if (!ctx) throw new Error("Scheduler parts must be used within <Scheduler>")
90
+ return ctx
91
+ }
92
+
93
+ const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))
94
+ const minutesOfDay = (d: Date) => d.getHours() * 60 + d.getMinutes()
95
+ const withMinutes = (day: Date, minutes: number) => setMinutes(setHours(startOfDay(day), 0), minutes)
96
+
97
+ function visibleRange(date: Date, view: SchedulerView, weekStartsOn: SchedulerContextValue["weekStartsOn"]) {
98
+ if (view === "day") return { start: startOfDay(date), end: startOfDay(date) }
99
+ if (view === "week") return { start: startOfWeek(date, { weekStartsOn }), end: endOfWeek(date, { weekStartsOn }) }
100
+ return {
101
+ start: startOfWeek(startOfMonth(date), { weekStartsOn }),
102
+ end: endOfWeek(endOfMonth(date), { weekStartsOn }),
103
+ }
104
+ }
105
+
106
+ /* ---------------------------------------------------------------------------
107
+ * Root
108
+ * ------------------------------------------------------------------------- */
109
+
110
+ function Scheduler({
111
+ date: dateProp,
112
+ defaultDate,
113
+ onDateChange,
114
+ view: viewProp,
115
+ defaultView = "week",
116
+ onViewChange,
117
+ weekStartsOn = 1,
118
+ hourHeight = 56,
119
+ slotMinutes = 15,
120
+ minHour = 0,
121
+ maxHour = 24,
122
+ editable = true,
123
+ onEventChange,
124
+ onCreate,
125
+ className,
126
+ children,
127
+ ...props
128
+ }: Omit<React.ComponentProps<"div">, "onChange"> & {
129
+ date?: Date
130
+ defaultDate?: Date
131
+ onDateChange?: (date: Date) => void
132
+ view?: SchedulerView
133
+ defaultView?: SchedulerView
134
+ onViewChange?: (view: SchedulerView) => void
135
+ weekStartsOn?: SchedulerContextValue["weekStartsOn"]
136
+ hourHeight?: number
137
+ slotMinutes?: number
138
+ minHour?: number
139
+ maxHour?: number
140
+ editable?: boolean
141
+ onEventChange?: (change: SchedulerEventChange) => void
142
+ /** Drag on empty time (or a month cell click) proposes a new event. */
143
+ onCreate?: (range: { start: Date; end: Date; allDay?: boolean }) => void
144
+ }) {
145
+ const [initialDate] = React.useState(() => startOfDay(defaultDate ?? new Date()))
146
+ const [date, setDate] = useControllable(dateProp, initialDate, onDateChange)
147
+ const [view, setView] = useControllable<SchedulerView>(viewProp, defaultView, onViewChange)
148
+
149
+ const range = React.useMemo(() => visibleRange(date, view, weekStartsOn), [date, view, weekStartsOn])
150
+ const days = React.useMemo(() => eachDayOfInterval(range), [range])
151
+
152
+ const bodyRef = React.useRef<HTMLDivElement>(null)
153
+ const minuteHeight = hourHeight / 60
154
+ const timeToY = React.useCallback(
155
+ (d: Date) => (minutesOfDay(d) - minHour * 60) * minuteHeight,
156
+ [minHour, minuteHeight]
157
+ )
158
+ const yToMinutes = React.useCallback(
159
+ (y: number) =>
160
+ clamp(Math.round((y / minuteHeight + minHour * 60) / slotMinutes) * slotMinutes, minHour * 60, maxHour * 60),
161
+ [minuteHeight, minHour, maxHour, slotMinutes]
162
+ )
163
+
164
+ const [interaction, setInteraction] = React.useState<Interaction | null>(null)
165
+ const [creating, setCreating] = React.useState<CreateState>(null)
166
+ const [grabbed, setGrabbed] = React.useState<string | null>(null)
167
+ const { message: announcement, announce } = useLiveRegion()
168
+ const onEventChangeRef = React.useRef(onEventChange)
169
+ const onCreateRef = React.useRef(onCreate)
170
+ React.useEffect(() => {
171
+ onEventChangeRef.current = onEventChange
172
+ onCreateRef.current = onCreate
173
+ }, [onEventChange, onCreate])
174
+ const grabOrigin = React.useRef<SchedulerEventChange | null>(null)
175
+
176
+ const track = React.useCallback((onMove: (e: PointerEvent) => void, onUp: (e: PointerEvent) => void) => {
177
+ const up = (e: PointerEvent) => {
178
+ window.removeEventListener("pointermove", onMove)
179
+ window.removeEventListener("pointerup", up)
180
+ window.removeEventListener("pointercancel", up)
181
+ onUp(e)
182
+ }
183
+ window.addEventListener("pointermove", onMove)
184
+ window.addEventListener("pointerup", up)
185
+ window.addEventListener("pointercancel", up)
186
+ }, [])
187
+
188
+ /** Which day column a client x falls in, from the column elements' rects. */
189
+ const dayAtX = React.useCallback((x: number, columnDates: Date[]) => {
190
+ const columns = Array.from(bodyRef.current?.querySelectorAll<HTMLElement>("[data-slot=scheduler-day-column]") ?? [])
191
+ for (const [i, col] of columns.entries()) {
192
+ const r = col.getBoundingClientRect()
193
+ if (x >= r.left && x < r.right) return columnDates[i] ?? null
194
+ }
195
+ return null
196
+ }, [])
197
+
198
+ const startMove = React.useCallback(
199
+ (event: EventLike, pointer: React.PointerEvent, columnDates: Date[]) => {
200
+ if (!editable) return
201
+ pointer.preventDefault()
202
+ pointer.stopPropagation()
203
+ const originY = pointer.clientY
204
+ const originDay = startOfDay(event.start)
205
+ let current: Interaction = { id: event.id, type: "move", start: event.start, end: event.end }
206
+ let moved = false
207
+ setInteraction(current)
208
+ track(
209
+ (e) => {
210
+ const dy = e.clientY - originY
211
+ const deltaMinutes = Math.round(dy / minuteHeight / slotMinutes) * slotMinutes
212
+ const day = dayAtX(e.clientX, columnDates) ?? originDay
213
+ const dayDelta = differenceInCalendarDays(day, originDay)
214
+ const duration = differenceInMinutes(event.end, event.start)
215
+ const startMin = clamp(minutesOfDay(event.start) + deltaMinutes, minHour * 60, maxHour * 60 - duration)
216
+ const start = addDays(withMinutes(originDay, startMin), dayDelta)
217
+ current = { id: event.id, type: "move", start, end: addMinutes(start, duration) }
218
+ if (Math.abs(dy) > 3 || dayDelta !== 0) moved = true
219
+ setInteraction(current)
220
+ },
221
+ () => {
222
+ setInteraction(null)
223
+ if (
224
+ moved &&
225
+ (current.start.getTime() !== event.start.getTime() || current.end.getTime() !== event.end.getTime())
226
+ ) {
227
+ onEventChangeRef.current?.({ id: event.id, start: current.start, end: current.end })
228
+ }
229
+ }
230
+ )
231
+ },
232
+ [editable, track, dayAtX, minuteHeight, slotMinutes, minHour, maxHour]
233
+ )
234
+
235
+ const startResize = React.useCallback(
236
+ (event: EventLike, pointer: React.PointerEvent) => {
237
+ if (!editable) return
238
+ pointer.preventDefault()
239
+ pointer.stopPropagation()
240
+ const originY = pointer.clientY
241
+ let current: Interaction = { id: event.id, type: "resize", start: event.start, end: event.end }
242
+ setInteraction(current)
243
+ track(
244
+ (e) => {
245
+ const deltaMinutes = Math.round((e.clientY - originY) / minuteHeight / slotMinutes) * slotMinutes
246
+ const endMin = clamp(
247
+ minutesOfDay(event.end) + deltaMinutes,
248
+ minutesOfDay(event.start) + slotMinutes,
249
+ maxHour * 60
250
+ )
251
+ current = { ...current, end: withMinutes(event.start, endMin) }
252
+ setInteraction(current)
253
+ },
254
+ () => {
255
+ setInteraction(null)
256
+ if (current.end.getTime() !== event.end.getTime()) {
257
+ onEventChangeRef.current?.({ id: event.id, start: event.start, end: current.end })
258
+ }
259
+ }
260
+ )
261
+ },
262
+ [editable, track, minuteHeight, slotMinutes, maxHour]
263
+ )
264
+
265
+ const startCreate = React.useCallback(
266
+ (day: Date, pointer: React.PointerEvent, columnDates: Date[]) => {
267
+ if (!editable || !onCreateRef.current) return
268
+ const column = (pointer.currentTarget as HTMLElement).getBoundingClientRect()
269
+ const originMin = yToMinutes(pointer.clientY - column.top)
270
+ let current: NonNullable<CreateState> = {
271
+ start: withMinutes(day, originMin),
272
+ end: withMinutes(day, originMin + slotMinutes),
273
+ }
274
+ let dragged = false
275
+ setCreating(current)
276
+ track(
277
+ (e) => {
278
+ const min = yToMinutes(e.clientY - column.top)
279
+ const targetDay = dayAtX(e.clientX, columnDates) ?? day
280
+ const a = Math.min(originMin, min)
281
+ const b = Math.max(originMin, min, a + slotMinutes)
282
+ current = { start: withMinutes(targetDay, a), end: withMinutes(targetDay, b) }
283
+ dragged = true
284
+ setCreating(current)
285
+ },
286
+ () => {
287
+ setCreating(null)
288
+ onCreateRef.current?.(dragged ? current : { start: current.start, end: addMinutes(current.start, 60) })
289
+ }
290
+ )
291
+ },
292
+ [editable, track, yToMinutes, slotMinutes, dayAtX]
293
+ )
294
+
295
+ /** Month view: drag a chip onto another cell to shift the event by whole days. */
296
+ const startMonthMove = React.useCallback(
297
+ (event: EventLike, pointer: React.PointerEvent) => {
298
+ if (!editable) return
299
+ pointer.preventDefault()
300
+ pointer.stopPropagation()
301
+ const originDay = startOfDay(event.start)
302
+ let current: Interaction = { id: event.id, type: "move", start: event.start, end: event.end }
303
+ let moved = false
304
+ setInteraction(current)
305
+ const cellAt = (x: number, y: number) =>
306
+ document.elementFromPoint(x, y)?.closest<HTMLElement>("[data-slot=scheduler-month-cell]")?.dataset.date
307
+ track(
308
+ (e) => {
309
+ const target = cellAt(e.clientX, e.clientY)
310
+ if (!target) return
311
+ const delta = differenceInCalendarDays(new Date(target), originDay)
312
+ current = { ...current, start: addDays(event.start, delta), end: addDays(event.end, delta) }
313
+ moved = true
314
+ setInteraction(current)
315
+ },
316
+ () => {
317
+ setInteraction(null)
318
+ if (moved && current.start.getTime() !== event.start.getTime()) {
319
+ onEventChangeRef.current?.({ id: event.id, start: current.start, end: current.end })
320
+ }
321
+ }
322
+ )
323
+ },
324
+ [editable, track]
325
+ )
326
+
327
+ const grab = React.useCallback(
328
+ (id: string | null) => {
329
+ if (id) {
330
+ setGrabbed(id)
331
+ announce("Picked up. Arrows move; Shift+Up/Down change the end. Space drops, Escape cancels.")
332
+ } else {
333
+ setGrabbed(null)
334
+ grabOrigin.current = null
335
+ announce("Dropped.")
336
+ }
337
+ },
338
+ [announce]
339
+ )
340
+ const cancelGrab = React.useCallback(() => {
341
+ if (grabOrigin.current) onEventChangeRef.current?.(grabOrigin.current)
342
+ setGrabbed(null)
343
+ grabOrigin.current = null
344
+ announce("Cancelled.")
345
+ }, [announce])
346
+
347
+ const nudge = React.useCallback(
348
+ (event: EventLike, deltaMinutes: number, deltaDays: number, part: "both" | "end") => {
349
+ if (!grabOrigin.current || grabOrigin.current.id !== event.id) {
350
+ grabOrigin.current = { id: event.id, start: event.start, end: event.end }
351
+ }
352
+ const next =
353
+ part === "end"
354
+ ? {
355
+ start: event.start,
356
+ end: addMinutes(
357
+ event.end,
358
+ Math.max(deltaMinutes, slotMinutes - differenceInMinutes(event.end, event.start))
359
+ ),
360
+ }
361
+ : {
362
+ start: addDays(addMinutes(event.start, deltaMinutes), deltaDays),
363
+ end: addDays(addMinutes(event.end, deltaMinutes), deltaDays),
364
+ }
365
+ onEventChangeRef.current?.({ id: event.id, ...next })
366
+ announce(`${format(next.start, "EEE d, p")} to ${format(next.end, "p")}.`)
367
+ },
368
+ [announce, slotMinutes]
369
+ )
370
+
371
+ const ctx = React.useMemo<SchedulerContextValue>(
372
+ () => ({
373
+ date,
374
+ setDate,
375
+ view,
376
+ setView,
377
+ range,
378
+ days,
379
+ weekStartsOn,
380
+ hourHeight,
381
+ slotMinutes,
382
+ minHour,
383
+ maxHour,
384
+ editable,
385
+ interaction,
386
+ creating,
387
+ grabbed,
388
+ timeToY,
389
+ yToMinutes,
390
+ bodyRef,
391
+ startMove,
392
+ startResize,
393
+ startCreate,
394
+ startMonthMove,
395
+ grab,
396
+ cancelGrab,
397
+ nudge,
398
+ announce,
399
+ }),
400
+ [
401
+ date,
402
+ setDate,
403
+ view,
404
+ setView,
405
+ range,
406
+ days,
407
+ weekStartsOn,
408
+ hourHeight,
409
+ slotMinutes,
410
+ minHour,
411
+ maxHour,
412
+ editable,
413
+ interaction,
414
+ creating,
415
+ grabbed,
416
+ timeToY,
417
+ yToMinutes,
418
+ startMove,
419
+ startResize,
420
+ startCreate,
421
+ startMonthMove,
422
+ grab,
423
+ cancelGrab,
424
+ nudge,
425
+ announce,
426
+ ]
427
+ )
428
+
429
+ return (
430
+ <SchedulerContext.Provider value={ctx}>
431
+ <div
432
+ data-slot="scheduler"
433
+ data-view={view}
434
+ data-interacting={interaction?.type ?? (creating ? "create" : undefined)}
435
+ className={cn("flex min-h-0 flex-1 flex-col gap-3", className)}
436
+ {...props}
437
+ >
438
+ {children}
439
+ <LiveRegion data-slot="scheduler-live-region" message={announcement} />
440
+ </div>
441
+ </SchedulerContext.Provider>
442
+ )
443
+ }
444
+
445
+ /* ---------------------------------------------------------------------------
446
+ * Toolbar
447
+ * ------------------------------------------------------------------------- */
448
+
449
+ function SchedulerToolbar({ className, ...props }: React.ComponentProps<"div">) {
450
+ return <div data-slot="scheduler-toolbar" className={cn("flex flex-wrap items-center gap-2", className)} {...props} />
451
+ }
452
+
453
+ function shift(date: Date, view: SchedulerView, direction: 1 | -1) {
454
+ if (view === "day") return addDays(date, direction)
455
+ if (view === "week") return addWeeks(date, direction)
456
+ return addMonths(date, direction)
457
+ }
458
+
459
+ function SchedulerPrevTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
460
+ const { date, view, setDate } = useScheduler()
461
+ return (
462
+ <Button
463
+ data-slot="scheduler-prev-trigger"
464
+ variant="outline"
465
+ size="icon-sm"
466
+ aria-label={`Previous ${view}`}
467
+ asChild={asChild}
468
+ onClick={(event) => {
469
+ onClick?.(event)
470
+ if (!event.defaultPrevented) setDate(shift(date, view, -1))
471
+ }}
472
+ {...props}
473
+ >
474
+ {asChild ? children : (children ?? <ChevronLeftIcon />)}
475
+ </Button>
476
+ )
477
+ }
478
+
479
+ function SchedulerNextTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
480
+ const { date, view, setDate } = useScheduler()
481
+ return (
482
+ <Button
483
+ data-slot="scheduler-next-trigger"
484
+ variant="outline"
485
+ size="icon-sm"
486
+ aria-label={`Next ${view}`}
487
+ asChild={asChild}
488
+ onClick={(event) => {
489
+ onClick?.(event)
490
+ if (!event.defaultPrevented) setDate(shift(date, view, 1))
491
+ }}
492
+ {...props}
493
+ >
494
+ {asChild ? children : (children ?? <ChevronRightIcon />)}
495
+ </Button>
496
+ )
497
+ }
498
+
499
+ function SchedulerTodayTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
500
+ const { setDate } = useScheduler()
501
+ return (
502
+ <Button
503
+ data-slot="scheduler-today-trigger"
504
+ variant="outline"
505
+ size="sm"
506
+ asChild={asChild}
507
+ onClick={(event) => {
508
+ onClick?.(event)
509
+ if (!event.defaultPrevented) setDate(startOfDay(new Date()))
510
+ }}
511
+ {...props}
512
+ >
513
+ {asChild
514
+ ? children
515
+ : (children ?? (
516
+ <>
517
+ <CalendarIcon /> Today
518
+ </>
519
+ ))}
520
+ </Button>
521
+ )
522
+ }
523
+
524
+ /** "September 2026", "Sep 7 – 13, 2026" or "Mon, Sep 7". */
525
+ function SchedulerTitle({ className, children, ...props }: React.ComponentProps<"h2">) {
526
+ const { date, view, range } = useScheduler()
527
+ const text =
528
+ view === "month"
529
+ ? format(date, "MMMM yyyy")
530
+ : view === "day"
531
+ ? format(date, "EEEE, MMM d, yyyy")
532
+ : isSameMonth(range.start, range.end)
533
+ ? `${format(range.start, "MMM d")} – ${format(range.end, "d, yyyy")}`
534
+ : `${format(range.start, "MMM d")} – ${format(range.end, "MMM d, yyyy")}`
535
+ return (
536
+ <h2 data-slot="scheduler-title" className={cn("text-base font-semibold tabular-nums", className)} {...props}>
537
+ {children ?? text}
538
+ </h2>
539
+ )
540
+ }
541
+
542
+ function SchedulerViewSelect({
543
+ className,
544
+ ...props
545
+ }: Omit<React.ComponentProps<typeof SegmentGroup>, "value" | "onValueChange">) {
546
+ const { view, setView } = useScheduler()
547
+ return (
548
+ <SegmentGroup
549
+ data-slot="scheduler-view-select"
550
+ value={view}
551
+ onValueChange={({ value }) => value && setView(value as SchedulerView)}
552
+ className={className}
553
+ {...props}
554
+ >
555
+ <SegmentGroupIndicator />
556
+ <SegmentGroupItem value="day">Day</SegmentGroupItem>
557
+ <SegmentGroupItem value="week">Week</SegmentGroupItem>
558
+ <SegmentGroupItem value="month">Month</SegmentGroupItem>
559
+ </SegmentGroup>
560
+ )
561
+ }
562
+
563
+ /* ---------------------------------------------------------------------------
564
+ * Time grid (day / week)
565
+ * ------------------------------------------------------------------------- */
566
+
567
+ const GUTTER = "3.5rem"
568
+
569
+ function SchedulerTimeGrid({ className, style, ...props }: React.ComponentProps<"div">) {
570
+ return (
571
+ <div
572
+ data-slot="scheduler-time-grid"
573
+ className={cn("flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border bg-background", className)}
574
+ style={{ ["--scheduler-gutter" as string]: GUTTER, ...style }}
575
+ {...props}
576
+ />
577
+ )
578
+ }
579
+
580
+ function SchedulerTimeGridHeader({ className, ...props }: React.ComponentProps<"div">) {
581
+ return (
582
+ <div
583
+ data-slot="scheduler-time-grid-header"
584
+ className={cn("grid shrink-0 border-b bg-muted/50", className)}
585
+ style={{ gridTemplateColumns: "var(--scheduler-gutter) 1fr" }}
586
+ {...props}
587
+ />
588
+ )
589
+ }
590
+
591
+ /** Day headings across the top, one per visible day. */
592
+ function SchedulerDayHeadings({
593
+ className,
594
+ children,
595
+ ...props
596
+ }: React.ComponentProps<"div"> & { children?: (day: Date) => React.ReactNode }) {
597
+ const { days, setDate, setView } = useScheduler()
598
+ return (
599
+ <>
600
+ <div aria-hidden className="border-e" />
601
+ <div
602
+ data-slot="scheduler-day-headings"
603
+ className={cn("grid", className)}
604
+ style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
605
+ {...props}
606
+ >
607
+ {days.map((day) => (
608
+ <button
609
+ key={day.toISOString()}
610
+ type="button"
611
+ data-slot="scheduler-day-heading"
612
+ data-today={isToday(day) ? "" : undefined}
613
+ className="group/heading flex flex-col items-center gap-0.5 border-e py-2 text-xs text-muted-foreground outline-none last:border-e-0 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset"
614
+ onClick={() => {
615
+ setDate(day)
616
+ setView("day")
617
+ }}
618
+ >
619
+ {children ? (
620
+ children(day)
621
+ ) : (
622
+ <>
623
+ <span>{format(day, "EEE")}</span>
624
+ <span className="flex size-7 items-center justify-center rounded-full text-base font-semibold text-foreground group-data-today/heading:bg-primary group-data-today/heading:text-primary-foreground">
625
+ {format(day, "d")}
626
+ </span>
627
+ </>
628
+ )}
629
+ </button>
630
+ ))}
631
+ </div>
632
+ </>
633
+ )
634
+ }
635
+
636
+ /** Strip under the headings for all-day events; render chips inside per day. */
637
+ function SchedulerAllDayRow({
638
+ className,
639
+ children,
640
+ ...props
641
+ }: Omit<React.ComponentProps<"div">, "children"> & { children: (day: Date) => React.ReactNode }) {
642
+ const { days } = useScheduler()
643
+ return (
644
+ <>
645
+ <div className="flex items-start justify-end border-e border-t px-1.5 py-1 text-[10px] text-muted-foreground uppercase">
646
+ all day
647
+ </div>
648
+ <div
649
+ data-slot="scheduler-all-day-row"
650
+ className={cn("grid border-t", className)}
651
+ style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
652
+ {...props}
653
+ >
654
+ {days.map((day) => (
655
+ <div
656
+ key={day.toISOString()}
657
+ data-slot="scheduler-all-day-cell"
658
+ className="flex min-h-7 flex-col gap-0.5 border-e p-0.5 last:border-e-0"
659
+ >
660
+ {children(day)}
661
+ </div>
662
+ ))}
663
+ </div>
664
+ </>
665
+ )
666
+ }
667
+
668
+ function SchedulerTimeGridBody({ className, children, ...props }: React.ComponentProps<"div">) {
669
+ const { bodyRef, hourHeight, minHour, maxHour } = useScheduler()
670
+ // Scroll to a sensible hour on mount.
671
+ React.useEffect(() => {
672
+ const el = bodyRef.current
673
+ if (el) el.scrollTop = Math.max(0, (8 - minHour) * hourHeight - 8)
674
+ }, [bodyRef, hourHeight, minHour])
675
+ return (
676
+ <div
677
+ ref={bodyRef}
678
+ data-slot="scheduler-time-grid-body"
679
+ className={cn("relative min-h-0 flex-1 overflow-y-auto", className)}
680
+ {...props}
681
+ >
682
+ <div
683
+ className="grid"
684
+ style={{ gridTemplateColumns: "var(--scheduler-gutter) 1fr", height: (maxHour - minHour) * hourHeight }}
685
+ >
686
+ {children}
687
+ </div>
688
+ </div>
689
+ )
690
+ }
691
+
692
+ function SchedulerTimeGutter({ className, ...props }: React.ComponentProps<"div">) {
693
+ const { hourHeight, minHour, maxHour } = useScheduler()
694
+ const hours = Array.from({ length: maxHour - minHour }, (_, i) => minHour + i)
695
+ return (
696
+ <div data-slot="scheduler-time-gutter" className={cn("relative border-e", className)} {...props}>
697
+ {hours.map((h) => (
698
+ <div
699
+ key={h}
700
+ className="absolute inset-e-1.5 -translate-y-1/2 text-[11px] text-muted-foreground tabular-nums"
701
+ style={{ top: (h - minHour) * hourHeight }}
702
+ >
703
+ {h > minHour ? format(setHours(new Date(), h), "h a") : ""}
704
+ </div>
705
+ ))}
706
+ </div>
707
+ )
708
+ }
709
+
710
+ /** The columns container; hour lines are drawn behind. */
711
+ function SchedulerDayColumns({ className, children, ...props }: React.ComponentProps<"div">) {
712
+ const { days, hourHeight, minHour, maxHour } = useScheduler()
713
+ const hours = Array.from({ length: maxHour - minHour }, (_, i) => i)
714
+ return (
715
+ <div
716
+ data-slot="scheduler-day-columns"
717
+ className={cn("relative grid", className)}
718
+ style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
719
+ {...props}
720
+ >
721
+ <div aria-hidden className="pointer-events-none absolute inset-0">
722
+ {hours.map((i) => (
723
+ <div key={i} className="absolute inset-x-0 border-t border-border/70" style={{ top: i * hourHeight }} />
724
+ ))}
725
+ </div>
726
+ {children}
727
+ </div>
728
+ )
729
+ }
730
+
731
+ /** Side-by-side lanes for overlapping events. */
732
+ function layoutLanes(events: EventLike[]) {
733
+ const sorted = [...events]
734
+ .filter((e) => !e.allDay)
735
+ .sort((a, b) => a.start.getTime() - b.start.getTime() || b.end.getTime() - a.end.getTime())
736
+ const result = new Map<string, { lane: number; lanes: number }>()
737
+ let cluster: EventLike[] = []
738
+ let clusterEnd = 0
739
+ const flush = () => {
740
+ const laneEnds: number[] = []
741
+ const assigned: { id: string; lane: number }[] = []
742
+ for (const e of cluster) {
743
+ let lane = laneEnds.findIndex((end) => end <= e.start.getTime())
744
+ if (lane === -1) lane = laneEnds.length
745
+ laneEnds[lane] = e.end.getTime()
746
+ assigned.push({ id: e.id, lane })
747
+ }
748
+ for (const a of assigned) result.set(a.id, { lane: a.lane, lanes: laneEnds.length })
749
+ cluster = []
750
+ clusterEnd = 0
751
+ }
752
+ for (const e of sorted) {
753
+ if (cluster.length && e.start.getTime() >= clusterEnd) flush()
754
+ cluster.push(e)
755
+ clusterEnd = Math.max(clusterEnd, e.end.getTime())
756
+ }
757
+ if (cluster.length) flush()
758
+ return result
759
+ }
760
+
761
+ const ColumnContext = React.createContext<{ day: Date; lanes: Map<string, { lane: number; lanes: number }> } | null>(
762
+ null
763
+ )
764
+
765
+ /** One day. `events` are the ones on this day (for overlap layout); `children` renders each. */
766
+ function SchedulerDayColumn<T extends EventLike>({
767
+ date,
768
+ events,
769
+ className,
770
+ children,
771
+ onPointerDown,
772
+ ...props
773
+ }: Omit<React.ComponentProps<"div">, "children"> & {
774
+ date: Date
775
+ events: T[]
776
+ children: (event: T) => React.ReactNode
777
+ }) {
778
+ const scheduler = useScheduler()
779
+ const lanes = React.useMemo(() => layoutLanes(events), [events])
780
+ const ctx = React.useMemo(() => ({ day: date, lanes }), [date, lanes])
781
+ return (
782
+ <ColumnContext.Provider value={ctx}>
783
+ <div
784
+ data-slot="scheduler-day-column"
785
+ data-date={date.toISOString()}
786
+ data-today={isToday(date) ? "" : undefined}
787
+ className={cn("relative border-e last:border-e-0 data-today:bg-primary/3", className)}
788
+ onPointerDown={(event) => {
789
+ onPointerDown?.(event)
790
+ if (event.defaultPrevented || event.button !== 0) return
791
+ if ((event.target as HTMLElement).closest("[data-slot=scheduler-event]")) return
792
+ scheduler.startCreate(date, event, scheduler.days)
793
+ }}
794
+ {...props}
795
+ >
796
+ {events.map((event) => (
797
+ <React.Fragment key={event.id}>{children(event)}</React.Fragment>
798
+ ))}
799
+ </div>
800
+ </ColumnContext.Provider>
801
+ )
802
+ }
803
+
804
+ const EventContext = React.createContext<EventLike | null>(null)
805
+
806
+ function SchedulerEvent({
807
+ value,
808
+ start,
809
+ end,
810
+ className,
811
+ style,
812
+ children,
813
+ onPointerDown,
814
+ onKeyDown,
815
+ onBlur,
816
+ ...props
817
+ }: React.ComponentProps<"div"> & { value: string; start: Date; end: Date }) {
818
+ const s = useScheduler()
819
+ const column = React.useContext(ColumnContext)
820
+ const live = s.interaction?.id === value ? s.interaction : null
821
+ const shownStart = live ? live.start : start
822
+ const shownEnd = live ? live.end : end
823
+ // While a move crosses days the event stays in its original column but shifts visually.
824
+ const dayOffset = column ? differenceInCalendarDays(startOfDay(shownStart), column.day) : 0
825
+ const lane = column?.lanes.get(value) ?? { lane: 0, lanes: 1 }
826
+ const top = s.timeToY(shownStart)
827
+ const height = Math.max(s.hourHeight / 4, (differenceInMinutes(shownEnd, shownStart) * s.hourHeight) / 60)
828
+ const isGrabbed = s.grabbed === value
829
+ const eventLike = React.useMemo(() => ({ id: value, start, end }), [value, start, end])
830
+ return (
831
+ <EventContext.Provider value={eventLike}>
832
+ <div
833
+ {...props}
834
+ data-slot="scheduler-event"
835
+ data-value={value}
836
+ data-dragging={live?.type}
837
+ data-grabbed={isGrabbed ? "" : undefined}
838
+ tabIndex={s.editable ? 0 : undefined}
839
+ aria-pressed={s.editable ? isGrabbed : undefined}
840
+ aria-label={`${format(shownStart, "EEE d, p")} to ${format(shownEnd, "p")}`}
841
+ className={cn(
842
+ "group/event absolute flex flex-col overflow-hidden rounded-md border border-primary/30 bg-primary/15 px-1.5 py-1 text-xs text-foreground outline-none select-none",
843
+ s.editable && "cursor-grab active:cursor-grabbing",
844
+ "focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring/50 data-dragging:z-20 data-dragging:shadow-lg data-grabbed:ring-2 data-grabbed:ring-primary",
845
+ className
846
+ )}
847
+ style={{
848
+ top,
849
+ height,
850
+ left: `calc(${(lane.lane / lane.lanes) * 100}% + 2px)`,
851
+ width: `calc(${100 / lane.lanes}% - 4px)`,
852
+ transform: dayOffset ? `translateX(calc(${dayOffset * 100}% + ${dayOffset * 1}px))` : undefined,
853
+ ...style,
854
+ }}
855
+ onPointerDown={(event) => {
856
+ onPointerDown?.(event)
857
+ if (event.defaultPrevented || event.button !== 0) return
858
+ if ((event.target as HTMLElement).closest("[data-slot=scheduler-event-resize-handle], button, a")) return
859
+ s.startMove(eventLike, event, s.days)
860
+ }}
861
+ onKeyDown={(event) => {
862
+ onKeyDown?.(event)
863
+ if (event.defaultPrevented || !s.editable || event.target !== event.currentTarget) return
864
+ if (event.key === " " || event.key === "Enter") {
865
+ event.preventDefault()
866
+ s.grab(isGrabbed ? null : value)
867
+ return
868
+ }
869
+ if (!isGrabbed) return
870
+ const step = s.slotMinutes
871
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
872
+ event.preventDefault()
873
+ const delta = event.key === "ArrowUp" ? -step : step
874
+ s.nudge(eventLike, delta, 0, event.shiftKey ? "end" : "both")
875
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
876
+ event.preventDefault()
877
+ s.nudge(eventLike, 0, event.key === "ArrowLeft" ? -1 : 1, "both")
878
+ } else if (event.key === "Escape") {
879
+ event.preventDefault()
880
+ s.cancelGrab()
881
+ }
882
+ }}
883
+ onBlur={(event) => {
884
+ onBlur?.(event)
885
+ if (isGrabbed && !event.currentTarget.contains(event.relatedTarget as Node | null)) s.grab(null)
886
+ }}
887
+ >
888
+ {children}
889
+ </div>
890
+ </EventContext.Provider>
891
+ )
892
+ }
893
+
894
+ function SchedulerEventTitle({ className, ...props }: React.ComponentProps<"div">) {
895
+ return <div data-slot="scheduler-event-title" className={cn("truncate font-medium", className)} {...props} />
896
+ }
897
+
898
+ /** "9:00 – 9:45 AM"; reflects the live drag. */
899
+ function SchedulerEventTime({ className, children, ...props }: React.ComponentProps<"div">) {
900
+ const s = useScheduler()
901
+ const e = React.useContext(EventContext)
902
+ if (!e) return null
903
+ const live = s.interaction?.id === e.id ? s.interaction : null
904
+ const start = live ? live.start : e.start
905
+ const end = live ? live.end : e.end
906
+ return (
907
+ <div
908
+ data-slot="scheduler-event-time"
909
+ className={cn("truncate text-[11px] tabular-nums opacity-80", className)}
910
+ {...props}
911
+ >
912
+ {children ?? `${format(start, "h:mm")} – ${format(end, "h:mm a")}`}
913
+ </div>
914
+ )
915
+ }
916
+
917
+ function SchedulerEventResizeHandle({ className, onPointerDown, ...props }: React.ComponentProps<"div">) {
918
+ const s = useScheduler()
919
+ const e = React.useContext(EventContext)
920
+ if (!s.editable || !e) return null
921
+ return (
922
+ <div
923
+ data-slot="scheduler-event-resize-handle"
924
+ role="separator"
925
+ aria-orientation="horizontal"
926
+ className={cn(
927
+ "absolute inset-x-0 bottom-0 h-2 cursor-ns-resize touch-none after:absolute after:inset-x-1/2 after:bottom-0.5 after:h-0.5 after:w-6 after:-translate-x-1/2 after:rounded-full after:bg-primary/50 after:opacity-0 group-hover/event:after:opacity-100",
928
+ className
929
+ )}
930
+ onPointerDown={(event) => {
931
+ onPointerDown?.(event)
932
+ if (event.defaultPrevented || event.button !== 0) return
933
+ s.startResize(e, event)
934
+ }}
935
+ {...props}
936
+ />
937
+ )
938
+ }
939
+
940
+ /** Outline of the event being created by dragging; renders nothing when idle. */
941
+ function SchedulerCreatePreview({ className, children, ...props }: React.ComponentProps<"div">) {
942
+ const s = useScheduler()
943
+ if (!s.creating) return null
944
+ const index = s.days.findIndex((d) => isSameDay(d, s.creating!.start))
945
+ if (index === -1) return null
946
+ const top = s.timeToY(s.creating.start)
947
+ const height = (differenceInMinutes(s.creating.end, s.creating.start) * s.hourHeight) / 60
948
+ return (
949
+ <div
950
+ data-slot="scheduler-create-preview"
951
+ aria-hidden
952
+ className={cn(
953
+ "pointer-events-none absolute z-20 rounded-md border-2 border-dashed border-primary/60 bg-primary/10 px-1.5 py-1 text-[11px] text-primary tabular-nums",
954
+ className
955
+ )}
956
+ style={{
957
+ top,
958
+ height,
959
+ left: `calc(${(index / s.days.length) * 100}% + 2px)`,
960
+ width: `calc(${100 / s.days.length}% - 4px)`,
961
+ }}
962
+ {...props}
963
+ >
964
+ {children ?? `${format(s.creating.start, "h:mm")} – ${format(s.creating.end, "h:mm a")}`}
965
+ </div>
966
+ )
967
+ }
968
+
969
+ /** Red line at the current time in today's column. */
970
+ function SchedulerNowIndicator({ className, ...props }: React.ComponentProps<"div">) {
971
+ const s = useScheduler()
972
+ const [now, setNow] = React.useState(() => new Date())
973
+ React.useEffect(() => {
974
+ const timer = setInterval(() => setNow(new Date()), 60_000)
975
+ return () => clearInterval(timer)
976
+ }, [])
977
+ const index = s.days.findIndex((d) => isToday(d))
978
+ if (index === -1) return null
979
+ return (
980
+ <div
981
+ data-slot="scheduler-now-indicator"
982
+ aria-hidden
983
+ className={cn("pointer-events-none absolute z-10 h-px bg-destructive", className)}
984
+ style={{ top: s.timeToY(now), left: `${(index / s.days.length) * 100}%`, width: `${100 / s.days.length}%` }}
985
+ {...props}
986
+ >
987
+ <span className="absolute -top-1 -left-1 size-2 rounded-full bg-destructive" />
988
+ </div>
989
+ )
990
+ }
991
+
992
+ /* ---------------------------------------------------------------------------
993
+ * Month grid
994
+ * ------------------------------------------------------------------------- */
995
+
996
+ function SchedulerMonthGrid({ className, ...props }: React.ComponentProps<"div">) {
997
+ return (
998
+ <div
999
+ data-slot="scheduler-month-grid"
1000
+ className={cn("flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border bg-background", className)}
1001
+ {...props}
1002
+ />
1003
+ )
1004
+ }
1005
+
1006
+ function SchedulerMonthHeader({ className, ...props }: React.ComponentProps<"div">) {
1007
+ const { days } = useScheduler()
1008
+ return (
1009
+ <div
1010
+ data-slot="scheduler-month-header"
1011
+ className={cn("grid grid-cols-7 border-b bg-muted/50 text-xs text-muted-foreground", className)}
1012
+ {...props}
1013
+ >
1014
+ {days.slice(0, 7).map((d) => (
1015
+ <div key={d.toISOString()} className="border-e px-2 py-1.5 text-center font-medium last:border-e-0">
1016
+ {format(d, "EEE")}
1017
+ </div>
1018
+ ))}
1019
+ </div>
1020
+ )
1021
+ }
1022
+
1023
+ function SchedulerMonthBody({ className, children, ...props }: React.ComponentProps<"div">) {
1024
+ const { days } = useScheduler()
1025
+ return (
1026
+ <div
1027
+ data-slot="scheduler-month-body"
1028
+ className={cn("grid min-h-0 flex-1 grid-cols-7 overflow-y-auto", className)}
1029
+ style={{ gridTemplateRows: `repeat(${Math.ceil(days.length / 7)}, minmax(6rem, 1fr))` }}
1030
+ {...props}
1031
+ >
1032
+ {children}
1033
+ </div>
1034
+ )
1035
+ }
1036
+
1037
+ function SchedulerMonthCell({
1038
+ date,
1039
+ className,
1040
+ children,
1041
+ onClick,
1042
+ ...props
1043
+ }: React.ComponentProps<"div"> & { date: Date }) {
1044
+ const s = useScheduler()
1045
+ const outside = !isSameMonth(date, s.date)
1046
+ return (
1047
+ <div
1048
+ data-slot="scheduler-month-cell"
1049
+ data-date={date.toISOString()}
1050
+ data-today={isToday(date) ? "" : undefined}
1051
+ data-outside={outside ? "" : undefined}
1052
+ className={cn(
1053
+ "group/cell flex min-h-0 flex-col gap-0.5 border-e border-b p-1 text-xs data-outside:bg-muted/30 data-outside:text-muted-foreground nth-[7n]:border-e-0",
1054
+ className
1055
+ )}
1056
+ onClick={(event) => {
1057
+ onClick?.(event)
1058
+ if (event.defaultPrevented) return
1059
+ if ((event.target as HTMLElement).closest("[data-slot=scheduler-month-event], button, a")) return
1060
+ s.setDate(date)
1061
+ }}
1062
+ {...props}
1063
+ >
1064
+ <button
1065
+ type="button"
1066
+ data-slot="scheduler-month-day"
1067
+ className="mb-0.5 flex size-6 items-center justify-center self-start rounded-full text-xs font-medium outline-none group-data-today/cell:bg-primary group-data-today/cell:text-primary-foreground hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50"
1068
+ onClick={(event) => {
1069
+ event.stopPropagation()
1070
+ s.setDate(date)
1071
+ s.setView("day")
1072
+ }}
1073
+ >
1074
+ {format(date, "d")}
1075
+ </button>
1076
+ <div className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-hidden">{children}</div>
1077
+ </div>
1078
+ )
1079
+ }
1080
+
1081
+ /** Compact chip for the month view; draggable between cells. */
1082
+ function SchedulerMonthEvent({
1083
+ value,
1084
+ start,
1085
+ end,
1086
+ className,
1087
+ children,
1088
+ onPointerDown,
1089
+ ...props
1090
+ }: React.ComponentProps<"div"> & { value: string; start: Date; end: Date }) {
1091
+ const s = useScheduler()
1092
+ const live = s.interaction?.id === value ? s.interaction : null
1093
+ const eventLike = React.useMemo(() => ({ id: value, start, end }), [value, start, end])
1094
+ return (
1095
+ <div
1096
+ {...props}
1097
+ data-slot="scheduler-month-event"
1098
+ data-value={value}
1099
+ data-dragging={live ? "" : undefined}
1100
+ tabIndex={s.editable ? 0 : undefined}
1101
+ aria-label={`${format(start, "EEE d, p")}`}
1102
+ className={cn(
1103
+ "flex cursor-grab items-center gap-1 truncate rounded-sm border border-primary/30 bg-primary/15 px-1.5 py-px text-[11px]/4 outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/50 active:cursor-grabbing data-dragging:opacity-50",
1104
+ className
1105
+ )}
1106
+ onPointerDown={(event) => {
1107
+ onPointerDown?.(event)
1108
+ if (event.defaultPrevented || event.button !== 0) return
1109
+ s.startMonthMove(eventLike, event)
1110
+ }}
1111
+ >
1112
+ {children}
1113
+ </div>
1114
+ )
1115
+ }
1116
+
1117
+ function SchedulerMonthMore({ className, ...props }: React.ComponentProps<"button">) {
1118
+ return (
1119
+ <button
1120
+ type="button"
1121
+ data-slot="scheduler-month-more"
1122
+ className={cn(
1123
+ "self-start rounded-sm px-1 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground",
1124
+ className
1125
+ )}
1126
+ {...props}
1127
+ />
1128
+ )
1129
+ }
1130
+
1131
+ export {
1132
+ Scheduler,
1133
+ SchedulerToolbar,
1134
+ SchedulerPrevTrigger,
1135
+ SchedulerNextTrigger,
1136
+ SchedulerTodayTrigger,
1137
+ SchedulerTitle,
1138
+ SchedulerViewSelect,
1139
+ SchedulerTimeGrid,
1140
+ SchedulerTimeGridHeader,
1141
+ SchedulerDayHeadings,
1142
+ SchedulerAllDayRow,
1143
+ SchedulerTimeGridBody,
1144
+ SchedulerTimeGutter,
1145
+ SchedulerDayColumns,
1146
+ SchedulerDayColumn,
1147
+ SchedulerEvent,
1148
+ SchedulerEventTitle,
1149
+ SchedulerEventTime,
1150
+ SchedulerEventResizeHandle,
1151
+ SchedulerCreatePreview,
1152
+ SchedulerNowIndicator,
1153
+ SchedulerMonthGrid,
1154
+ SchedulerMonthHeader,
1155
+ SchedulerMonthBody,
1156
+ SchedulerMonthCell,
1157
+ SchedulerMonthEvent,
1158
+ SchedulerMonthMore,
1159
+ useScheduler,
1160
+ layoutLanes,
1161
+ visibleRange,
1162
+ type SchedulerView,
1163
+ type SchedulerEventChange,
1164
+ }