{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "availability",
  "type": "registry:block",
  "title": "Availability",
  "description": "A weekly availability designer with draggable and resizable time slots.",
  "dependencies": [
    "lucide-react",
    "@radix-ui/react-slot",
    "class-variance-authority",
    "@dnd-kit/core",
    "tunnel-rat",
    "nanoid"
  ],
  "registryDependencies": ["button"],
  "files": [
    {
      "path": "registry/abui/ui/availability.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Clock, Settings, X } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DndContext,\n  DragOverlay,\n  useSensor,\n  useSensors,\n  PointerSensor,\n  useDraggable,\n  useDroppable,\n  DragStartEvent,\n  DragEndEvent,\n  DragMoveEvent,\n  DragOverEvent,\n} from \"@dnd-kit/core\"\nimport tunnel from \"tunnel-rat\"\nimport { nanoid } from \"nanoid\"\n\n// --- Types ---\n\nexport interface TimeSpan {\n  active?: boolean\n  id: string\n  week_day: number // 0-6 - Sunday-Saturday\n  start_time: string // \"HH:mm\"\n  end_time: string // \"HH:mm\"\n}\n\ninterface AvailabilityProps {\n  value?: TimeSpan[]\n  onValueChange?: (value: TimeSpan[]) => void\n  disabled?: TimeSpan[] // Disabled regions where no events can exist\n  days?: number[] // 0-6\n  showAllDays?: boolean // If true, renders all 7 days, but disables interactions on days not in 'days' array\n  timeIncrements?: number // minutes, default 30\n  startTime?: number // hour 0-23, default 7\n  endTime?: number // hour 0-23, default 23\n  useAmPm?: boolean\n  mergeAdjacent?: boolean // default true - merge spans that touch end-to-end\n  slotClassName?: string // className for time slot items (default: \"bg-muted\")\n  className?: string\n}\n\n// --- Utils ---\n\nconst timeToMinutes = (time: string) => {\n  const [h, m] = time.split(\":\").map(Number)\n  return h * 60 + m\n}\n\nconst minutesToTime = (minutes: number) => {\n  const h = Math.floor(minutes / 60)\n  const m = minutes % 60\n  return `${h.toString().padStart(2, \"0\")}:${m.toString().padStart(2, \"0\")}`\n}\n\nconst formatDisplayTime = (time: string, useAmPm: boolean) => {\n  if (!useAmPm) return time\n  const [h, m] = time.split(\":\").map(Number)\n  const ampm = h >= 12 ? \"PM\" : \"AM\"\n  const h12 = h % 12 || 12\n  return `${h12}:${m.toString().padStart(2, \"0\")} ${ampm}`\n}\n\n// Helper to generate a simple unique ID (not crypto secure but sufficient for UI)\nconst generateId = () => nanoid()\n\nconst DAYS = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"]\n\n/**\n * Merges adjacent (contiguous) time spans on the same day.\n * Adjacent means one span's end_time equals another's start_time.\n * The merged span keeps the id of the earliest span.\n */\nconst mergeAdjacentSpans = (spans: TimeSpan[]): TimeSpan[] => {\n  if (spans.length === 0) return spans\n\n  // Group by day\n  const byDay = new Map<number, TimeSpan[]>()\n  spans.forEach(span => {\n    const daySpans = byDay.get(span.week_day) || []\n    daySpans.push(span)\n    byDay.set(span.week_day, daySpans)\n  })\n\n  const merged: TimeSpan[] = []\n\n  // Process each day\n  byDay.forEach(daySpans => {\n    // Sort by start time\n    const sorted = [...daySpans].sort((a, b) => timeToMinutes(a.start_time) - timeToMinutes(b.start_time))\n\n    let current = sorted[0]\n\n    for (let i = 1; i < sorted.length; i++) {\n      const next = sorted[i]\n\n      // Check if current and next are adjacent (touching)\n      if (current.end_time === next.start_time) {\n        // Merge: extend current to include next\n        current = {\n          ...current,\n          end_time: next.end_time,\n          // Keep the earliest span's id (current is already earlier due to sorting)\n        }\n      } else {\n        // Not adjacent, push current and move to next\n        merged.push(current)\n        current = next\n      }\n    }\n\n    // Push the last span\n    merged.push(current)\n  })\n\n  return merged\n}\n\n// --- Hooks ---\n\n/**\n * Hook to handle creation dragging logic on a day column.\n */\nfunction useCalendarCreation({\n  containerRef,\n  timeIncrements,\n  startTime,\n  endTime,\n  events,\n  disabledEvents = [],\n  onCreate,\n  colIndex,\n  isDayDisabled = false,\n}: {\n  containerRef: React.RefObject<HTMLDivElement | null>\n  timeIncrements: number\n  startTime: number\n  endTime: number\n  events: TimeSpan[]\n  disabledEvents?: TimeSpan[]\n  onCreate: (dayIndex: number, start: number, end: number) => void\n  colIndex: number\n  isDayDisabled?: boolean\n}) {\n  const [isCreating, setIsCreating] = React.useState(false)\n  const [creationStart, setCreationStart] = React.useState<number | null>(null)\n  const [currentMouseY, setCurrentMouseY] = React.useState<number | null>(null)\n\n  const totalMinutes = (endTime - startTime) * 60\n  const startOffset = startTime * 60\n\n  // Combine events and disabled regions for constraints\n  // Sort all items by start time to determine safe zones\n  const sortedConstraints = React.useMemo(() => {\n    return [...events, ...disabledEvents].sort((a, b) => timeToMinutes(a.start_time) - timeToMinutes(b.start_time))\n  }, [events, disabledEvents])\n\n  const getMinutesFromY = (y: number) => {\n    if (!containerRef.current) return 0\n    const rect = containerRef.current.getBoundingClientRect()\n    const relativeY = y - rect.top\n    const percentage = Math.max(0, Math.min(1, relativeY / rect.height))\n    const minutes = percentage * totalMinutes + startOffset\n    return Math.round(minutes / timeIncrements) * timeIncrements\n  }\n\n  const handlePointerDown = (e: React.PointerEvent) => {\n    if (isDayDisabled) return // No interaction if day is fully disabled\n    if (e.target !== e.currentTarget) return // Only trigger on empty space\n    // Prevent default drag behavior and text selection, but allow touch scrolling if not handled\n    e.preventDefault()\n\n    // Capture pointer to track movement even if it leaves the element\n    containerRef.current?.setPointerCapture(e.pointerId)\n\n    const startMins = getMinutesFromY(e.clientY)\n\n    // Check strict overlap at start point (cannot start creation inside an event or disabled region)\n    const isOverlapping = sortedConstraints.some(ev => {\n      const s = timeToMinutes(ev.start_time)\n      const e = timeToMinutes(ev.end_time)\n      return startMins >= s && startMins < e\n    })\n    if (isOverlapping) return\n\n    // Find constraints\n    const prevEvent = sortedConstraints.filter(ev => timeToMinutes(ev.end_time) <= startMins).pop()\n    const nextEvent = sortedConstraints.find(ev => timeToMinutes(ev.start_time) >= startMins)\n\n    const minStartMins = prevEvent ? timeToMinutes(prevEvent.end_time) : startOffset\n    const maxEndMins = nextEvent ? timeToMinutes(nextEvent.start_time) : endTime * 60\n\n    setCreationStart(startMins)\n    setCurrentMouseY(startMins)\n    setIsCreating(true)\n\n    const handlePointerMove = (ev: PointerEvent) => {\n      const currentMins = getMinutesFromY(ev.clientY)\n      // Clamp to constraints\n      const clampedMins = Math.max(minStartMins, Math.min(currentMins, maxEndMins))\n      setCurrentMouseY(clampedMins)\n    }\n\n    const handlePointerUp = (ev: PointerEvent) => {\n      const currentMins = getMinutesFromY(ev.clientY)\n\n      let finalStart = Math.min(startMins, currentMins)\n      let finalEnd = Math.max(startMins, currentMins)\n\n      // Clamp to constraints\n      finalStart = Math.max(minStartMins, finalStart)\n      finalEnd = Math.min(maxEndMins, finalEnd)\n\n      // Ensure minimum size\n      if (finalEnd - finalStart < timeIncrements) {\n        finalEnd = Math.min(finalStart + timeIncrements, maxEndMins)\n      }\n\n      // Click-to-create logic (if essentially no drag occurred, try to make a 1-hour slot)\n      if (finalEnd - finalStart <= timeIncrements) {\n        const oneHourEnd = finalStart + 60\n        finalEnd = Math.min(oneHourEnd, maxEndMins)\n      }\n\n      if (finalEnd > finalStart) {\n        onCreate(colIndex, finalStart, finalEnd)\n      }\n\n      setIsCreating(false)\n      setCreationStart(null)\n      setCurrentMouseY(null)\n\n      // Release pointer capture\n      containerRef.current?.releasePointerCapture(ev.pointerId)\n\n      // Cleanup listeners (though using setPointerCapture implicitly handles some of this, explicit cleanup is safe)\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  return {\n    isCreating,\n    creationStart,\n    currentMouseY,\n    totalMinutes,\n    startOffset,\n    sortedConstraints,\n    handlePointerDown,\n  }\n}\n\n// --- Components ---\n\n// Context to share dragging state across components\nconst AvailabilityDragContext = React.createContext<{\n  dragPreviewTunnel: ReturnType<typeof tunnel>\n  activeId: string | null\n  activeSpan: TimeSpan | null\n  overDayIndex: number | null\n  deltaY: number\n  timeIncrements: number\n  isDropValid: boolean\n} | null>(null)\n\nexport function Availability({\n  value = [],\n  onValueChange,\n  disabled = [],\n  days = [0, 1, 2, 3, 4, 5, 6],\n  showAllDays = true,\n  timeIncrements = 30,\n  startTime = 7,\n  endTime = 23,\n  useAmPm = false,\n  mergeAdjacent = true,\n  slotClassName = \"bg-muted\",\n  className,\n}: AvailabilityProps) {\n  const [internalValue, setInternalValue] = React.useState<TimeSpan[]>(value)\n\n  // Drag state\n  const dragPreviewTunnel = React.useMemo(() => tunnel(), [])\n  const [activeId, setActiveId] = React.useState<string | null>(null)\n  const [overDayIndex, setOverDayIndex] = React.useState<number | null>(null)\n  const [deltaY, setDeltaY] = React.useState(0)\n  const [isDropValid, setIsDropValid] = React.useState(true)\n\n  const mainContainerRef = React.useRef<HTMLDivElement>(null)\n\n  // Determine which days to render\n  const renderedDays = React.useMemo(() => {\n    if (showAllDays) {\n      return [0, 1, 2, 3, 4, 5, 6]\n    }\n    return days\n  }, [days, showAllDays])\n\n  React.useEffect(() => {\n    setInternalValue(value)\n  }, [value])\n\n  const updateValue = (newValue: TimeSpan[], shouldMerge = false) => {\n    const finalValue = shouldMerge && mergeAdjacent ? mergeAdjacentSpans(newValue) : newValue\n    setInternalValue(finalValue)\n    onValueChange?.(finalValue)\n  }\n\n  const handleResize = (id: string, newStart: string, newEnd: string, isComplete = false) => {\n    const newValue = internalValue.map(span => {\n      if (span.id === id) {\n        return { ...span, start_time: newStart, end_time: newEnd }\n      }\n      return span\n    })\n    updateValue(newValue, isComplete)\n  }\n\n  const handleCreate = (dayIndex: number, startMinutes: number, endMinutes: number) => {\n    const newSpan: TimeSpan = {\n      id: generateId(),\n      week_day: dayIndex, // Directly use the dayIndex (0-6)\n      start_time: minutesToTime(startMinutes),\n      end_time: minutesToTime(endMinutes),\n      active: true,\n    }\n    updateValue([...internalValue, newSpan], true)\n  }\n\n  const handleDelete = (id: string) => {\n    updateValue(\n      internalValue.filter(s => s.id !== id),\n      true,\n    )\n  }\n\n  const handleMove = (id: string, newStart: string, newEnd: string, newDayIndex: number) => {\n    const newValue = internalValue.map(span => {\n      if (span.id === id) {\n        return { ...span, start_time: newStart, end_time: newEnd, week_day: newDayIndex }\n      }\n      return span\n    })\n    updateValue(newValue, true)\n  }\n\n  // Validation helper\n  const validatePlacement = (\n    span: TimeSpan,\n    targetDayIndex: number,\n    deltaY: number,\n    containerHeight: number,\n  ): { isValid: boolean; newStart: number; duration: number } => {\n    const totalMinutes = (endTime - startTime) * 60\n    const pixelsPerMinute = containerHeight / totalMinutes\n    const deltaMinutesRaw = deltaY / pixelsPerMinute\n    const deltaMinutes = Math.round(deltaMinutesRaw / timeIncrements) * timeIncrements\n\n    const originalStart = timeToMinutes(span.start_time)\n    const duration = timeToMinutes(span.end_time) - originalStart\n\n    const newStart = originalStart + deltaMinutes\n    const newEnd = newStart + duration\n\n    // Check bounds\n    const dayStartMins = startTime * 60\n    const dayEndMins = endTime * 60\n\n    if (newStart < dayStartMins || newEnd > dayEndMins) {\n      return { isValid: false, newStart, duration }\n    }\n\n    // Check if day is active (in allowed days list)\n    if (!days.includes(targetDayIndex)) {\n      return { isValid: false, newStart, duration }\n    }\n\n    // Check collisions with active events\n    const dayEvents = internalValue.filter(e => e.week_day === targetDayIndex && e.id !== span.id)\n    const hasEventOverlap = dayEvents.some(e => {\n      const eStart = timeToMinutes(e.start_time)\n      const eEnd = timeToMinutes(e.end_time)\n      return newStart < eEnd && newEnd > eStart\n    })\n\n    if (hasEventOverlap) {\n      return { isValid: false, newStart, duration }\n    }\n\n    // Check collisions with disabled regions\n    const dayDisabled = disabled.filter(e => e.week_day === targetDayIndex)\n    const hasDisabledOverlap = dayDisabled.some(e => {\n      const eStart = timeToMinutes(e.start_time)\n      const eEnd = timeToMinutes(e.end_time)\n      return newStart < eEnd && newEnd > eStart\n    })\n\n    if (hasDisabledOverlap) {\n      return { isValid: false, newStart, duration }\n    }\n\n    return { isValid: true, newStart, duration }\n  }\n\n  // DnD Handlers\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: {\n        distance: 8,\n      },\n    }),\n  )\n\n  const handleDragStart = (event: DragStartEvent) => {\n    setActiveId(event.active.id as string)\n    setDeltaY(0)\n    setOverDayIndex(null)\n    setIsDropValid(true)\n  }\n\n  const handleDragMove = (event: DragMoveEvent) => {\n    setDeltaY(event.delta.y)\n    checkValidity(event.active.id as string, event.over?.id, event.delta.y)\n  }\n\n  const handleDragOver = (event: DragOverEvent) => {\n    if (event.over) {\n      const dayIndex = parseInt(event.over.id.toString().replace(\"day-\", \"\"), 10)\n      if (!isNaN(dayIndex)) {\n        setOverDayIndex(dayIndex)\n      }\n    } else {\n      setOverDayIndex(null)\n    }\n    checkValidity(event.active.id as string, event.over?.id, event.delta.y)\n  }\n\n  const checkValidity = (activeId: string, overId: string | number | undefined, currentDeltaY: number) => {\n    if (!mainContainerRef.current || !overId) {\n      setIsDropValid(false)\n      return\n    }\n\n    const span = internalValue.find(s => s.id === activeId)\n    if (!span) return\n\n    const targetDayIndex = parseInt(overId.toString().replace(\"day-\", \"\"), 10)\n    if (isNaN(targetDayIndex)) {\n      setIsDropValid(false)\n      return\n    }\n\n    const result = validatePlacement(span, targetDayIndex, currentDeltaY, mainContainerRef.current.clientHeight)\n    setIsDropValid(result.isValid)\n  }\n\n  const handleDragCancel = () => {\n    setActiveId(null)\n    setOverDayIndex(null)\n    setDeltaY(0)\n    setIsDropValid(true)\n  }\n\n  const handleDragEnd = (event: DragEndEvent) => {\n    const { active, delta, over } = event\n    setActiveId(null)\n    setOverDayIndex(null)\n    setDeltaY(0)\n    setIsDropValid(true)\n\n    const span = internalValue.find(s => s.id === active.id)\n    if (!span || !mainContainerRef.current || !over) return\n\n    const targetDayIndex = parseInt(over.id.toString().replace(\"day-\", \"\"), 10)\n    if (isNaN(targetDayIndex)) return\n\n    // Final validation before commit\n    const { isValid, newStart, duration } = validatePlacement(\n      span,\n      targetDayIndex,\n      delta.y,\n      mainContainerRef.current.clientHeight,\n    )\n\n    if (!isValid) {\n      // Invalid drop, do nothing (snaps back)\n      return\n    }\n\n    const newEndVal = newStart + duration\n    handleMove(span.id, minutesToTime(newStart), minutesToTime(newEndVal), targetDayIndex)\n  }\n\n  const activeSpan = React.useMemo(() => internalValue.find(s => s.id === activeId) || null, [activeId, internalValue])\n\n  return (\n    <DndContext\n      sensors={sensors}\n      onDragStart={handleDragStart}\n      onDragMove={handleDragMove}\n      onDragOver={handleDragOver}\n      onDragEnd={handleDragEnd}\n      onDragCancel={handleDragCancel}\n    >\n      <AvailabilityDragContext.Provider\n        value={{\n          dragPreviewTunnel,\n          activeId,\n          activeSpan,\n          overDayIndex,\n          deltaY,\n          timeIncrements,\n          isDropValid,\n        }}\n      >\n        <div\n          suppressHydrationWarning\n          className={cn(\n            \"flex h-[600px] w-full flex-col overflow-hidden rounded-md border bg-background select-none touch-none\",\n            className,\n          )}\n        >\n          {/* Header */}\n          <div className=\"flex w-full border-b bg-muted/40\">\n            <div className=\"w-16 flex-shrink-0 border-r p-2 text-xs font-medium text-muted-foreground\" />\n            <div className=\"flex flex-1\">\n              {renderedDays.map(dayIndex => {\n                const isActive = days.includes(dayIndex)\n                return (\n                  <div\n                    key={dayIndex}\n                    className={cn(\n                      \"flex-1 border-r px-2 py-3 text-center text-sm font-medium last:border-r-0\",\n                      !isActive && \"bg-muted/30 text-muted-foreground\",\n                    )}\n                  >\n                    {DAYS[dayIndex]}\n                  </div>\n                )\n              })}\n            </div>\n          </div>\n\n          {/* Body */}\n          <div className=\"flex flex-1 overflow-y-auto relative\" ref={mainContainerRef}>\n            {/* Time Labels */}\n            <div className=\"w-16 flex-shrink-0 border-r bg-muted/10 flex flex-col\">\n              {Array.from({ length: endTime - startTime }).map((_, i) => {\n                const hour = startTime + i\n                return (\n                  <div\n                    key={hour}\n                    className=\"flex-1 border-b border-dashed border-muted-foreground/20 relative flex items-center justify-start pl-3\"\n                  >\n                    <span className=\"text-xs text-muted-foreground\">{formatDisplayTime(`${hour}:00`, useAmPm)}</span>\n                  </div>\n                )\n              })}\n            </div>\n\n            {/* Days Grid */}\n            <div className=\"flex flex-1 relative\">\n              <div className=\"absolute inset-0 pointer-events-none flex flex-col\">\n                {Array.from({ length: endTime - startTime }).map((_, i) => (\n                  <div\n                    key={i}\n                    className=\"flex-1 border-b border-dashed border-foreground/10 dark:border-muted/60 w-full relative\"\n                  />\n                ))}\n              </div>\n\n              {renderedDays.map((dayIndex, i) => {\n                // Check if this day is in the active list\n                const isActive = days.includes(dayIndex)\n\n                return (\n                  <DayColumn\n                    key={dayIndex}\n                    dayIndex={dayIndex}\n                    colIndex={i}\n                    startTime={startTime}\n                    endTime={endTime}\n                    timeIncrements={timeIncrements}\n                    events={internalValue.filter(e => e.week_day === dayIndex)}\n                    disabledEvents={disabled.filter(e => e.week_day === dayIndex)}\n                    onCreate={handleCreate}\n                    onResize={handleResize}\n                    onDelete={handleDelete}\n                    useAmPm={useAmPm}\n                    isDayDisabled={!isActive}\n                    slotClassName={slotClassName}\n                  />\n                )\n              })}\n            </div>\n          </div>\n\n          {/* Drag Overlay */}\n          <DragOverlay>\n            {activeSpan && (\n              <div className=\"w-full h-full cursor-grabbing relative opacity-80\">\n                <dragPreviewTunnel.Out />\n              </div>\n            )}\n          </DragOverlay>\n        </div>\n      </AvailabilityDragContext.Provider>\n    </DndContext>\n  )\n}\n\ninterface DayColumnProps {\n  dayIndex: number\n  colIndex: number\n  startTime: number\n  endTime: number\n  timeIncrements: number\n  events: TimeSpan[]\n  disabledEvents?: TimeSpan[]\n  onCreate: (dayIndex: number, start: number, end: number) => void\n  onResize: (id: string, start: string, end: string, isComplete?: boolean) => void\n  onDelete: (id: string) => void\n  useAmPm: boolean\n  isDayDisabled?: boolean\n  slotClassName?: string\n}\n\nfunction DayColumn({\n  dayIndex,\n  colIndex,\n  startTime,\n  endTime,\n  timeIncrements,\n  events,\n  disabledEvents = [],\n  onCreate,\n  onResize,\n  onDelete,\n  useAmPm,\n  isDayDisabled = false,\n  slotClassName = \"bg-muted\",\n}: DayColumnProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n\n  const context = React.useContext(AvailabilityDragContext)\n\n  // Make the column a droppable zone for day detection\n  // Only droppable if not disabled\n  const { setNodeRef } = useDroppable({\n    id: `day-${dayIndex}`,\n    disabled: isDayDisabled,\n  })\n\n  const mergedRef = (node: HTMLDivElement | null) => {\n    containerRef.current = node\n    setNodeRef(node)\n  }\n\n  const { isCreating, creationStart, currentMouseY, totalMinutes, startOffset, sortedConstraints, handlePointerDown } =\n    useCalendarCreation({\n      containerRef,\n      timeIncrements,\n      startTime,\n      endTime,\n      events,\n      disabledEvents,\n      onCreate,\n      colIndex,\n      isDayDisabled,\n    })\n\n  // Calculate ghost position\n  const showGhost = context?.activeId && context.overDayIndex === dayIndex && containerRef.current && !isDayDisabled\n\n  const ghostStyle = React.useMemo(() => {\n    if (!showGhost || !context?.activeSpan || !containerRef.current) return null\n\n    const span = context.activeSpan\n    const containerHeight = containerRef.current.clientHeight\n    const pixelsPerMinute = containerHeight / totalMinutes\n\n    const deltaMinutesRaw = context.deltaY / pixelsPerMinute\n    const deltaMinutes = Math.round(deltaMinutesRaw / timeIncrements) * timeIncrements\n\n    const originalStart = timeToMinutes(span.start_time)\n    const duration = timeToMinutes(span.end_time) - originalStart\n\n    // Calculate proposed start\n    const newStart = originalStart + deltaMinutes\n\n    // Determine visual feedback based on validity\n    // If we are here, context.isDropValid has already determined if this position is valid\n    // We just need to render the ghost at the proposed position (not clamped, so user sees why it fails)\n\n    return {\n      top: `${((newStart - startOffset) / totalMinutes) * 100}%`,\n      height: `${(duration / totalMinutes) * 100}%`,\n    }\n  }, [\n    context?.activeId,\n    context?.deltaY,\n    context?.activeSpan,\n    context?.overDayIndex,\n    timeIncrements,\n    totalMinutes,\n    startOffset,\n    showGhost,\n  ])\n\n  return (\n    <div\n      ref={mergedRef}\n      className={cn(\n        \"flex-1 relative border-r last:border-r-0 min-w-[100px] touch-none\",\n        isDayDisabled && \"bg-muted/30\",\n        context?.activeId && \"z-10\", // Ensure z-index when dragging\n      )}\n      onPointerDown={handlePointerDown}\n    >\n      {/* Full Day Disabled Overlay */}\n      {isDayDisabled && (\n        <div\n          className=\"absolute inset-0 bg-muted/10 pointer-events-none z-20\"\n          style={{\n            backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(128,128,128,0.15) 5px, rgba(128,128,128,0.15) 10px)`,\n          }}\n        />\n      )}\n\n      {/* Disabled Regions */}\n      {disabledEvents.map((disabled, i) => {\n        const startMins = timeToMinutes(disabled.start_time)\n        const endMins = timeToMinutes(disabled.end_time)\n        const duration = endMins - startMins\n\n        return (\n          <div\n            key={`disabled-${i}`}\n            className=\"absolute left-0 right-0 bg-muted/40 bg-stripes-muted pointer-events-none z-0\"\n            style={{\n              top: `${((startMins - startOffset) / totalMinutes) * 100}%`,\n              height: `${(duration / totalMinutes) * 100}%`,\n              // Striped pattern using CSS gradient\n              backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(128,128,128,0.15) 5px, rgba(128,128,128,0.15) 10px)`,\n            }}\n          />\n        )\n      })}\n\n      {/* Ghost Element */}\n      {ghostStyle && (\n        <div\n          className={cn(\n            \"absolute left-1 right-1 rounded-md border z-0 pointer-events-none transition-all duration-100 ease-out\",\n            context?.isDropValid ? \"bg-foreground/20 border-foreground/30\" : \"bg-destructive/20 border-destructive/50\",\n          )}\n          style={ghostStyle}\n        />\n      )}\n\n      {events.map((event, i) => {\n        // Calculate neighbors considering BOTH events and disabled regions for resizing constraints\n        // sortedConstraints contains both. We just need to find neighbors relative to THIS event in that list.\n\n        // We need a clean list of constraints excluding the event itself\n        const otherConstraints = sortedConstraints.filter(e => e.id !== event.id)\n\n        const eventStart = timeToMinutes(event.start_time)\n        const eventEnd = timeToMinutes(event.end_time)\n\n        const prevItem = otherConstraints.filter(e => timeToMinutes(e.end_time) <= eventStart).pop()\n        const nextItem = otherConstraints.find(e => timeToMinutes(e.start_time) >= eventEnd)\n\n        const minStart = prevItem ? timeToMinutes(prevItem.end_time) : startOffset\n        const maxEnd = nextItem ? timeToMinutes(nextItem.start_time) : endTime * 60\n\n        const isDragging = context?.activeId === event.id\n\n        return (\n          <DraggableTimeSpan\n            key={event.id}\n            span={event}\n            startTime={startTime}\n            endTime={endTime}\n            minStart={minStart}\n            maxEnd={maxEnd}\n            onResize={onResize}\n            onDelete={onDelete}\n            useAmPm={useAmPm}\n            timeIncrements={timeIncrements}\n            containerRef={containerRef}\n            isDragging={isDragging}\n            isLocked={isDayDisabled} // Pass lock state if day is disabled\n            slotClassName={slotClassName}\n          />\n        )\n      })}\n\n      {isCreating && creationStart !== null && currentMouseY !== null && (\n        <div\n          className=\"absolute left-0 right-0 mx-1 rounded bg-primary/30 border border-primary z-20 pointer-events-none\"\n          style={{\n            top: `${((Math.min(creationStart, currentMouseY) - startOffset) / totalMinutes) * 100}%`,\n            height: `${(Math.abs(currentMouseY - creationStart) / totalMinutes) * 100}%`,\n          }}\n        />\n      )}\n    </div>\n  )\n}\n\ninterface DraggableTimeSpanProps {\n  span: TimeSpan\n  startTime: number\n  endTime: number\n  minStart: number\n  maxEnd: number\n  onResize: (id: string, start: string, end: string, isComplete?: boolean) => void\n  onDelete: (id: string) => void\n  useAmPm: boolean\n  timeIncrements: number\n  containerRef: React.RefObject<HTMLDivElement | null>\n  isDragging?: boolean\n  isLocked?: boolean\n  slotClassName?: string\n}\n\nfunction DraggableTimeSpan({\n  span,\n  startTime,\n  endTime,\n  minStart,\n  maxEnd,\n  onResize,\n  onDelete,\n  useAmPm,\n  timeIncrements,\n  containerRef,\n  isDragging,\n  isLocked = false,\n  slotClassName = \"bg-muted\",\n}: DraggableTimeSpanProps) {\n  const context = React.useContext(AvailabilityDragContext)\n  const { attributes, listeners, setNodeRef } = useDraggable({\n    id: span.id,\n    data: span,\n    disabled: isLocked, // Disable drag if locked\n  })\n\n  const startMinutes = timeToMinutes(span.start_time)\n  const endMinutes = timeToMinutes(span.end_time)\n  const totalMinutes = (endTime - startTime) * 60\n  const startOffset = startTime * 60\n  const durationMinutes = endMinutes - startMinutes\n\n  // Apply transform if dragging\n  const style: React.CSSProperties = {\n    top: `${((startMinutes - startOffset) / totalMinutes) * 100}%`,\n    height: `${(durationMinutes / totalMinutes) * 100}%`,\n    // We hide the original element when dragging, but we render the tunnel content\n    opacity: isDragging ? 0 : isLocked ? 0.6 : 1, // Fade if locked\n  }\n\n  const handleResizeStart = (e: React.PointerEvent, edge: \"top\" | \"bottom\") => {\n    if (isLocked) return\n    e.stopPropagation()\n    e.preventDefault()\n\n    // Capture pointer for resize drag\n    const target = e.target as HTMLElement\n    target.setPointerCapture(e.pointerId)\n\n    const initialY = e.clientY\n    const initialStart = startMinutes\n    const initialEnd = endMinutes\n\n    // Re-calculate local constraints for resize (using passed props or logic)\n    // We can trust the parent passed correct minStart/maxEnd for neighbors\n\n    const handlePointerMove = (ev: PointerEvent) => {\n      if (!containerRef.current) return\n\n      const containerHeight = containerRef.current.clientHeight\n      const pixelsPerMinute = containerHeight / totalMinutes\n      const deltaY = ev.clientY - initialY\n      const deltaMinutes = Math.round(deltaY / pixelsPerMinute / timeIncrements) * timeIncrements\n\n      if (deltaMinutes === 0) return\n\n      let newStart = initialStart\n      let newEnd = initialEnd\n\n      if (edge === \"top\") {\n        newStart += deltaMinutes\n        // For resize we still want clamping to neighbors\n        if (newStart < minStart) newStart = minStart\n        if (newStart >= newEnd - timeIncrements) newStart = newEnd - timeIncrements\n      } else {\n        newEnd += deltaMinutes\n        if (newEnd > maxEnd) newEnd = maxEnd\n        if (newEnd <= newStart + timeIncrements) newEnd = newStart + timeIncrements\n      }\n\n      // During drag: don't merge (isComplete = false)\n      onResize(span.id, minutesToTime(newStart), minutesToTime(newEnd), false)\n    }\n\n    const handlePointerUp = (ev: PointerEvent) => {\n      target.releasePointerCapture(ev.pointerId)\n\n      // Final commit with clamping logic repeated\n      if (containerRef.current) {\n        const containerHeight = containerRef.current.clientHeight\n        const pixelsPerMinute = containerHeight / totalMinutes\n        const deltaY = ev.clientY - initialY\n        const deltaMinutes = Math.round(deltaY / pixelsPerMinute / timeIncrements) * timeIncrements\n\n        let newStart = initialStart\n        let newEnd = initialEnd\n\n        if (edge === \"top\") {\n          newStart += deltaMinutes\n          if (newStart < minStart) newStart = minStart\n          if (newStart >= newEnd - timeIncrements) newStart = newEnd - timeIncrements\n        } else {\n          newEnd += deltaMinutes\n          if (newEnd > maxEnd) newEnd = maxEnd\n          if (newEnd <= newStart + timeIncrements) newEnd = newStart + timeIncrements\n        }\n\n        onResize(span.id, minutesToTime(newStart), minutesToTime(newEnd), true)\n      }\n\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 canResize = !isLocked\n\n  const content = (\n    <>\n      {/* Resize Handle Top - Increased hit area */}\n      {canResize && (\n        <div\n          className=\"absolute top-0 left-0 right-0 h-4 -mt-2 cursor-row-resize z-10\"\n          onPointerDown={e => handleResizeStart(e, \"top\")}\n        />\n      )}\n      {/* Visual Top Handle */}\n      <div className=\"absolute top-0 left-1 right-1 h-1 bg-transparent group-hover:bg-foreground/20 rounded-t-sm\" />\n\n      {/* Drag Handle Area (middle) - Use dnd-kit listeners here */}\n      <div\n        className={cn(\n          \"absolute inset-0 top-2 bottom-2 z-0\",\n          canResize ? \"cursor-grab active:cursor-grabbing\" : \"cursor-default\",\n        )}\n        {...listeners}\n        {...attributes}\n      />\n\n      <TimeSpanCard\n        span={span}\n        useAmPm={useAmPm}\n        duration={durationMinutes / 60}\n        onDelete={canResize ? () => onDelete(span.id) : undefined}\n      />\n\n      {/* Resize Handle Bottom - Increased hit area */}\n      {canResize && (\n        <div\n          className=\"absolute bottom-0 left-0 right-0 h-4 -mb-2 cursor-row-resize z-10\"\n          onPointerDown={e => handleResizeStart(e, \"bottom\")}\n        />\n      )}\n      {/* Visual Bottom Handle */}\n      <div className=\"absolute bottom-0 left-1 right-1 h-1 bg-transparent group-hover:bg-foreground/20 rounded-b-sm\" />\n    </>\n  )\n\n  return (\n    <>\n      <div\n        ref={setNodeRef}\n        style={style}\n        className={cn(\n          \"absolute left-1 right-1 rounded border p-3 shadow-sm text-xs group overflow-hidden touch-none\",\n          slotClassName,\n          isDragging && \"opacity-0\", // Hide original while dragging\n          isLocked && \"border-dashed opacity-60 cursor-default bg-muted/50\",\n        )}\n      >\n        {content}\n      </div>\n\n      {/* Tunnel visual content to overlay if dragging */}\n      {isDragging && context && (\n        <context.dragPreviewTunnel.In>\n          <div\n            className={cn(\n              \"absolute left-0 right-0 rounded-md border p-3 shadow-lg text-xs overflow-hidden h-full w-full\",\n              context.isDropValid ? \"border-foreground/50 bg-foreground/10\" : \"border-destructive/50 bg-destructive/20\",\n            )}\n          >\n            {/* Render content without interactive handlers for the preview */}\n            <div className=\"absolute top-0 left-1 right-1 h-1 bg-transparent group-hover:bg-foreground/20 rounded-t-sm\" />\n            <TimeSpanCard\n              span={span}\n              useAmPm={useAmPm}\n              duration={durationMinutes / 60}\n              // No delete button in preview\n            />\n            <div className=\"absolute bottom-0 left-1 right-1 h-1 bg-transparent group-hover:bg-foreground/20 rounded-b-sm\" />\n          </div>\n        </context.dragPreviewTunnel.In>\n      )}\n    </>\n  )\n}\n\nfunction TimeSpanCard({\n  span,\n  useAmPm,\n  duration,\n  onDelete,\n}: {\n  span: TimeSpan\n  useAmPm: boolean\n  duration?: number\n  onDelete?: () => void\n}) {\n  const calculatedDuration = duration || (timeToMinutes(span.end_time) - timeToMinutes(span.start_time)) / 60\n\n  return (\n    <div className=\"h-full flex flex-col relative items-between text-foreground timespan-inner-area pointer-events-none\">\n      <div className=\"flex flex-col gap-0.5 text-inherit\">\n        <p className=\"font-semibold leading-none\">{formatDisplayTime(span.start_time, useAmPm)}</p>\n        <div className=\"flex items-center gap-0.5\">\n          <Clock className=\"h-2 w-2\" />{\" \"}\n          <p className=\"text-[10px] opacity-80\">{calculatedDuration.toFixed(1).replace(\".0\", \"\")}h</p>\n        </div>\n      </div>\n      {onDelete && (\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-5 w-5 hover:bg-foreground/5 dark:hover:bg-foreground/10 -mt-1 -mr-1 absolute top-0 right-0 z-20 pointer-events-auto\"\n          onPointerDown={e => {\n            e.stopPropagation() // Prevent drag/resize from card\n          }}\n          onClick={e => {\n            e.stopPropagation()\n            onDelete()\n          }}\n        >\n          <X className=\"h-3 w-3\" />\n        </Button>\n      )}\n      <div className=\"flex flex-col gap-1 mt-auto text-inherit\">\n        {!onDelete && <Settings className=\"h-3 w-3 opacity-50\" />}\n        <p className=\"font-semibold leading-none !text-inherit\">{formatDisplayTime(span.end_time, useAmPm)}</p>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/ui/button.tsx",
      "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n)\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n  }) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return <Comp data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props} />\n}\n\nexport { Button, buttonVariants }\n",
      "type": "registry:ui",
      "target": ""
    }
  ]
}
