{
  "name": "calendar",
  "type": "registry:block",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/utilities",
    "date-fns",
    "lucide-react",
    "react"
  ],
  "registryDependencies": [
    "button",
    "select"
  ],
  "files": [
    {
      "path": "./items/blocks/calendar/component.tsx",
      "type": "registry:block",
      "target": "./components/block/shuip/calendar.tsx",
      "content": "'use client';\n\nimport {\n  DndContext,\n  type DragEndEvent,\n  PointerSensor,\n  useDraggable,\n  useDroppable,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport { CSS } from '@dnd-kit/utilities';\nimport type { Locale } from 'date-fns';\nimport {\n  addDays,\n  addMinutes,\n  addMonths,\n  eachDayOfInterval,\n  endOfMonth,\n  endOfWeek,\n  format,\n  isSameDay,\n  isSameMonth,\n  setHours,\n  setMinutes,\n  startOfDay,\n  startOfMonth,\n  startOfWeek,\n} from 'date-fns';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport * as React from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { cn } from '@/lib/utils';\n\nexport type CalendarView = 'month' | 'week' | 'day' | 'agenda';\nexport type CalendarEventColor = 'primary' | 'secondary' | 'accent' | 'destructive' | 'muted';\nexport type CalendarSlot = { start: Date; end: Date; allDay: boolean };\n\nexport type CalendarMessages = {\n  today: string;\n  previous: string;\n  next: string;\n  allDay: string;\n  noEvents: string;\n  more: (count: number) => string;\n  views: Record<CalendarView, string>;\n};\n\nexport type CalendarMessagesInput = Partial<Omit<CalendarMessages, 'views'>> & {\n  views?: Partial<Record<CalendarView, string>>;\n};\n\nconst defaultMessages: CalendarMessages = {\n  today: 'Today',\n  previous: 'Previous',\n  next: 'Next',\n  allDay: 'all-day',\n  noEvents: 'No upcoming events',\n  more: (count) => `+${count} more`,\n  views: { month: 'Month', week: 'Week', day: 'Day', agenda: 'Agenda' },\n};\n\ntype CalendarEventLike = Record<string, unknown>;\n\nexport type CalendarRootProps<T extends CalendarEventLike> = {\n  children: React.ReactNode;\n\n  events?: T[];\n  defaultEvents?: T[];\n  onEventsChange?: (next: T[]) => void;\n\n  idField?: keyof T;\n  titleField: keyof T;\n  startField: keyof T;\n  endField: keyof T;\n  allDayField?: keyof T;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n\n  view?: CalendarView;\n  defaultView?: CalendarView;\n  onViewChange?: (v: CalendarView) => void;\n  date?: Date;\n  defaultDate?: Date;\n  onDateChange?: (d: Date) => void;\n\n  views?: CalendarView[];\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  editable?: boolean;\n  locale?: Locale;\n  messages?: CalendarMessagesInput;\n\n  onEventClick?: (item: T) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n};\n\ntype CalendarContextValue<T extends CalendarEventLike = CalendarEventLike> = {\n  view: CalendarView;\n  setView: (v: CalendarView) => void;\n  views: CalendarView[];\n  date: Date;\n  setDate: (d: Date) => void;\n  effectiveView: CalendarView;\n  isMobile: boolean;\n  weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  editable: boolean;\n  locale?: Locale;\n  messages: CalendarMessages;\n  periodLabel: string;\n  goToToday: () => void;\n  goToPrevious: () => void;\n  goToNext: () => void;\n\n  events: T[];\n  getId: (item: T) => string;\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getTitle: (item: T) => string;\n  isAllDay: (item: T) => boolean;\n  getColor: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  onEventClick?: (item: T) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n  resizeEvent: (id: string, end: Date) => void;\n  draggedRef: React.MutableRefObject<boolean>;\n};\n\nconst CalendarContext = React.createContext<CalendarContextValue | null>(null);\n\nexport function useCalendar<T extends CalendarEventLike = CalendarEventLike>(): CalendarContextValue<T> {\n  const ctx = React.use(CalendarContext);\n  if (!ctx) throw new Error('useCalendar must be used within <Calendar.Root>');\n  return ctx as CalendarContextValue<T>;\n}\n\nfunction useControllableState<V>(\n  controlled: V | undefined,\n  defaultValue: V,\n  onChange?: (v: V) => void,\n): [V, (v: V) => void] {\n  const [uncontrolled, setUncontrolled] = React.useState<V>(defaultValue);\n  const isControlled = controlled !== undefined;\n  const value = isControlled ? controlled : uncontrolled;\n  const setValue = React.useCallback(\n    (next: V) => {\n      if (!isControlled) setUncontrolled(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n  return [value, setValue];\n}\n\nfunction useIsMobile(breakpoint = 768): boolean {\n  const subscribe = React.useCallback(\n    (cb: () => void) => {\n      const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);\n      mql.addEventListener('change', cb);\n      return () => mql.removeEventListener('change', cb);\n    },\n    [breakpoint],\n  );\n  const getSnapshot = React.useCallback(\n    () => window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches,\n    [breakpoint],\n  );\n  return React.useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n\nfunction CalendarRoot<T extends CalendarEventLike>({\n  children,\n  events,\n  defaultEvents = [] as T[],\n  onEventsChange,\n  idField = 'id' as keyof T,\n  titleField,\n  startField,\n  endField,\n  allDayField,\n  color,\n  renderEvent,\n  view: viewProp,\n  defaultView = 'month',\n  onViewChange,\n  date: dateProp,\n  defaultDate,\n  onDateChange,\n  views = ['month', 'week', 'day', 'agenda'],\n  weekStartsOn = 1,\n  editable = false,\n  locale,\n  messages: messagesProp,\n  onEventClick,\n  onSlotSelect,\n}: CalendarRootProps<T>) {\n  const messages = React.useMemo<CalendarMessages>(\n    () => ({\n      ...defaultMessages,\n      ...messagesProp,\n      views: { ...defaultMessages.views, ...messagesProp?.views },\n    }),\n    [messagesProp],\n  );\n  const [view, setView] = useControllableState<CalendarView>(viewProp, defaultView, onViewChange);\n  const [date, setDate] = useControllableState<Date>(dateProp, defaultDate ?? startOfDay(new Date()), onDateChange);\n  const [items, setItems] = useControllableState<T[]>(events, defaultEvents, onEventsChange);\n\n  const isMobile = useIsMobile();\n  const effectiveView = isMobile && view === 'week' ? 'day' : view;\n  const draggedRef = React.useRef(false);\n\n  const getStart = React.useCallback((it: T) => it[startField] as unknown as Date, [startField]);\n  const getEnd = React.useCallback((it: T) => it[endField] as unknown as Date, [endField]);\n  const isAllDay = React.useCallback((it: T) => (allDayField ? Boolean(it[allDayField]) : false), [allDayField]);\n  const getId = React.useCallback((it: T) => String(it[idField]), [idField]);\n  const getTitle = React.useCallback((it: T) => String(it[titleField] ?? ''), [titleField]);\n  const getColor = React.useCallback((it: T) => color?.(it), [color]);\n\n  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));\n\n  const handleDragEnd = React.useCallback(\n    (e: DragEndEvent) => {\n      const { active, over, delta } = e;\n      draggedRef.current = Math.abs(delta.x) > 4 || Math.abs(delta.y) > 4;\n      const id = String(active.id);\n      const item = items.find((it) => getId(it) === id);\n      if (!item) return;\n      const oldStart = getStart(item);\n      const oldEnd = getEnd(item);\n      const duration = oldEnd.getTime() - oldStart.getTime();\n      const overDay = (over?.data.current?.day as Date | undefined) ?? oldStart;\n\n      let newStart: Date;\n      if (effectiveView === 'month') {\n        newStart = setMinutes(setHours(startOfDay(overDay), oldStart.getHours()), oldStart.getMinutes());\n      } else {\n        const movedMinutes = minutesSinceDayStart(oldStart) + (delta.y / HOUR_HEIGHT) * 60;\n        newStart = snapToMinutes(addMinutes(startOfDay(overDay), movedMinutes));\n      }\n      if (newStart.getTime() === oldStart.getTime()) return;\n      const next = items.map((it) =>\n        getId(it) === id\n          ? ({ ...it, [startField]: newStart, [endField]: new Date(newStart.getTime() + duration) } as T)\n          : it,\n      );\n      setItems(next);\n    },\n    [items, getId, getStart, getEnd, effectiveView, startField, endField, setItems],\n  );\n\n  const resizeEvent = React.useCallback(\n    (id: string, newEnd: Date) => {\n      setItems(items.map((it) => (getId(it) === id ? ({ ...it, [endField]: newEnd } as T) : it)));\n    },\n    [items, getId, endField, setItems],\n  );\n\n  const periodLabel = React.useMemo(() => {\n    switch (effectiveView) {\n      case 'month':\n        return format(date, 'MMMM yyyy', { locale });\n      case 'week': {\n        const weekStart = startOfWeek(date, { weekStartsOn });\n        const weekEnd = endOfWeek(date, { weekStartsOn });\n        return `${format(weekStart, 'MMM d', { locale })} – ${format(weekEnd, 'MMM d, yyyy', { locale })}`;\n      }\n      case 'day':\n        return format(date, 'PPP', { locale });\n      case 'agenda':\n        return format(date, 'MMMM yyyy', { locale });\n    }\n  }, [effectiveView, date, weekStartsOn, locale]);\n\n  const shift = React.useCallback(\n    (dir: 1 | -1) => {\n      switch (effectiveView) {\n        case 'month':\n          setDate(addMonths(date, dir));\n          break;\n        case 'week':\n          setDate(addDays(date, 7 * dir));\n          break;\n        default:\n          setDate(addDays(date, dir));\n      }\n    },\n    [effectiveView, date, setDate],\n  );\n\n  const goToToday = React.useCallback(() => setDate(startOfDay(new Date())), [setDate]);\n  const goToPrevious = React.useCallback(() => shift(-1), [shift]);\n  const goToNext = React.useCallback(() => shift(1), [shift]);\n\n  const value = React.useMemo<CalendarContextValue<T>>(\n    () => ({\n      view,\n      setView,\n      views,\n      date,\n      setDate,\n      effectiveView,\n      isMobile,\n      weekStartsOn,\n      editable,\n      locale,\n      messages,\n      periodLabel,\n      goToToday,\n      goToPrevious,\n      goToNext,\n      events: items,\n      getId,\n      getStart,\n      getEnd,\n      getTitle,\n      isAllDay,\n      getColor,\n      renderEvent,\n      onEventClick,\n      onSlotSelect,\n      resizeEvent,\n      draggedRef,\n    }),\n    [\n      view,\n      setView,\n      views,\n      date,\n      setDate,\n      effectiveView,\n      isMobile,\n      weekStartsOn,\n      editable,\n      locale,\n      messages,\n      periodLabel,\n      goToToday,\n      goToPrevious,\n      goToNext,\n      items,\n      getId,\n      getStart,\n      getEnd,\n      getTitle,\n      isAllDay,\n      getColor,\n      renderEvent,\n      onEventClick,\n      onSlotSelect,\n      resizeEvent,\n    ],\n  );\n\n  return (\n    <CalendarContext.Provider value={value as unknown as CalendarContextValue}>\n      {editable ? (\n        <DndContext sensors={sensors} onDragEnd={handleDragEnd}>\n          {children}\n        </DndContext>\n      ) : (\n        children\n      )}\n    </CalendarContext.Provider>\n  );\n}\n\nfunction CalendarNav({ className }: { className?: string }) {\n  const { periodLabel, views, view, setView, goToToday, goToPrevious, goToNext, messages } = useCalendar();\n  return (\n    <div className={cn('flex w-full flex-wrap items-center justify-between gap-2', className)}>\n      <div className='flex items-center gap-1'>\n        <Button variant='outline' size='sm' onClick={goToToday}>\n          {messages.today}\n        </Button>\n        <Button variant='outline' size='icon' className='size-8' onClick={goToPrevious} aria-label={messages.previous}>\n          <ChevronLeft className='size-4' />\n        </Button>\n        <Button variant='outline' size='icon' className='size-8' onClick={goToNext} aria-label={messages.next}>\n          <ChevronRight className='size-4' />\n        </Button>\n        <span className='ml-2 text-sm font-medium'>{periodLabel}</span>\n      </div>\n      {views.length > 1 ? (\n        <Select value={view} onValueChange={(v) => setView(v as CalendarView)}>\n          <SelectTrigger className='w-32' size='sm'>\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            {views.map((v) => (\n              <SelectItem key={v} value={v}>\n                {messages.views[v]}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      ) : null}\n    </div>\n  );\n}\n\nfunction CalendarViewComponent({ className }: { className?: string }) {\n  const {\n    effectiveView,\n    date,\n    weekStartsOn,\n    events: items,\n    getStart,\n    getEnd,\n    getId,\n    getTitle,\n    isAllDay,\n    getColor,\n    renderEvent,\n    editable,\n    draggedRef,\n    resizeEvent,\n    onEventClick,\n    onSlotSelect,\n    locale,\n    messages,\n  } = useCalendar();\n\n  const weekDays = React.useMemo(\n    () => eachDayOfInterval({ start: startOfWeek(date, { weekStartsOn }), end: endOfWeek(date, { weekStartsOn }) }),\n    [date, weekStartsOn],\n  );\n\n  const agendaDays = React.useMemo(\n    () => eachDayOfInterval({ start: startOfDay(date), end: addDays(startOfDay(date), 29) }),\n    [date],\n  );\n\n  const body = (() => {\n    switch (effectiveView) {\n      case 'month':\n        return (\n          <MonthView\n            date={date}\n            weekStartsOn={weekStartsOn}\n            items={items}\n            getStart={getStart}\n            getId={getId}\n            getTitle={getTitle}\n            color={getColor}\n            renderEvent={renderEvent}\n            editable={editable}\n            draggedRef={draggedRef}\n            onEventClick={onEventClick}\n            onSlotSelect={onSlotSelect}\n            locale={locale}\n            more={messages.more}\n          />\n        );\n      case 'week':\n      case 'day':\n        return (\n          <TimeGridView\n            days={effectiveView === 'week' ? weekDays : [date]}\n            items={items}\n            getStart={getStart}\n            getEnd={getEnd}\n            getId={getId}\n            getTitle={getTitle}\n            isAllDay={isAllDay}\n            color={getColor}\n            renderEvent={renderEvent}\n            editable={editable}\n            draggedRef={draggedRef}\n            onResize={editable ? resizeEvent : undefined}\n            onEventClick={onEventClick}\n            onSlotSelect={onSlotSelect}\n            locale={locale}\n            allDayLabel={messages.allDay}\n          />\n        );\n      case 'agenda':\n        return (\n          <AgendaView\n            days={agendaDays}\n            items={items}\n            getStart={getStart}\n            getEnd={getEnd}\n            getId={getId}\n            getTitle={getTitle}\n            color={getColor}\n            renderEvent={renderEvent}\n            onEventClick={onEventClick}\n            locale={locale}\n            noEventsLabel={messages.noEvents}\n          />\n        );\n    }\n  })();\n\n  return <div className={cn('w-full', className)}>{body}</div>;\n}\n\nexport const Calendar = {\n  Root: CalendarRoot,\n  Nav: CalendarNav,\n  View: CalendarViewComponent,\n};\n\nfunction monthGrid(date: Date, weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6): Date[] {\n  const start = startOfWeek(startOfMonth(date), { weekStartsOn });\n  const end = endOfWeek(endOfMonth(date), { weekStartsOn });\n  return eachDayOfInterval({ start, end });\n}\n\nconst colorClassMap: Record<CalendarEventColor, string> = {\n  primary: 'bg-primary text-primary-foreground',\n  secondary: 'bg-secondary text-secondary-foreground',\n  accent: 'bg-accent text-accent-foreground',\n  destructive: 'bg-destructive text-destructive-foreground',\n  muted: 'bg-muted text-muted-foreground',\n};\n\nfunction colorClasses(c?: CalendarEventColor): string {\n  return c ? colorClassMap[c] : colorClassMap.primary;\n}\n\nconst HOUR_HEIGHT = 48;\nconst SNAP_MINUTES = 15;\n\nfunction snapToMinutes(date: Date, minutes = SNAP_MINUTES): Date {\n  const ms = minutes * 60_000;\n  return new Date(Math.round(date.getTime() / ms) * ms);\n}\n\nfunction minutesSinceDayStart(date: Date): number {\n  return date.getHours() * 60 + date.getMinutes();\n}\nfunction eventTop(start: Date): number {\n  return (minutesSinceDayStart(start) / 60) * HOUR_HEIGHT;\n}\nfunction eventHeight(start: Date, end: Date): number {\n  const mins = Math.max(15, (end.getTime() - start.getTime()) / 60_000);\n  return (mins / 60) * HOUR_HEIGHT;\n}\n\nfunction offsetToMinutes(clientY: number, columnEl: HTMLElement): number {\n  const rect = columnEl.getBoundingClientRect();\n  return ((clientY - rect.top) / HOUR_HEIGHT) * 60;\n}\n\ntype Creating = { dayIndex: number; startMin: number; endMin: number };\n\ntype LaidOut<T> = { item: T; col: number; cols: number };\n\nfunction layoutDay<T>(dayEvents: T[], getStart: (t: T) => Date, getEnd: (t: T) => Date): LaidOut<T>[] {\n  const sorted = [...dayEvents].sort((a, b) => getStart(a).getTime() - getStart(b).getTime());\n  const colEnds: number[] = [];\n  const placed = sorted.map((item) => {\n    const s = getStart(item).getTime();\n    let col = colEnds.findIndex((end) => end <= s);\n    if (col === -1) {\n      col = colEnds.length;\n      colEnds.push(0);\n    }\n    colEnds[col] = getEnd(item).getTime();\n    return { item, col };\n  });\n  const cols = Math.max(1, colEnds.length);\n  return placed.map((p) => ({ ...p, cols }));\n}\n\nfunction eventBlockStyle(start: Date, end: Date, laidOut: { col: number; cols: number }): React.CSSProperties {\n  const widthPct = 100 / laidOut.cols;\n  return {\n    top: eventTop(start),\n    height: eventHeight(start, end),\n    left: `${laidOut.col * widthPct}%`,\n    width: `calc(${widthPct}% - 2px)`,\n  };\n}\n\nfunction EventBlockContent({\n  title,\n  start,\n  end,\n  label,\n}: {\n  title: string;\n  start: Date;\n  end: Date;\n  label?: React.ReactNode;\n}) {\n  if (label !== undefined) return <>{label}</>;\n  return (\n    <>\n      <span className='block truncate font-medium'>{title}</span>\n      <span className='block truncate opacity-80'>\n        {format(start, 'HH:mm')} – {format(end, 'HH:mm')}\n      </span>\n    </>\n  );\n}\n\nconst eventBlockClass = 'absolute z-10 overflow-hidden rounded px-1 py-0.5 text-left text-xs';\n\nfunction EventBlock({\n  title,\n  start,\n  end,\n  label,\n  laidOut,\n  color,\n  onClick,\n}: {\n  title: string;\n  start: Date;\n  end: Date;\n  label?: React.ReactNode;\n  laidOut: { col: number; cols: number };\n  color?: CalendarEventColor;\n  onClick?: () => void;\n}) {\n  return (\n    <button\n      type='button'\n      data-event=''\n      onClick={onClick}\n      style={eventBlockStyle(start, end, laidOut)}\n      className={cn(eventBlockClass, colorClasses(color))}\n    >\n      <EventBlockContent title={title} start={start} end={end} label={label} />\n    </button>\n  );\n}\n\nfunction DraggableEventBlock({\n  id,\n  title,\n  start,\n  end,\n  realEnd,\n  label,\n  laidOut,\n  color,\n  draggedRef,\n  onClick,\n  onResize,\n  onPreview,\n}: {\n  id: string;\n  title: string;\n  start: Date;\n  end: Date;\n  realEnd: Date;\n  label?: React.ReactNode;\n  laidOut: { col: number; cols: number };\n  color?: CalendarEventColor;\n  draggedRef: React.MutableRefObject<boolean>;\n  onClick?: () => void;\n  onResize?: (id: string, end: Date) => void;\n  onPreview: (end: Date | null) => void;\n}) {\n  const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id });\n  return (\n    <button\n      type='button'\n      data-event=''\n      ref={setNodeRef}\n      {...attributes}\n      {...listeners}\n      onClick={() => {\n        if (draggedRef.current) {\n          draggedRef.current = false;\n          return;\n        }\n        onClick?.();\n      }}\n      style={{\n        ...eventBlockStyle(start, end, laidOut),\n        transform: CSS.Translate.toString(transform),\n        zIndex: isDragging ? 40 : undefined,\n        touchAction: 'none',\n      }}\n      className={cn(eventBlockClass, colorClasses(color))}\n    >\n      <EventBlockContent title={title} start={start} end={end} label={label} />\n      <ResizeHandle id={id} start={start} end={realEnd} onResize={onResize} onPreview={onPreview} />\n    </button>\n  );\n}\n\nfunction ResizeHandle({\n  id,\n  start,\n  end,\n  onResize,\n  onPreview,\n}: {\n  id: string;\n  start: Date;\n  end: Date;\n  onResize?: (id: string, end: Date) => void;\n  onPreview: (end: Date | null) => void;\n}) {\n  const handlePointerDown = (e: React.PointerEvent<HTMLSpanElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    const target = e.target as HTMLElement;\n    target.setPointerCapture(e.pointerId);\n\n    const startY = e.clientY;\n    const originalEnd = end;\n    const minEnd = new Date(start.getTime() + SNAP_MINUTES * 60_000);\n    let finalEnd = originalEnd;\n\n    const compute = (clientY: number) => {\n      const deltaMin = ((clientY - startY) / HOUR_HEIGHT) * 60;\n      let proposed = snapToMinutes(new Date(originalEnd.getTime() + deltaMin * 60_000));\n      if (proposed.getTime() - start.getTime() < SNAP_MINUTES * 60_000) proposed = minEnd;\n      return proposed;\n    };\n\n    const onMove = (moveE: PointerEvent) => {\n      finalEnd = compute(moveE.clientY);\n      onPreview(finalEnd);\n    };\n\n    const onUp = (upE: PointerEvent) => {\n      finalEnd = compute(upE.clientY);\n      target.releasePointerCapture(e.pointerId);\n      target.removeEventListener('pointermove', onMove);\n      target.removeEventListener('pointerup', onUp);\n      onResize?.(id, finalEnd);\n      onPreview(null);\n    };\n\n    target.addEventListener('pointermove', onMove);\n    target.addEventListener('pointerup', onUp);\n  };\n\n  return (\n    <span\n      data-event=''\n      className='absolute inset-x-0 bottom-0 z-20 h-1.5 cursor-ns-resize'\n      onPointerDown={handlePointerDown}\n      onClick={(e) => e.stopPropagation()}\n    />\n  );\n}\n\nconst HOURS = Array.from({ length: 24 }, (_, h) => h);\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nfunction CreatingOverlay({ startMin, endMin }: { startMin: number; endMin: number }) {\n  const lo = Math.min(startMin, endMin);\n  const span = Math.max(15, Math.abs(endMin - startMin));\n  return (\n    <div\n      className='pointer-events-none absolute inset-x-0 z-30 rounded border border-primary bg-primary/30'\n      style={{ top: (lo / 60) * HOUR_HEIGHT, height: (span / 60) * HOUR_HEIGHT }}\n    />\n  );\n}\n\nfunction DayColumnContent<T extends Record<string, unknown>>({\n  laidOut,\n  getStart,\n  getEnd,\n  getId,\n  getTitle,\n  color,\n  renderEvent,\n  editable,\n  draggedRef,\n  resizing,\n  creatingRange,\n  onResize,\n  onPreview,\n  onEventClick,\n}: {\n  laidOut: LaidOut<T>[];\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  editable: boolean;\n  draggedRef: React.MutableRefObject<boolean>;\n  resizing: { id: string; end: Date } | null;\n  creatingRange?: { startMin: number; endMin: number } | null;\n  onResize?: (id: string, end: Date) => void;\n  onPreview: (preview: { id: string; end: Date } | null) => void;\n  onEventClick?: (item: T) => void;\n}) {\n  return (\n    <>\n      {HOURS.map((h) => (\n        <div key={h} className='h-12 border-t first:border-t-0' />\n      ))}\n      {creatingRange ? <CreatingOverlay startMin={creatingRange.startMin} endMin={creatingRange.endMin} /> : null}\n      {laidOut.map((entry) => {\n        const id = getId(entry.item);\n        const start = getStart(entry.item);\n        const realEnd = getEnd(entry.item);\n        const shownEnd = resizing?.id === id ? resizing.end : realEnd;\n        const label = renderEvent ? renderEvent(entry.item) : undefined;\n        return editable ? (\n          <DraggableEventBlock\n            key={id}\n            id={id}\n            title={getTitle(entry.item)}\n            start={start}\n            end={shownEnd}\n            realEnd={realEnd}\n            label={label}\n            laidOut={entry}\n            color={color?.(entry.item)}\n            draggedRef={draggedRef}\n            onClick={() => onEventClick?.(entry.item)}\n            onResize={onResize}\n            onPreview={(end) => onPreview(end ? { id, end } : null)}\n          />\n        ) : (\n          <EventBlock\n            key={id}\n            title={getTitle(entry.item)}\n            start={start}\n            end={realEnd}\n            label={label}\n            laidOut={entry}\n            color={color?.(entry.item)}\n            onClick={() => onEventClick?.(entry.item)}\n          />\n        );\n      })}\n    </>\n  );\n}\n\nconst dayColumnClass = 'relative flex-1 border-l first:border-l-0';\n\nfunction DroppableDayColumn<T extends Record<string, unknown>>({\n  day,\n  dayIndex,\n  creating,\n  onCreatingChange,\n  onSlotSelect,\n  ...content\n}: {\n  day: Date;\n  dayIndex: number;\n  creating: Creating | null;\n  onCreatingChange: (next: Creating | null) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n  laidOut: LaidOut<T>[];\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  editable: boolean;\n  draggedRef: React.MutableRefObject<boolean>;\n  resizing: { id: string; end: Date } | null;\n  onResize?: (id: string, end: Date) => void;\n  onPreview: (preview: { id: string; end: Date } | null) => void;\n  onEventClick?: (item: T) => void;\n}) {\n  const { setNodeRef } = useDroppable({ id: `col-${day.toISOString()}`, data: { day } });\n  const columnRef = React.useRef<HTMLDivElement | null>(null);\n  const startMinRef = React.useRef(0);\n  const canCreate = Boolean(onSlotSelect);\n\n  const mergedRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      setNodeRef(node);\n      columnRef.current = node;\n    },\n    [setNodeRef],\n  );\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!canCreate) return;\n    if ((e.target as HTMLElement).closest('[data-event]')) return;\n    const columnEl = columnRef.current;\n    if (!columnEl) return;\n    e.preventDefault();\n    columnEl.setPointerCapture(e.pointerId);\n    const startMin = offsetToMinutes(e.clientY, columnEl);\n    startMinRef.current = startMin;\n    onCreatingChange({ dayIndex, startMin, endMin: startMin });\n  };\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!creating || creating.dayIndex !== dayIndex) return;\n    const columnEl = columnRef.current;\n    if (!columnEl) return;\n    onCreatingChange({ dayIndex, startMin: startMinRef.current, endMin: offsetToMinutes(e.clientY, columnEl) });\n  };\n\n  const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!creating || creating.dayIndex !== dayIndex) return;\n    const columnEl = columnRef.current;\n    if (columnEl?.hasPointerCapture(e.pointerId)) columnEl.releasePointerCapture(e.pointerId);\n    let lo = Math.min(creating.startMin, creating.endMin);\n    let hi = Math.max(creating.startMin, creating.endMin);\n    if (hi - lo < 5) {\n      lo = creating.startMin;\n      hi = creating.startMin + 60;\n    }\n    const base = startOfDay(day);\n    const start = snapToMinutes(addMinutes(base, lo));\n    let end = snapToMinutes(addMinutes(base, hi));\n    if (end.getTime() - start.getTime() < SNAP_MINUTES * 60_000) {\n      end = new Date(start.getTime() + SNAP_MINUTES * 60_000);\n    }\n    onCreatingChange(null);\n    onSlotSelect?.({ start, end, allDay: false });\n  };\n\n  const creatingRange = creating?.dayIndex === dayIndex ? creating : null;\n\n  return (\n    <div\n      ref={mergedRef}\n      className={cn(dayColumnClass, 'cursor-pointer')}\n      style={{ height: 24 * HOUR_HEIGHT }}\n      onPointerDown={canCreate ? handlePointerDown : undefined}\n      onPointerMove={canCreate ? handlePointerMove : undefined}\n      onPointerUp={canCreate ? handlePointerUp : undefined}\n    >\n      <DayColumnContent {...content} creatingRange={creatingRange} />\n    </div>\n  );\n}\n\nfunction PlainDayColumn<T extends Record<string, unknown>>(content: {\n  laidOut: LaidOut<T>[];\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  editable: boolean;\n  draggedRef: React.MutableRefObject<boolean>;\n  resizing: { id: string; end: Date } | null;\n  onResize?: (id: string, end: Date) => void;\n  onPreview: (preview: { id: string; end: Date } | null) => void;\n  onEventClick?: (item: T) => void;\n}) {\n  return (\n    <div className={dayColumnClass} style={{ height: 24 * HOUR_HEIGHT }}>\n      <DayColumnContent {...content} />\n    </div>\n  );\n}\n\nfunction TimeGridView<T extends Record<string, unknown>>({\n  days,\n  items,\n  getStart,\n  getEnd,\n  getId,\n  getTitle,\n  isAllDay,\n  color,\n  renderEvent,\n  editable,\n  draggedRef,\n  onResize,\n  onEventClick,\n  onSlotSelect: selectSlot,\n  locale,\n  allDayLabel,\n}: {\n  days: Date[];\n  items: T[];\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  isAllDay: (item: T) => boolean;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  editable: boolean;\n  draggedRef: React.MutableRefObject<boolean>;\n  onResize?: (id: string, end: Date) => void;\n  onEventClick?: (item: T) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n  locale?: Locale;\n  allDayLabel: string;\n}) {\n  const [resizing, setResizing] = React.useState<{ id: string; end: Date } | null>(null);\n  const [creating, setCreating] = React.useState<Creating | null>(null);\n\n  const spansAllDay = React.useCallback(\n    (item: T) => isAllDay(item) || getEnd(item).getTime() - getStart(item).getTime() >= DAY_MS,\n    [isAllDay, getEnd, getStart],\n  );\n\n  const canCreateAllDay = editable && Boolean(selectSlot);\n\n  return (\n    <div className='w-full overflow-hidden rounded-md border'>\n      <div className='max-h-[600px] overflow-y-auto'>\n        <div className='sticky top-0 z-30 bg-background'>\n          <div className='flex border-b'>\n            <div className='w-14 shrink-0 border-r' />\n            {days.map((day) => (\n              <div\n                key={day.toISOString()}\n                className='flex-1 border-l p-2 text-center text-xs font-medium first:border-l-0'\n              >\n                {format(day, 'EEE d', { locale })}\n              </div>\n            ))}\n          </div>\n\n          <div className='flex border-b'>\n            <div className='flex w-14 shrink-0 items-center justify-center border-r py-1 text-xs text-muted-foreground'>\n              {allDayLabel}\n            </div>\n            {days.map((day) => {\n              const allDayEvents = items.filter((item) => spansAllDay(item) && isSameDay(getStart(item), day));\n              return (\n                <div\n                  key={day.toISOString()}\n                  className={cn(\n                    'flex min-h-7 flex-1 flex-col gap-1 border-l p-1 first:border-l-0',\n                    canCreateAllDay && 'cursor-pointer',\n                  )}\n                  onClick={\n                    canCreateAllDay\n                      ? () => selectSlot?.({ start: startOfDay(day), end: addDays(startOfDay(day), 1), allDay: true })\n                      : undefined\n                  }\n                >\n                  {allDayEvents.map((item) => (\n                    <button\n                      type='button'\n                      key={getId(item)}\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        onEventClick?.(item);\n                      }}\n                      className={cn(\n                        'w-full truncate rounded px-1 py-0.5 text-left text-xs',\n                        colorClasses(color?.(item)),\n                      )}\n                    >\n                      {renderEvent ? renderEvent(item) : getTitle(item)}\n                    </button>\n                  ))}\n                </div>\n              );\n            })}\n          </div>\n        </div>\n\n        <div className='flex'>\n          <div className='w-14 shrink-0 border-r'>\n            {HOURS.map((h) => (\n              <div key={h} className='h-12 pr-1 text-right text-xs text-muted-foreground'>\n                {`${String(h).padStart(2, '0')}:00`}\n              </div>\n            ))}\n          </div>\n          {days.map((day, dayIndex) => {\n            const timed = items.filter((item) => !spansAllDay(item) && isSameDay(getStart(item), day));\n            const laidOut = layoutDay(timed, getStart, getEnd);\n            const columnProps = {\n              laidOut,\n              getStart,\n              getEnd,\n              getId,\n              getTitle,\n              color,\n              renderEvent,\n              editable,\n              draggedRef,\n              resizing,\n              onResize,\n              onPreview: setResizing,\n              onEventClick,\n            };\n            return editable ? (\n              <DroppableDayColumn\n                key={day.toISOString()}\n                day={day}\n                dayIndex={dayIndex}\n                creating={creating}\n                onCreatingChange={setCreating}\n                onSlotSelect={selectSlot}\n                {...columnProps}\n              />\n            ) : (\n              <PlainDayColumn key={day.toISOString()} {...columnProps} />\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nconst monthPillClass = 'w-full truncate rounded px-1 py-0.5 text-left text-xs';\n\nfunction MonthEventPill<T>({\n  item,\n  id,\n  title,\n  label,\n  color,\n  draggedRef,\n  onEventClick,\n}: {\n  item: T;\n  id: string;\n  title: string;\n  label?: React.ReactNode;\n  color?: CalendarEventColor;\n  draggedRef: React.MutableRefObject<boolean>;\n  onEventClick?: (item: T) => void;\n}) {\n  const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id });\n  return (\n    <button\n      type='button'\n      ref={setNodeRef}\n      {...attributes}\n      {...listeners}\n      onClick={(e) => {\n        e.stopPropagation();\n        if (draggedRef.current) {\n          draggedRef.current = false;\n          return;\n        }\n        onEventClick?.(item);\n      }}\n      style={{ transform: CSS.Translate.toString(transform), zIndex: isDragging ? 40 : undefined, touchAction: 'none' }}\n      className={cn(monthPillClass, colorClasses(color))}\n    >\n      {label ?? title}\n    </button>\n  );\n}\n\nfunction MonthCell<T extends Record<string, unknown>>({\n  day,\n  date,\n  visible,\n  overflow,\n  getId,\n  getTitle,\n  color,\n  renderEvent,\n  draggedRef,\n  onEventClick,\n  onSlotSelect,\n  more,\n}: {\n  day: Date;\n  date: Date;\n  visible: T[];\n  overflow: number;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  draggedRef: React.MutableRefObject<boolean>;\n  onEventClick?: (item: T) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n  more: (count: number) => string;\n}) {\n  const { setNodeRef } = useDroppable({ id: `cell-${day.toISOString()}`, data: { day } });\n  return (\n    <div\n      ref={setNodeRef}\n      className='flex min-h-24 cursor-pointer flex-col gap-1 border-b border-l p-1 hover:bg-accent/50 [&:nth-child(7n+1)]:border-l-0'\n      onClick={() => onSlotSelect?.({ start: startOfDay(day), end: addDays(startOfDay(day), 1), allDay: true })}\n    >\n      <span className={cn('text-xs', !isSameMonth(day, date) && 'text-muted-foreground')}>{format(day, 'd')}</span>\n      {visible.map((item) => (\n        <MonthEventPill\n          key={getId(item)}\n          item={item}\n          id={getId(item)}\n          title={getTitle(item)}\n          label={renderEvent ? renderEvent(item) : undefined}\n          color={color?.(item)}\n          draggedRef={draggedRef}\n          onEventClick={onEventClick}\n        />\n      ))}\n      {overflow > 0 ? <span className='px-1 text-xs text-muted-foreground'>{more(overflow)}</span> : null}\n    </div>\n  );\n}\n\nfunction PlainMonthCell<T extends Record<string, unknown>>({\n  day,\n  date,\n  visible,\n  overflow,\n  getId,\n  getTitle,\n  color,\n  renderEvent,\n  onEventClick,\n  more,\n}: {\n  day: Date;\n  date: Date;\n  visible: T[];\n  overflow: number;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  onEventClick?: (item: T) => void;\n  more: (count: number) => string;\n}) {\n  return (\n    <div className='flex min-h-24 flex-col gap-1 border-b border-l p-1 [&:nth-child(7n+1)]:border-l-0'>\n      <span className={cn('text-xs', !isSameMonth(day, date) && 'text-muted-foreground')}>{format(day, 'd')}</span>\n      {visible.map((item) => (\n        <button\n          type='button'\n          key={getId(item)}\n          className={cn(monthPillClass, colorClasses(color?.(item)))}\n          onClick={() => onEventClick?.(item)}\n        >\n          {renderEvent ? renderEvent(item) : getTitle(item)}\n        </button>\n      ))}\n      {overflow > 0 ? <span className='px-1 text-xs text-muted-foreground'>{more(overflow)}</span> : null}\n    </div>\n  );\n}\n\nfunction MonthView<T extends Record<string, unknown>>({\n  date,\n  weekStartsOn,\n  items,\n  getStart,\n  getId,\n  getTitle,\n  color,\n  renderEvent,\n  editable,\n  draggedRef,\n  onEventClick,\n  onSlotSelect,\n  locale,\n  more,\n}: {\n  date: Date;\n  weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  items: T[];\n  getStart: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  editable: boolean;\n  draggedRef: React.MutableRefObject<boolean>;\n  onEventClick?: (item: T) => void;\n  onSlotSelect?: (range: CalendarSlot) => void;\n  locale?: Locale;\n  more: (count: number) => string;\n}) {\n  const days = monthGrid(date, weekStartsOn);\n  const weekdayLabels = days.slice(0, 7).map((day) => format(day, 'EEE', { locale }));\n\n  return (\n    <div className='w-full overflow-hidden rounded-md border'>\n      <div className='grid grid-cols-7'>\n        {weekdayLabels.map((label) => (\n          <div\n            key={label}\n            className='border-b border-l p-2 text-center text-xs font-medium text-muted-foreground first:border-l-0'\n          >\n            {label}\n          </div>\n        ))}\n        {days.map((day) => {\n          const dayEvents = items\n            .filter((item) => isSameDay(getStart(item), day))\n            .sort((a, b) => getStart(a).getTime() - getStart(b).getTime());\n          const visible = dayEvents.slice(0, 3);\n          const overflow = dayEvents.length - visible.length;\n\n          return editable ? (\n            <MonthCell\n              key={day.toISOString()}\n              day={day}\n              date={date}\n              visible={visible}\n              overflow={overflow}\n              getId={getId}\n              getTitle={getTitle}\n              color={color}\n              renderEvent={renderEvent}\n              draggedRef={draggedRef}\n              onEventClick={onEventClick}\n              onSlotSelect={onSlotSelect}\n              more={more}\n            />\n          ) : (\n            <PlainMonthCell\n              key={day.toISOString()}\n              day={day}\n              date={date}\n              visible={visible}\n              overflow={overflow}\n              getId={getId}\n              getTitle={getTitle}\n              color={color}\n              renderEvent={renderEvent}\n              onEventClick={onEventClick}\n              more={more}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction AgendaView<T extends Record<string, unknown>>({\n  days,\n  items,\n  getStart,\n  getEnd,\n  getId,\n  getTitle,\n  color,\n  renderEvent,\n  onEventClick,\n  locale,\n  noEventsLabel,\n}: {\n  days: Date[];\n  items: T[];\n  getStart: (item: T) => Date;\n  getEnd: (item: T) => Date;\n  getId: (item: T) => string;\n  getTitle: (item: T) => string;\n  color?: (item: T) => CalendarEventColor | undefined;\n  renderEvent?: (item: T) => React.ReactNode;\n  onEventClick?: (item: T) => void;\n  locale?: Locale;\n  noEventsLabel: string;\n}) {\n  const groups = days\n    .map((day) => ({\n      day,\n      events: items\n        .filter((item) => isSameDay(getStart(item), day))\n        .sort((a, b) => getStart(a).getTime() - getStart(b).getTime()),\n    }))\n    .filter((group) => group.events.length > 0);\n\n  if (groups.length === 0) {\n    return <p className='w-full text-sm text-muted-foreground'>{noEventsLabel}</p>;\n  }\n\n  return (\n    <div className='flex max-h-[600px] w-full flex-col gap-4 overflow-y-auto'>\n      {groups.map((group) => (\n        <div key={group.day.toISOString()} className='flex flex-col gap-1'>\n          <div className='text-sm font-medium'>{format(group.day, 'EEEE, MMM d', { locale })}</div>\n          {group.events.map((item) => (\n            <button\n              type='button'\n              key={getId(item)}\n              onClick={() => onEventClick?.(item)}\n              className='flex w-full items-center gap-2 text-left text-sm'\n            >\n              <span className={cn('size-2 rounded-full', colorClasses(color?.(item)))} />\n              <span className='text-muted-foreground'>\n                {format(getStart(item), 'HH:mm')} – {format(getEnd(item), 'HH:mm')}\n              </span>\n              <span>{renderEvent ? renderEvent(item) : getTitle(item)}</span>\n            </button>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
