{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timeline",
  "title": "Timeline",
  "description": "A composable horizontal timeline with draggable slots, rows, drop validation, current-time markers, and custom slot rendering.",
  "dependencies": [
    "@dnd-kit/core",
    "@radix-ui/react-slot",
    "tunnel-rat"
  ],
  "registryDependencies": [
    "button",
    "field",
    "input",
    "popover",
    "select"
  ],
  "files": [
    {
      "path": "registry/corr/components/timeline.tsx",
      "content": "import * as React from \"react\"\nimport {\n  closestCenter,\n  DndContext,\n  DragOverlay,\n  PointerSensor,\n  pointerWithin,\n  useDraggable,\n  useDroppable,\n  useSensor,\n  useSensors,\n  type CollisionDetection,\n  type DragEndEvent,\n  type DragMoveEvent,\n  type DragOverEvent,\n  type DragStartEvent,\n} from \"@dnd-kit/core\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { createPortal } from \"react-dom\"\nimport tunnel from \"tunnel-rat\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldGroup, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverDescription,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface TimelineSlotData {\n  id: string\n  rowId: string\n  startTime: string\n  duration: number\n  [key: string]: unknown\n}\n\nexport interface TimelineRowData {\n  id: string\n  label: string\n  [key: string]: unknown\n}\n\nexport interface TimelineConfig {\n  startHour: number\n  endHour: number\n  snapIntervalMinutes?: number\n  columnWidth?: number\n}\n\ntype DragPreviewTunnel = ReturnType<typeof tunnel>\n\ntype TimelineContextValue = {\n  config: TimelineConfig\n  pixelsPerMinute: number\n  timelineWidth: number\n  timelineRef: React.RefObject<HTMLDivElement | null>\n  columnRef: React.RefObject<HTMLDivElement | null>\n  dragPreviewTunnel: DragPreviewTunnel\n  onSlotPositionChange?: (\n    slotId: string,\n    newTime: string,\n    newRowId: string\n  ) => Promise<boolean> | boolean\n  onSlotResize?: (\n    slotId: string,\n    newTime: string,\n    newDuration: number,\n    isComplete?: boolean\n  ) => Promise<boolean> | boolean | void\n  onValidateDrop?: (\n    slotId: string,\n    newTime: string,\n    newRowId: string\n  ) => boolean\n  onSlotClick?: (slotId: string) => void\n}\n\nconst TimelineContext = React.createContext<TimelineContextValue | null>(null)\n\nconst TIMELINE_AGENDA_BREAKPOINT = 700\n\nexport function useTimeline() {\n  const context = React.useContext(TimelineContext)\n\n  if (!context) {\n    throw new Error(\"Timeline components must be used within TimelineProvider\")\n  }\n\n  return context\n}\n\nexport function timeToMinutes(time: string): number {\n  const [hours, minutes] = time.split(\":\").map(Number)\n  return hours * 60 + minutes\n}\n\nexport function minutesToTime(minutes: number): string {\n  const hours = Math.floor(minutes / 60)\n  const remainingMinutes = minutes % 60\n\n  return `${hours.toString().padStart(2, \"0\")}:${remainingMinutes\n    .toString()\n    .padStart(2, \"0\")}`\n}\n\nexport interface TimelineProviderProps {\n  children: React.ReactNode\n  config: TimelineConfig\n  percentageInView?: number\n  onSlotPositionChange?: (\n    slotId: string,\n    newTime: string,\n    newRowId: string\n  ) => Promise<boolean> | boolean\n  onSlotResize?: (\n    slotId: string,\n    newTime: string,\n    newDuration: number,\n    isComplete?: boolean\n  ) => Promise<boolean> | boolean | void\n  onValidateDrop?: (\n    slotId: string,\n    newTime: string,\n    newRowId: string\n  ) => boolean\n  onSlotClick?: (slotId: string) => void\n  style?: React.CSSProperties\n  className?: string\n}\n\nexport function TimelineProvider({\n  children,\n  config,\n  percentageInView = 100,\n  onSlotPositionChange,\n  onSlotResize,\n  onValidateDrop,\n  onSlotClick,\n  style,\n  className,\n}: TimelineProviderProps) {\n  const [viewportWidth, setViewportWidth] = React.useState(0)\n  const timelineRef = React.useRef<HTMLDivElement>(null)\n  const columnRef = React.useRef<HTMLDivElement>(null)\n  const dragPreviewTunnel = React.useMemo(() => tunnel(), [])\n  const columnWidth = config.columnWidth ?? 112\n\n  React.useEffect(() => {\n    function measure() {\n      if (timelineRef.current) {\n        setViewportWidth(timelineRef.current.clientWidth - columnWidth)\n      }\n    }\n\n    measure()\n    window.addEventListener(\"resize\", measure)\n    const timeout = window.setTimeout(measure, 100)\n\n    return () => {\n      window.removeEventListener(\"resize\", measure)\n      window.clearTimeout(timeout)\n    }\n  }, [columnWidth])\n\n  const totalMinutes = (config.endHour - config.startHour) * 60\n  const basePixelsPerMinute =\n    viewportWidth > 0 ? viewportWidth / totalMinutes : 10\n  const pixelsPerMinute = basePixelsPerMinute * (100 / percentageInView)\n  const timelineWidth = totalMinutes * pixelsPerMinute\n  const contextValue = React.useMemo<TimelineContextValue>(\n    () => ({\n      config,\n      pixelsPerMinute,\n      timelineWidth,\n      timelineRef,\n      columnRef,\n      dragPreviewTunnel,\n      onSlotPositionChange,\n      onSlotResize,\n      onValidateDrop,\n      onSlotClick,\n    }),\n    [\n      config,\n      dragPreviewTunnel,\n      onSlotClick,\n      onSlotPositionChange,\n      onSlotResize,\n      onValidateDrop,\n      pixelsPerMinute,\n      timelineWidth,\n    ]\n  )\n\n  return (\n    <TimelineContext.Provider value={contextValue}>\n      <div\n        data-slot=\"timeline-wrapper\"\n        style={\n          {\n            \"--timeline-column-width\": `${columnWidth}px`,\n            \"--timeline-width\": `${timelineWidth}px`,\n            \"--timeline-pixels-per-minute\": pixelsPerMinute,\n            ...style,\n          } as React.CSSProperties\n        }\n        className={cn(\"relative w-full\", className)}\n      >\n        {children}\n      </div>\n    </TimelineContext.Provider>\n  )\n}\n\ntype TimelineInjectedProps = {\n  slots?: TimelineSlotData[]\n  rows?: TimelineRowData[]\n  activeSlotId?: string | null\n  overRowId?: string | null\n  draggedNewTime?: string | null\n  isValidDrop?: boolean\n  getSnappedDelta?: (deltaX: number) => number\n  _showDropRegion?: boolean\n  _dropRegionTime?: string | null\n}\n\nexport interface TimelineProps {\n  slots: TimelineSlotData[]\n  rows: TimelineRowData[]\n  children: React.ReactNode\n  mobileMode?: \"auto\" | \"agenda\" | \"scroll\"\n  afterGrid?: React.ReactNode\n  onSlotCreate?: (slot: TimelineSlotData) => void\n  onExternalDrop?: (\n    activeId: string,\n    overId: string | null\n  ) => Promise<void> | void\n  className?: string\n}\n\nexport function Timeline({\n  slots,\n  rows,\n  children,\n  mobileMode = \"auto\",\n  afterGrid,\n  onSlotCreate,\n  onExternalDrop,\n  className,\n}: TimelineProps) {\n  const {\n    config,\n    pixelsPerMinute,\n    timelineRef,\n    dragPreviewTunnel,\n    onSlotClick,\n    onSlotPositionChange,\n    onValidateDrop,\n  } = useTimeline()\n  const [mousePosition, setMousePosition] = React.useState<{\n    x: number\n    y: number\n  } | null>(null)\n  const [localActiveSlot, setLocalActiveSlot] = React.useState<string | null>(\n    null\n  )\n  const [localOverRow, setLocalOverRow] = React.useState<string | null>(null)\n  const [localDraggedTime, setLocalDraggedTime] = React.useState<string | null>(\n    null\n  )\n  const [isValid, setIsValid] = React.useState(true)\n  const DragPreviewOut = dragPreviewTunnel.Out\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: { distance: 8 },\n    })\n  )\n  const reduceMotion = useReducedMotion()\n  const snapInterval = config.snapIntervalMinutes ?? 15\n  const columnWidth = config.columnWidth ?? 112\n  const rowIds = React.useMemo(\n    () => new Set(rows.map((row) => String(row.id))),\n    [rows]\n  )\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const [containerWidth, setContainerWidth] = React.useState(0)\n\n  React.useEffect(() => {\n    const element = containerRef.current\n    if (!element) return\n\n    const measure = () => setContainerWidth(element.clientWidth)\n    const observer = new ResizeObserver(measure)\n\n    measure()\n    observer.observe(element)\n\n    return () => observer.disconnect()\n  }, [])\n\n  function snapToInterval(minutes: number): number {\n    return Math.round(minutes / snapInterval) * snapInterval\n  }\n\n  function getSnappedDelta(deltaX: number): number {\n    const deltaMinutes = deltaX / pixelsPerMinute\n    const snappedDeltaMinutes =\n      Math.round(deltaMinutes / snapInterval) * snapInterval\n\n    return snappedDeltaMinutes * pixelsPerMinute\n  }\n\n  function calculateNewTime(originalTime: string, deltaX: number): string {\n    const originalMinutes = timeToMinutes(originalTime)\n    const deltaMinutes = Math.round(deltaX / pixelsPerMinute)\n    const newMinutes = originalMinutes + deltaMinutes\n    const snappedMinutes = snapToInterval(newMinutes)\n    const clampedMinutes = Math.max(\n      config.startHour * 60,\n      Math.min((config.endHour - 1) * 60, snappedMinutes)\n    )\n\n    return minutesToTime(clampedMinutes)\n  }\n\n  function handleMouseMove(event: React.MouseEvent) {\n    if (!timelineRef.current) return\n\n    const rect = timelineRef.current.getBoundingClientRect()\n    setMousePosition({\n      x: event.clientX - rect.left + timelineRef.current.scrollLeft,\n      y: event.clientY - rect.top,\n    })\n  }\n\n  function handleMouseLeave() {\n    setMousePosition(null)\n  }\n\n  const mouseTime = mousePosition\n    ? minutesToTime(\n        Math.floor((mousePosition.x - columnWidth) / pixelsPerMinute) +\n          config.startHour * 60\n      )\n    : null\n\n  function handleDragStart(event: DragStartEvent) {\n    const activeId = String(event.active.id)\n    const isSlotDrag = slots.some((slot) => slot.id === activeId)\n    setLocalActiveSlot(isSlotDrag ? activeId : null)\n  }\n\n  function handleDragOver(event: DragOverEvent) {\n    if (localActiveSlot && event.over && rowIds.has(String(event.over.id))) {\n      setLocalOverRow(String(event.over.id))\n    } else {\n      setLocalOverRow(null)\n      setLocalDraggedTime(null)\n    }\n  }\n\n  function handleDragMove(event: DragMoveEvent) {\n    const { over, active, delta } = event\n\n    if (localActiveSlot && over && rowIds.has(String(over.id))) {\n      const slot = slots.find((item) => item.id === active.id)\n\n      if (slot) {\n        const newTime = calculateNewTime(slot.startTime, delta.x)\n        const overId = String(over.id)\n        setLocalDraggedTime(newTime)\n        setIsValid(\n          onValidateDrop ? onValidateDrop(slot.id, newTime, overId) : true\n        )\n      }\n    } else if (localActiveSlot) {\n      setLocalDraggedTime(null)\n      setIsValid(true)\n    }\n  }\n\n  const collisionDetection: CollisionDetection = (args) => {\n    const pointerCollisions = pointerWithin(args)\n\n    if (pointerCollisions.length > 0) {\n      const externalCollision = pointerCollisions.find(\n        (collision) => !rowIds.has(String(collision.id))\n      )\n\n      if (externalCollision) {\n        return [externalCollision]\n      }\n\n      return pointerCollisions\n    }\n\n    return closestCenter(args)\n  }\n\n  function handleDragCancel() {\n    setLocalActiveSlot(null)\n    setLocalOverRow(null)\n    setLocalDraggedTime(null)\n    setIsValid(true)\n  }\n\n  async function handleDragEnd(event: DragEndEvent) {\n    const { active, over, delta } = event\n\n    setLocalActiveSlot(null)\n    setLocalOverRow(null)\n    setLocalDraggedTime(null)\n    setIsValid(true)\n\n    const activeId = String(active.id)\n\n    if (over) {\n      const overId = String(over.id)\n      const isRowTarget = rows.some((row) => row.id === overId)\n      const slot = slots.find((item) => item.id === active.id)\n\n      if (!slot || !isRowTarget) {\n        await onExternalDrop?.(activeId, overId)\n        return\n      }\n\n      const newTime = calculateNewTime(slot.startTime, delta.x)\n      const newRowId = overId\n\n      if (onValidateDrop && !onValidateDrop(slot.id, newTime, newRowId)) {\n        return\n      }\n\n      if (slot.rowId === newRowId && slot.startTime === newTime) {\n        return\n      }\n\n      await onSlotPositionChange?.(activeId, newTime, newRowId)\n    } else if (onExternalDrop && !slots.some((slot) => slot.id === activeId)) {\n      await onExternalDrop(activeId, null)\n    }\n  }\n\n  const activeSlot = localActiveSlot\n    ? slots.find((slot) => slot.id === localActiveSlot)\n    : null\n  const showAgenda =\n    mobileMode === \"agenda\" ||\n    (mobileMode === \"auto\" &&\n      containerWidth > 0 &&\n      containerWidth < TIMELINE_AGENDA_BREAKPOINT)\n  const showGrid = mobileMode === \"scroll\" || !showAgenda\n\n  return (\n    <DndContext\n      sensors={sensors}\n      collisionDetection={collisionDetection}\n      onDragStart={handleDragStart}\n      onDragOver={handleDragOver}\n      onDragMove={handleDragMove}\n      onDragEnd={handleDragEnd}\n      onDragCancel={handleDragCancel}\n    >\n      <div ref={containerRef} className=\"relative w-full\">\n        {showAgenda ? (\n          <TimelineAgenda\n            slots={slots}\n            rows={rows}\n            onSlotClick={onSlotClick}\n            onSlotCreate={onSlotCreate}\n            reduceMotion={reduceMotion}\n          />\n        ) : null}\n        {showGrid && mousePosition && mouseTime && !localActiveSlot ? (\n          <TimelineMouseIndicator mouseX={mousePosition.x} time={mouseTime} />\n        ) : null}\n\n        {showGrid ? (\n          <div\n            ref={timelineRef}\n            data-slot=\"timeline-grid\"\n            className={cn(\"relative overflow-auto border bg-background\", className)}\n            onMouseMove={handleMouseMove}\n            onMouseLeave={handleMouseLeave}\n          >\n            {React.Children.map(children, (child) => {\n              if (!React.isValidElement<TimelineInjectedProps>(child))\n                return child\n\n              return React.cloneElement(child, {\n                slots,\n                rows,\n                activeSlotId: localActiveSlot,\n                overRowId: localOverRow,\n                draggedNewTime: localDraggedTime,\n                isValidDrop: isValid,\n                getSnappedDelta,\n                _showDropRegion: Boolean(localActiveSlot && localDraggedTime),\n                _dropRegionTime: localDraggedTime,\n              })\n            })}\n          </div>\n        ) : null}\n\n        {afterGrid}\n\n        {typeof document !== \"undefined\"\n          ? createPortal(\n              <DragOverlay dropAnimation={null}>\n                {localActiveSlot && activeSlot ? (\n                  <div\n                    data-slot=\"timeline-drag-overlay\"\n                    style={{\n                      width: `${Math.max(activeSlot.duration * pixelsPerMinute, 60)}px`,\n                      height: \"54px\",\n                      position: \"relative\",\n                    }}\n                  >\n                    <DragPreviewOut />\n                  </div>\n                ) : null}\n              </DragOverlay>,\n              document.body\n            )\n          : null}\n      </div>\n    </DndContext>\n  )\n}\n\nfunction TimelineAgenda({\n  slots,\n  rows,\n  onSlotClick,\n  onSlotCreate,\n  reduceMotion,\n}: {\n  slots: TimelineSlotData[]\n  rows: TimelineRowData[]\n  onSlotClick?: (slotId: string) => void\n  onSlotCreate?: (slot: TimelineSlotData) => void\n  reduceMotion: boolean | null\n}) {\n  const rowMap = React.useMemo(\n    () => new Map(rows.map((row) => [row.id, row])),\n    [rows]\n  )\n  const sortedSlots = React.useMemo(\n    () =>\n      slots\n        .slice()\n        .sort(\n          (left, right) =>\n            timeToMinutes(left.startTime) - timeToMinutes(right.startTime)\n        ),\n    [slots]\n  )\n\n  return (\n    <div\n      data-slot=\"timeline-agenda\"\n      className=\"grid gap-3 rounded-md border bg-background p-3\"\n    >\n      <div className=\"flex items-start justify-between gap-3 border-b pb-3\">\n        <div>\n          <div className=\"text-sm font-semibold\">Agenda</div>\n          <div className=\"text-xs text-muted-foreground\">\n            {sortedSlots.length} scheduled items\n          </div>\n        </div>\n        <div className=\"rounded-full border bg-muted/40 px-2 py-1 text-xs font-medium\">\n          {rows.length} lanes\n        </div>\n      </div>\n      <div className=\"grid gap-2\">\n        {sortedSlots.map((slot, index) => {\n          const row = rowMap.get(slot.rowId)\n          const nextSlot = sortedSlots[index + 1]\n\n          return (\n            <React.Fragment key={slot.id}>\n              <motion.button\n                type=\"button\"\n                onClick={() => onSlotClick?.(slot.id)}\n                className=\"grid grid-cols-[4.5rem_1fr] gap-3 rounded-md border bg-card p-3 text-left shadow-xs transition-colors hover:bg-muted/35 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50\"\n                initial={{ opacity: 0, y: reduceMotion ? 0 : 6 }}\n                animate={{ opacity: 1, y: 0 }}\n                transition={{\n                  duration: reduceMotion ? 0 : 0.16,\n                  delay: reduceMotion ? 0 : index * 0.02,\n                }}\n              >\n                <div className=\"text-xs font-medium tabular-nums text-muted-foreground\">\n                  <div className=\"text-foreground\">{slot.startTime}</div>\n                  <div className=\"mt-1\">\n                    {formatTimelineDuration(slot.duration)}\n                  </div>\n                </div>\n                <div className=\"min-w-0\">\n                  <div className=\"flex items-start justify-between gap-3\">\n                    <div className=\"min-w-0\">\n                      <div className=\"truncate text-sm font-medium\">\n                        {String(slot.title ?? slot.label ?? \"Timeline item\")}\n                      </div>\n                      <div className=\"mt-1 truncate text-xs text-muted-foreground\">\n                        {row?.label ?? slot.rowId}\n                      </div>\n                    </div>\n                    {slot.owner ? (\n                      <div className=\"shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium text-muted-foreground\">\n                        {String(slot.owner)}\n                      </div>\n                    ) : null}\n                  </div>\n                </div>\n              </motion.button>\n              {onSlotCreate ? (\n                <TimelineAgendaCreateSlot\n                  rows={rows}\n                  afterSlot={slot}\n                  beforeSlot={nextSlot}\n                  onSlotCreate={onSlotCreate}\n                />\n              ) : null}\n            </React.Fragment>\n          )\n        })}\n        {sortedSlots.length === 0 && onSlotCreate ? (\n          <TimelineAgendaCreateSlot\n            rows={rows}\n            onSlotCreate={onSlotCreate}\n          />\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nfunction TimelineAgendaCreateSlot({\n  rows,\n  afterSlot,\n  beforeSlot,\n  onSlotCreate,\n}: {\n  rows: TimelineRowData[]\n  afterSlot?: TimelineSlotData\n  beforeSlot?: TimelineSlotData\n  onSlotCreate: (slot: TimelineSlotData) => void\n}) {\n  const startMinutes = afterSlot\n    ? timeToMinutes(afterSlot.startTime) + afterSlot.duration\n    : 9 * 60\n  const nextStartMinutes = beforeSlot ? timeToMinutes(beforeSlot.startTime) : null\n  const defaultStart = minutesToTime(startMinutes)\n  const defaultEnd = minutesToTime(\n    nextStartMinutes && nextStartMinutes > startMinutes\n      ? Math.min(nextStartMinutes, startMinutes + 60)\n      : startMinutes + 60\n  )\n  const [open, setOpen] = React.useState(false)\n  const [title, setTitle] = React.useState(\"New slot\")\n  const [startTime, setStartTime] = React.useState(defaultStart)\n  const [endTime, setEndTime] = React.useState(defaultEnd)\n  const [rowId, setRowId] = React.useState(\n    afterSlot?.rowId ?? rows[0]?.id ?? \"default\"\n  )\n\n  React.useEffect(() => {\n    if (!open) return\n\n    setStartTime(defaultStart)\n    setEndTime(defaultEnd)\n    setRowId(afterSlot?.rowId ?? rows[0]?.id ?? \"default\")\n  }, [afterSlot?.rowId, defaultEnd, defaultStart, open, rows])\n\n  function createSlot(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n\n    const start = timeToMinutes(startTime)\n    const end = timeToMinutes(endTime)\n    const duration = Math.max(15, end > start ? end - start : 60)\n\n    onSlotCreate({\n      id: `slot-${Date.now()}`,\n      rowId,\n      startTime,\n      duration,\n      title: title.trim() || \"New slot\",\n    })\n    setTitle(\"New slot\")\n    setOpen(false)\n  }\n\n  return (\n    <div className=\"group/timeline-create relative -my-1 flex h-2 items-center justify-center\">\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            className=\"absolute top-1/2 z-1 size-6 -translate-y-1/2 scale-95 bg-transparent opacity-0 shadow-none transition-all duration-150 hover:bg-transparent focus:bg-transparent active:bg-transparent data-open:bg-transparent data-[state=open]:bg-transparent aria-expanded:bg-transparent focus-visible:scale-100 focus-visible:bg-transparent focus-visible:opacity-100 group-hover/timeline-create:scale-100 group-hover/timeline-create:opacity-100 group-focus-within/timeline-create:scale-100 group-focus-within/timeline-create:opacity-100\"\n            aria-label=\"Add timeline slot\"\n          >\n            +\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-72\" align=\"center\">\n          <PopoverHeader>\n            <PopoverTitle>Add slot</PopoverTitle>\n            <PopoverDescription>\n              Create a scheduled item in the agenda.\n            </PopoverDescription>\n          </PopoverHeader>\n          <form onSubmit={createSlot}>\n            <FieldGroup className=\"gap-3\">\n              <Field>\n                <FieldLabel htmlFor=\"timeline-slot-title\">Title</FieldLabel>\n                <Input\n                  id=\"timeline-slot-title\"\n                  value={title}\n                  onChange={(event) => setTitle(event.target.value)}\n                />\n              </Field>\n              <div className=\"grid grid-cols-2 gap-2\">\n                <Field>\n                  <FieldLabel htmlFor=\"timeline-slot-start\">From</FieldLabel>\n                  <Input\n                    id=\"timeline-slot-start\"\n                    type=\"time\"\n                    value={startTime}\n                    onChange={(event) => setStartTime(event.target.value)}\n                  />\n                </Field>\n                <Field>\n                  <FieldLabel htmlFor=\"timeline-slot-end\">To</FieldLabel>\n                  <Input\n                    id=\"timeline-slot-end\"\n                    type=\"time\"\n                    value={endTime}\n                    onChange={(event) => setEndTime(event.target.value)}\n                  />\n                </Field>\n              </div>\n              <Field>\n                <FieldLabel>Lane</FieldLabel>\n                <Select value={rowId} onValueChange={setRowId}>\n                  <SelectTrigger>\n                    <SelectValue placeholder=\"Choose lane\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {rows.map((row) => (\n                      <SelectItem key={row.id} value={row.id}>\n                        {row.label}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </Field>\n              <Button type=\"submit\" className=\"w-full\">\n                Add slot\n              </Button>\n            </FieldGroup>\n          </form>\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}\n\nfunction formatTimelineDuration(duration: number) {\n  if (duration < 60) return `${duration}m`\n  const hours = Math.floor(duration / 60)\n  const minutes = duration % 60\n  return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`\n}\n\nexport interface TimelineHeaderProps {\n  className?: string\n  columnLabel?: React.ReactNode\n}\n\nexport function TimelineHeader({\n  className,\n  columnLabel = \"Row\",\n}: TimelineHeaderProps) {\n  const { config, pixelsPerMinute, columnRef } = useTimeline()\n  const hourMarkers = []\n\n  for (let hour = config.startHour; hour < config.endHour; hour += 1) {\n    hourMarkers.push({\n      hour,\n      label: `${hour}:00`,\n      position: (hour - config.startHour) * 60 * pixelsPerMinute,\n    })\n  }\n\n  return (\n    <div\n      data-slot=\"timeline-header\"\n      className={cn(\"sticky top-0 z-10 border-b bg-background\", className)}\n    >\n      <div className=\"flex h-12\">\n        <div\n          ref={columnRef}\n          data-slot=\"timeline-header-column\"\n          className=\"sticky left-0 z-1 flex w-[var(--timeline-column-width)] items-center border-r bg-background px-3 text-xs font-semibold\"\n        >\n          {columnLabel}\n        </div>\n        <div data-slot=\"timeline-header-markers\" className=\"relative flex-1\">\n          {hourMarkers.map((marker) => (\n            <div\n              key={marker.hour}\n              data-slot=\"timeline-hour-marker\"\n              className=\"absolute top-0 bottom-0 flex items-center pl-2 text-xs text-muted-foreground\"\n              style={{ left: `${marker.position}px` }}\n            >\n              {marker.label}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport interface TimelineRowProps extends TimelineInjectedProps {\n  row: TimelineRowData\n  slots: TimelineSlotData[]\n  children: (slot: TimelineSlotData) => React.ReactNode\n  renderRowHeader?: (row: TimelineRowData) => React.ReactNode\n  renderRowExtras?: (row: TimelineRowData) => React.ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport function TimelineRow({\n  row,\n  slots,\n  children,\n  renderRowHeader,\n  renderRowExtras,\n  className,\n  asChild,\n  ...props\n}: TimelineRowProps) {\n  const { config, pixelsPerMinute, timelineWidth } = useTimeline()\n  const Comp = asChild ? Slot : \"div\"\n  const { setNodeRef, isOver } = useDroppable({\n    id: row.id,\n  })\n  const rowSlots = slots.filter((slot) => slot.rowId === row.id)\n  const isValidDrop = props.isValidDrop !== false\n  const isHovered = isOver || props.overRowId === row.id\n  const hourMarkers = []\n\n  for (let hour = config.startHour; hour <= config.endHour; hour += 1) {\n    hourMarkers.push({\n      hour,\n      position: (hour - config.startHour) * 60 * pixelsPerMinute,\n    })\n  }\n\n  const quarterHourMarkers = []\n  const totalMinutes = (config.endHour - config.startHour) * 60\n\n  for (let minutes = 15; minutes < totalMinutes; minutes += 15) {\n    if (minutes % 60 !== 0) {\n      quarterHourMarkers.push({\n        position: minutes * pixelsPerMinute,\n      })\n    }\n  }\n\n  return (\n    <Comp\n      ref={setNodeRef}\n      data-slot=\"timeline-row\"\n      data-state={\n        isHovered ? (isValidDrop ? \"hover-valid\" : \"hover-invalid\") : \"idle\"\n      }\n      className={cn(\n        \"flex h-12 border-b\",\n        isHovered && isValidDrop && \"ring-2 ring-blue-500 ring-inset\",\n        isHovered && !isValidDrop && \"ring-2 ring-red-500 ring-inset\",\n        className\n      )}\n    >\n      <div\n        data-slot=\"timeline-row-label\"\n        className=\"sticky left-0 z-[5] flex w-[var(--timeline-column-width)] items-center border-r bg-inherit text-xs font-medium\"\n      >\n        {renderRowHeader ? (\n          renderRowHeader(row)\n        ) : (\n          <span className=\"px-3\">{row.label}</span>\n        )}\n      </div>\n\n      <div\n        data-slot=\"timeline-row-grid\"\n        className=\"relative flex-1\"\n        style={{ width: `${timelineWidth}px` }}\n      >\n        {quarterHourMarkers.map((marker, index) => (\n          <div\n            key={`quarter-${index}`}\n            data-slot=\"timeline-grid-line-quarter\"\n            className=\"absolute top-0 bottom-0 w-px bg-border/30\"\n            style={{ left: `${marker.position}px` }}\n          />\n        ))}\n        {hourMarkers.map((marker) => (\n          <div\n            key={marker.hour}\n            data-slot=\"timeline-grid-line-hour\"\n            className=\"absolute top-0 bottom-0 w-px bg-border\"\n            style={{ left: `${marker.position}px` }}\n          />\n        ))}\n\n        {renderRowExtras ? renderRowExtras(row) : null}\n\n        {rowSlots.map((slot) => {\n          const slotElement = children(slot)\n\n          return (\n            <React.Fragment key={slot.id}>\n              {React.isValidElement<TimelineSlotInjectedProps>(slotElement)\n                ? React.cloneElement(slotElement, {\n                    activeSlotId: props.activeSlotId,\n                    getSnappedDelta: props.getSnappedDelta,\n                  })\n                : slotElement}\n            </React.Fragment>\n          )\n        })}\n\n        {isHovered && props.draggedNewTime && props.activeSlotId ? (\n          <TimelineDropGhost\n            activeSlotId={props.activeSlotId}\n            allSlots={slots}\n            newTime={props.draggedNewTime}\n            isValid={isValidDrop}\n            pixelsPerMinute={pixelsPerMinute}\n            config={config}\n          />\n        ) : null}\n      </div>\n    </Comp>\n  )\n}\n\ntype TimelineSlotInjectedProps = {\n  activeSlotId?: string | null\n  getSnappedDelta?: (deltaX: number) => number\n}\n\nexport interface TimelineSlotProps extends TimelineSlotInjectedProps {\n  slot: TimelineSlotData\n  children: React.ReactNode\n  className?: string\n  asChild?: boolean\n}\n\nexport function TimelineSlot({\n  slot,\n  children,\n  className,\n  asChild,\n  ...props\n}: TimelineSlotProps) {\n  const {\n    config,\n    pixelsPerMinute,\n    onSlotClick,\n    onSlotResize,\n    dragPreviewTunnel,\n  } = useTimeline()\n  const Comp = asChild ? Slot : \"div\"\n  const DragPreviewIn = dragPreviewTunnel.In\n  const { attributes, listeners, setNodeRef, isDragging, transform } =\n    useDraggable({\n      id: slot.id,\n    })\n  const startMinutes = timeToMinutes(slot.startTime)\n  const endMinutes = startMinutes + slot.duration\n  const left = (startMinutes - config.startHour * 60) * pixelsPerMinute\n  const width = slot.duration * pixelsPerMinute\n  const isActiveSlot = props.activeSlotId === slot.id\n  const snapInterval = config.snapIntervalMinutes ?? 15\n\n  function handleResizeStart(\n    event: React.PointerEvent,\n    edge: \"left\" | \"right\"\n  ) {\n    if (!onSlotResize) return\n\n    event.stopPropagation()\n    event.preventDefault()\n\n    const target = event.currentTarget as HTMLElement\n    target.setPointerCapture(event.pointerId)\n\n    const initialX = event.clientX\n    const initialStart = startMinutes\n    const initialEnd = endMinutes\n    const timelineStart = config.startHour * 60\n    const timelineEnd = config.endHour * 60\n    const minDuration = snapInterval\n\n    function getDeltaMinutes(clientX: number) {\n      const rawDeltaMinutes = (clientX - initialX) / pixelsPerMinute\n      return Math.round(rawDeltaMinutes / snapInterval) * snapInterval\n    }\n\n    function getNextSize(clientX: number) {\n      const deltaMinutes = getDeltaMinutes(clientX)\n\n      if (edge === \"left\") {\n        const newStart = Math.max(\n          timelineStart,\n          Math.min(initialStart + deltaMinutes, initialEnd - minDuration)\n        )\n\n        return {\n          start: newStart,\n          duration: initialEnd - newStart,\n        }\n      }\n\n      const newEnd = Math.max(\n        initialStart + minDuration,\n        Math.min(initialEnd + deltaMinutes, timelineEnd)\n      )\n\n      return {\n        start: initialStart,\n        duration: newEnd - initialStart,\n      }\n    }\n\n    function handlePointerMove(moveEvent: PointerEvent) {\n      const next = getNextSize(moveEvent.clientX)\n      void onSlotResize?.(\n        slot.id,\n        minutesToTime(next.start),\n        next.duration,\n        false\n      )\n    }\n\n    function handlePointerUp(upEvent: PointerEvent) {\n      const next = getNextSize(upEvent.clientX)\n      void onSlotResize?.(\n        slot.id,\n        minutesToTime(next.start),\n        next.duration,\n        true\n      )\n      target.releasePointerCapture(upEvent.pointerId)\n      window.removeEventListener(\"pointermove\", handlePointerMove)\n      window.removeEventListener(\"pointerup\", handlePointerUp)\n    }\n\n    window.addEventListener(\"pointermove\", handlePointerMove)\n    window.addEventListener(\"pointerup\", handlePointerUp)\n  }\n\n  const style =\n    transform && props.getSnappedDelta\n      ? {\n          left: `${left}px`,\n          width: `${Math.max(width, 60)}px`,\n          top: 0,\n          bottom: 0,\n          transform: `translate3d(${props.getSnappedDelta(transform.x)}px, 0px, 0)`,\n        }\n      : {\n          left: `${left}px`,\n          width: `${Math.max(width, 60)}px`,\n          top: 0,\n          bottom: 0,\n        }\n  const slotContent = (\n    <Comp\n      data-slot=\"timeline-slot\"\n      data-state={isDragging ? \"dragging\" : \"idle\"}\n      data-active={isActiveSlot}\n      className={cn(\n        \"group/timeline-slot absolute inset-1 cursor-move overflow-hidden rounded transition-all\",\n        isDragging\n          ? \"opacity-40 shadow-sm ring-2 ring-foreground/50\"\n          : \"shadow-md\",\n        onSlotClick && \"cursor-pointer hover:ring-2 hover:ring-foreground/30\",\n        className\n      )}\n      onClick={(event: React.MouseEvent) => {\n        if (onSlotClick && !isDragging) {\n          event.stopPropagation()\n          onSlotClick(slot.id)\n        }\n      }}\n      style={\n        {\n          \"--slot-start-time\": slot.startTime,\n          \"--slot-duration\": `${slot.duration}min`,\n        } as React.CSSProperties\n      }\n    >\n      {children}\n      {onSlotResize ? (\n        <>\n          <div\n            aria-hidden=\"true\"\n            className=\"absolute top-0 bottom-0 left-0 z-20 w-4 -translate-x-1/2 cursor-col-resize\"\n            onPointerDown={(event) => handleResizeStart(event, \"left\")}\n          />\n          <div\n            aria-hidden=\"true\"\n            className=\"absolute top-0 right-0 bottom-0 z-20 w-4 translate-x-1/2 cursor-col-resize\"\n            onPointerDown={(event) => handleResizeStart(event, \"right\")}\n          />\n          <div className=\"pointer-events-none absolute top-1 bottom-1 left-0 w-1 rounded-r-sm bg-transparent group-hover/timeline-slot:bg-foreground/20\" />\n          <div className=\"pointer-events-none absolute top-1 right-0 bottom-1 w-1 rounded-l-sm bg-transparent group-hover/timeline-slot:bg-foreground/20\" />\n        </>\n      ) : null}\n    </Comp>\n  )\n\n  return (\n    <div\n      ref={setNodeRef}\n      {...listeners}\n      {...attributes}\n      className=\"absolute\"\n      style={style}\n    >\n      {slotContent}\n\n      {isActiveSlot && isDragging ? (\n        <DragPreviewIn>\n          <Comp\n            data-slot=\"timeline-slot-preview\"\n            className={cn(\n              \"h-full w-full cursor-move overflow-hidden rounded shadow-lg\",\n              className\n            )}\n            style={\n              {\n                \"--slot-start-time\": slot.startTime,\n                \"--slot-duration\": `${slot.duration}min`,\n              } as React.CSSProperties\n            }\n          >\n            {children}\n          </Comp>\n        </DragPreviewIn>\n      ) : null}\n    </div>\n  )\n}\n\nexport interface TimelineSlotLabelProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean\n}\n\nexport function TimelineSlotLabel({\n  asChild,\n  className,\n  ...props\n}: TimelineSlotLabelProps) {\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"timeline-slot-label\"\n      className={cn(\"truncate text-xs font-medium\", className)}\n      {...props}\n    />\n  )\n}\n\nexport interface TimelineSlotContentProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean\n}\n\nexport function TimelineSlotContent({\n  asChild,\n  className,\n  ...props\n}: TimelineSlotContentProps) {\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"timeline-slot-content\"\n      className={cn(\"text-xs\", className)}\n      {...props}\n    />\n  )\n}\n\nexport function TimelineMouseIndicator({\n  mouseX,\n  time,\n}: {\n  mouseX: number\n  time: string\n}) {\n  return (\n    <div\n      data-slot=\"timeline-mouse-indicator\"\n      className=\"pointer-events-none absolute top-0 bottom-0 z-20\"\n      style={{ left: `${mouseX}px` }}\n    >\n      <div className=\"absolute top-0 bottom-0 left-0 w-px bg-accent\" />\n      <div className=\"absolute top-0 left-1/2 -translate-x-1/2 rounded bg-accent px-2 py-1 text-xs font-semibold whitespace-nowrap text-accent-foreground shadow-md\">\n        {time}\n      </div>\n    </div>\n  )\n}\n\nexport interface TimelineDropRegionProps {\n  startTime: string\n  duration: number\n}\n\nexport function TimelineDropRegion({\n  startTime,\n  duration,\n}: TimelineDropRegionProps) {\n  const { config, pixelsPerMinute } = useTimeline()\n  const columnWidth = config.columnWidth ?? 112\n  const startMinutes = timeToMinutes(startTime)\n  const endMinutes = startMinutes + duration\n  const endTime = minutesToTime(endMinutes)\n  const startPosition =\n    (startMinutes - config.startHour * 60) * pixelsPerMinute + columnWidth\n  const endPosition =\n    (endMinutes - config.startHour * 60) * pixelsPerMinute + columnWidth\n  const width = endPosition - startPosition\n\n  return (\n    <div\n      data-slot=\"timeline-drop-region\"\n      className=\"pointer-events-none absolute top-0 bottom-0 z-[12]\"\n      style={{ left: `${startPosition}px`, width: `${width}px` }}\n    >\n      <div className=\"absolute top-2 left-1/2 -translate-x-1/2 rounded bg-accent px-3 py-1.5 text-sm font-semibold whitespace-nowrap text-accent-foreground shadow-md\">\n        {startTime} - {endTime}\n      </div>\n      <div className=\"absolute top-0 bottom-0 left-0 w-0.5 bg-accent\" />\n      <div className=\"absolute top-0 right-0 bottom-0 w-0.5 bg-accent\" />\n      <div className=\"absolute inset-0 bg-accent/[0.07]\" />\n    </div>\n  )\n}\n\nexport interface TimelineCurrentTimeProps {\n  className?: string\n  nowLabel?: string\n}\n\nexport function TimelineCurrentTime({\n  className,\n  nowLabel = \"Now\",\n}: TimelineCurrentTimeProps) {\n  const { config, pixelsPerMinute } = useTimeline()\n  const columnWidth = config.columnWidth ?? 112\n  const [now, setNow] = React.useState(new Date())\n\n  React.useEffect(() => {\n    const interval = window.setInterval(() => setNow(new Date()), 60000)\n    return () => window.clearInterval(interval)\n  }, [])\n\n  const currentMinutes = now.getHours() * 60 + now.getMinutes()\n  const position =\n    (currentMinutes - config.startHour * 60) * pixelsPerMinute + columnWidth\n\n  if (\n    currentMinutes < config.startHour * 60 ||\n    currentMinutes > config.endHour * 60\n  ) {\n    return null\n  }\n\n  return (\n    <div\n      data-slot=\"timeline-current-time\"\n      className={cn(\n        \"pointer-events-none absolute top-0 bottom-0 z-[15] w-0.5 bg-secondary\",\n        className\n      )}\n      style={{ left: `${position}px` }}\n    >\n      <div className=\"absolute top-0 left-1/2 z-50 -translate-x-1/2 rounded bg-secondary px-2 py-1 text-xs font-medium whitespace-nowrap text-foreground shadow-md\">\n        {nowLabel}: {minutesToTime(currentMinutes)}\n      </div>\n    </div>\n  )\n}\n\nexport interface TimelineGridProps extends TimelineInjectedProps {\n  children: React.ReactNode\n  className?: string\n}\n\nexport function TimelineGrid({\n  children,\n  className,\n  ...props\n}: TimelineGridProps) {\n  const { timelineWidth } = useTimeline()\n  const activeSlot =\n    props._showDropRegion && props.slots\n      ? props.slots.find((slot) => slot.id === props.activeSlotId)\n      : null\n\n  return (\n    <div\n      data-slot=\"timeline-grid-container\"\n      className={cn(\"relative\", className)}\n      style={{ minWidth: `${timelineWidth + 200}px` }}\n    >\n      {React.Children.map(children, (child) => {\n        if (!React.isValidElement<TimelineInjectedProps>(child)) return child\n        return React.cloneElement(child, props)\n      })}\n\n      {props._showDropRegion && props._dropRegionTime && activeSlot ? (\n        <TimelineDropRegion\n          startTime={props._dropRegionTime}\n          duration={activeSlot.duration}\n        />\n      ) : null}\n    </div>\n  )\n}\n\nfunction TimelineDropGhost({\n  activeSlotId,\n  allSlots,\n  newTime,\n  isValid,\n  config,\n  pixelsPerMinute,\n}: {\n  activeSlotId: string\n  allSlots: TimelineSlotData[]\n  newTime: string\n  isValid: boolean\n  config: TimelineConfig\n  pixelsPerMinute: number\n}) {\n  const slot = allSlots.find((item) => item.id === activeSlotId)\n  if (!slot || !isValid) return null\n\n  const startMinutes = timeToMinutes(newTime)\n  const left = (startMinutes - config.startHour * 60) * pixelsPerMinute\n  const width = slot.duration * pixelsPerMinute\n\n  return (\n    <div\n      data-slot=\"timeline-drop-ghost\"\n      className=\"pointer-events-none absolute rounded-md bg-foreground/20\"\n      style={{\n        left: `${left}px`,\n        width: `${Math.max(width, 60)}px`,\n        top: \"2px\",\n        bottom: \"2px\",\n        zIndex: 100,\n      }}\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}