{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "availability",
  "title": "Availability",
  "description": "A draggable weekly availability scheduler for creating, resizing, moving, disabling, and deleting time spans.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/utilities",
    "lucide-react",
    "nanoid"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/corr/components/availability.tsx",
      "content": "import * as React from \"react\"\nimport {\n  DndContext,\n  PointerSensor,\n  useDraggable,\n  useDroppable,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n  type DragMoveEvent,\n  type DragOverEvent,\n  type DragStartEvent,\n} from \"@dnd-kit/core\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { Clock, Settings, X } from \"lucide-react\"\nimport { nanoid } from \"nanoid\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface TimeSpan {\n  active?: boolean\n  id: string\n  week_day: number\n  start_time: string\n  end_time: string\n  label?: string\n}\n\nexport interface AvailabilityProps {\n  value?: TimeSpan[]\n  onValueChange?: (value: TimeSpan[]) => void\n  disabled?: TimeSpan[]\n  days?: number[]\n  showAllDays?: boolean\n  timeIncrements?: number\n  startTime?: number\n  endTime?: number\n  useAmPm?: boolean\n  mergeAdjacent?: boolean\n  slotClassName?: string\n  slotVariant?: \"default\" | \"success\" | \"destructive\"\n  defaultLabel?: string\n  className?: string\n}\n\nconst DAYS = [\n  \"Sunday\",\n  \"Monday\",\n  \"Tuesday\",\n  \"Wednesday\",\n  \"Thursday\",\n  \"Friday\",\n  \"Saturday\",\n]\n\nfunction timeToMinutes(time: string) {\n  const [hours, minutes] = time.split(\":\").map(Number)\n  return hours * 60 + minutes\n}\n\nfunction minutesToTime(minutes: number) {\n  const hours = Math.floor(minutes / 60)\n  const remainingMinutes = minutes % 60\n  return `${hours.toString().padStart(2, \"0\")}:${remainingMinutes\n    .toString()\n    .padStart(2, \"0\")}`\n}\n\nfunction formatDisplayTime(time: string, useAmPm: boolean) {\n  if (!useAmPm) return time\n\n  const [hours, minutes] = time.split(\":\").map(Number)\n  const period = hours >= 12 ? \"PM\" : \"AM\"\n  const displayHours = hours % 12 || 12\n\n  return `${displayHours}:${minutes.toString().padStart(2, \"0\")} ${period}`\n}\n\nfunction mergeAdjacentSpans(spans: TimeSpan[]): TimeSpan[] {\n  if (spans.length === 0) return spans\n\n  const byDay = new Map<number, TimeSpan[]>()\n\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  byDay.forEach((daySpans) => {\n    const sorted = [...daySpans].sort(\n      (left, right) =>\n        timeToMinutes(left.start_time) - timeToMinutes(right.start_time)\n    )\n\n    let current = sorted[0]\n\n    for (let index = 1; index < sorted.length; index += 1) {\n      const next = sorted[index]\n\n      if (current.end_time === next.start_time) {\n        current = {\n          ...current,\n          end_time: next.end_time,\n        }\n      } else {\n        merged.push(current)\n        current = next\n      }\n    }\n\n    merged.push(current)\n  })\n\n  return merged\n}\n\nfunction useCalendarCreation({\n  containerRef,\n  timeIncrements,\n  startTime,\n  endTime,\n  events,\n  disabledEvents = [],\n  onCreate,\n  dayIndex,\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  dayIndex: 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  const sortedConstraints = React.useMemo(() => {\n    return [...events, ...disabledEvents].sort(\n      (left, right) =>\n        timeToMinutes(left.start_time) - timeToMinutes(right.start_time)\n    )\n  }, [disabledEvents, events])\n\n  function getMinutesFromY(y: number) {\n    if (!containerRef.current) return 0\n\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\n    return Math.round(minutes / timeIncrements) * timeIncrements\n  }\n\n  function handlePointerDown(event: React.PointerEvent) {\n    if (isDayDisabled) return\n    if (event.target !== event.currentTarget) return\n\n    event.preventDefault()\n    containerRef.current?.setPointerCapture(event.pointerId)\n\n    const startMinutes = getMinutesFromY(event.clientY)\n    const isOverlapping = sortedConstraints.some((constraint) => {\n      const constraintStart = timeToMinutes(constraint.start_time)\n      const constraintEnd = timeToMinutes(constraint.end_time)\n      return startMinutes >= constraintStart && startMinutes < constraintEnd\n    })\n\n    if (isOverlapping) return\n\n    const previousEvent = sortedConstraints\n      .filter(\n        (constraint) => timeToMinutes(constraint.end_time) <= startMinutes\n      )\n      .pop()\n    const nextEvent = sortedConstraints.find(\n      (constraint) => timeToMinutes(constraint.start_time) >= startMinutes\n    )\n\n    const minStartMinutes = previousEvent\n      ? timeToMinutes(previousEvent.end_time)\n      : startOffset\n    const maxEndMinutes = nextEvent\n      ? timeToMinutes(nextEvent.start_time)\n      : endTime * 60\n\n    setCreationStart(startMinutes)\n    setCurrentMouseY(startMinutes)\n    setIsCreating(true)\n\n    function handlePointerMove(pointerEvent: PointerEvent) {\n      const currentMinutes = getMinutesFromY(pointerEvent.clientY)\n      const clampedMinutes = Math.max(\n        minStartMinutes,\n        Math.min(currentMinutes, maxEndMinutes)\n      )\n\n      setCurrentMouseY(clampedMinutes)\n    }\n\n    function handlePointerUp(pointerEvent: PointerEvent) {\n      const currentMinutes = getMinutesFromY(pointerEvent.clientY)\n\n      let finalStart = Math.min(startMinutes, currentMinutes)\n      let finalEnd = Math.max(startMinutes, currentMinutes)\n\n      finalStart = Math.max(minStartMinutes, finalStart)\n      finalEnd = Math.min(maxEndMinutes, finalEnd)\n\n      if (finalEnd - finalStart < timeIncrements) {\n        finalEnd = Math.min(finalStart + timeIncrements, maxEndMinutes)\n      }\n\n      if (finalEnd - finalStart <= timeIncrements) {\n        finalEnd = Math.min(finalStart + 60, maxEndMinutes)\n      }\n\n      if (finalEnd > finalStart) {\n        onCreate(dayIndex, finalStart, finalEnd)\n      }\n\n      setIsCreating(false)\n      setCreationStart(null)\n      setCurrentMouseY(null)\n      containerRef.current?.releasePointerCapture(pointerEvent.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  return {\n    isCreating,\n    creationStart,\n    currentMouseY,\n    totalMinutes,\n    startOffset,\n    sortedConstraints,\n    handlePointerDown,\n  }\n}\n\nconst AvailabilityDragContext = React.createContext<{\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  slotVariant = \"default\",\n  defaultLabel = \"Available slot\",\n  className,\n}: AvailabilityProps) {\n  const [internalValue, setInternalValue] = React.useState<TimeSpan[]>(value)\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  const mainContainerRef = React.useRef<HTMLDivElement>(null)\n\n  const renderedDays = React.useMemo(() => {\n    return showAllDays ? [0, 1, 2, 3, 4, 5, 6] : days\n  }, [days, showAllDays])\n\n  React.useEffect(() => {\n    setInternalValue(value)\n  }, [value])\n\n  function updateValue(newValue: TimeSpan[], shouldMerge = false) {\n    const finalValue =\n      shouldMerge && mergeAdjacent ? mergeAdjacentSpans(newValue) : newValue\n    setInternalValue(finalValue)\n    onValueChange?.(finalValue)\n  }\n\n  function handleResize(\n    id: string,\n    newStart: string,\n    newEnd: string,\n    isComplete = false\n  ) {\n    updateValue(\n      internalValue.map((span) => {\n        if (span.id === id) {\n          return { ...span, start_time: newStart, end_time: newEnd }\n        }\n        return span\n      }),\n      isComplete\n    )\n  }\n\n  function handleCreate(\n    dayIndex: number,\n    startMinutes: number,\n    endMinutes: number\n  ) {\n    const newSpan: TimeSpan = {\n      id: nanoid(),\n      week_day: dayIndex,\n      start_time: minutesToTime(startMinutes),\n      end_time: minutesToTime(endMinutes),\n      active: true,\n      label: defaultLabel,\n    }\n\n    updateValue([...internalValue, newSpan], true)\n  }\n\n  function handleDelete(id: string) {\n    updateValue(\n      internalValue.filter((span) => span.id !== id),\n      true\n    )\n  }\n\n  function handleMove(\n    id: string,\n    newStart: string,\n    newEnd: string,\n    newDayIndex: number\n  ) {\n    updateValue(\n      internalValue.map((span) => {\n        if (span.id === id) {\n          return {\n            ...span,\n            start_time: newStart,\n            end_time: newEnd,\n            week_day: newDayIndex,\n          }\n        }\n        return span\n      }),\n      true\n    )\n  }\n\n  function 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 =\n      Math.round(deltaMinutesRaw / timeIncrements) * timeIncrements\n    const originalStart = timeToMinutes(span.start_time)\n    const duration = timeToMinutes(span.end_time) - originalStart\n    const newStart = originalStart + deltaMinutes\n    const newEnd = newStart + duration\n    const dayStartMinutes = startTime * 60\n    const dayEndMinutes = endTime * 60\n\n    if (newStart < dayStartMinutes || newEnd > dayEndMinutes) {\n      return { isValid: false, newStart, duration }\n    }\n\n    if (!days.includes(targetDayIndex)) {\n      return { isValid: false, newStart, duration }\n    }\n\n    const dayEvents = internalValue.filter(\n      (event) => event.week_day === targetDayIndex && event.id !== span.id\n    )\n    const hasEventOverlap = dayEvents.some((event) => {\n      const eventStart = timeToMinutes(event.start_time)\n      const eventEnd = timeToMinutes(event.end_time)\n      return newStart < eventEnd && newEnd > eventStart\n    })\n\n    if (hasEventOverlap) {\n      return { isValid: false, newStart, duration }\n    }\n\n    const dayDisabled = disabled.filter(\n      (event) => event.week_day === targetDayIndex\n    )\n    const hasDisabledOverlap = dayDisabled.some((event) => {\n      const eventStart = timeToMinutes(event.start_time)\n      const eventEnd = timeToMinutes(event.end_time)\n      return newStart < eventEnd && newEnd > eventStart\n    })\n\n    if (hasDisabledOverlap) {\n      return { isValid: false, newStart, duration }\n    }\n\n    return { isValid: true, newStart, duration }\n  }\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: {\n        distance: 8,\n      },\n    })\n  )\n\n  function checkValidity(\n    activeSpanId: string,\n    overId: string | number | undefined,\n    currentDeltaY: number\n  ) {\n    if (!mainContainerRef.current || !overId) {\n      setIsDropValid(false)\n      return\n    }\n\n    const span = internalValue.find((item) => item.id === activeSpanId)\n    if (!span) return\n\n    const targetDayIndex = Number.parseInt(\n      overId.toString().replace(\"day-\", \"\"),\n      10\n    )\n\n    if (Number.isNaN(targetDayIndex)) {\n      setIsDropValid(false)\n      return\n    }\n\n    const result = validatePlacement(\n      span,\n      targetDayIndex,\n      currentDeltaY,\n      mainContainerRef.current.clientHeight\n    )\n\n    setIsDropValid(result.isValid)\n  }\n\n  function handleDragStart(event: DragStartEvent) {\n    setActiveId(event.active.id as string)\n    setDeltaY(0)\n    setOverDayIndex(null)\n    setIsDropValid(true)\n  }\n\n  function handleDragMove(event: DragMoveEvent) {\n    setDeltaY(event.delta.y)\n    checkValidity(event.active.id as string, event.over?.id, event.delta.y)\n  }\n\n  function handleDragOver(event: DragOverEvent) {\n    if (event.over) {\n      const nextDayIndex = Number.parseInt(\n        event.over.id.toString().replace(\"day-\", \"\"),\n        10\n      )\n\n      setOverDayIndex(Number.isNaN(nextDayIndex) ? null : nextDayIndex)\n    } else {\n      setOverDayIndex(null)\n    }\n\n    checkValidity(event.active.id as string, event.over?.id, event.delta.y)\n  }\n\n  function handleDragCancel() {\n    setActiveId(null)\n    setOverDayIndex(null)\n    setDeltaY(0)\n    setIsDropValid(true)\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, delta, over } = event\n\n    setActiveId(null)\n    setOverDayIndex(null)\n    setDeltaY(0)\n    setIsDropValid(true)\n\n    const span = internalValue.find((item) => item.id === active.id)\n    if (!span || !mainContainerRef.current || !over) return\n\n    const targetDayIndex = Number.parseInt(\n      over.id.toString().replace(\"day-\", \"\"),\n      10\n    )\n\n    if (Number.isNaN(targetDayIndex)) return\n\n    const { isValid, newStart, duration } = validatePlacement(\n      span,\n      targetDayIndex,\n      delta.y,\n      mainContainerRef.current.clientHeight\n    )\n\n    if (!isValid) return\n\n    handleMove(\n      span.id,\n      minutesToTime(newStart),\n      minutesToTime(newStart + duration),\n      targetDayIndex\n    )\n  }\n\n  const activeSpan = React.useMemo(\n    () => internalValue.find((span) => span.id === activeId) ?? null,\n    [activeId, internalValue]\n  )\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          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 touch-none flex-col overflow-hidden rounded-md border bg-background select-none\",\n            className\n          )}\n        >\n          <div className=\"flex w-full border-b bg-muted/40\">\n            <div className=\"w-16 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\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          <div\n            className=\"relative flex flex-1 overflow-y-auto\"\n            ref={mainContainerRef}\n          >\n            <div className=\"flex w-16 shrink-0 flex-col border-r bg-muted/10\">\n              {Array.from({ length: endTime - startTime }).map((_, index) => {\n                const hour = startTime + index\n\n                return (\n                  <div\n                    key={hour}\n                    className=\"relative flex flex-1 items-center justify-start border-b border-dashed border-muted-foreground/20 pl-3\"\n                  >\n                    <span className=\"text-xs text-muted-foreground\">\n                      {formatDisplayTime(`${hour}:00`, useAmPm)}\n                    </span>\n                  </div>\n                )\n              })}\n            </div>\n\n            <div className=\"relative flex flex-1\">\n              <div className=\"pointer-events-none absolute inset-0 flex flex-col\">\n                {Array.from({ length: endTime - startTime }).map((_, index) => (\n                  <div\n                    key={index}\n                    className=\"relative w-full flex-1 border-b border-dashed border-foreground/10 dark:border-muted/60\"\n                  />\n                ))}\n              </div>\n\n              {renderedDays.map((dayIndex) => {\n                const isActive = days.includes(dayIndex)\n\n                return (\n                  <DayColumn\n                    key={dayIndex}\n                    dayIndex={dayIndex}\n                    startTime={startTime}\n                    endTime={endTime}\n                    timeIncrements={timeIncrements}\n                    events={internalValue.filter(\n                      (event) => event.week_day === dayIndex\n                    )}\n                    disabledEvents={disabled.filter(\n                      (event) => event.week_day === dayIndex\n                    )}\n                    onCreate={handleCreate}\n                    onResize={handleResize}\n                    onDelete={handleDelete}\n                    useAmPm={useAmPm}\n                    isDayDisabled={!isActive}\n                    slotClassName={slotClassName}\n                    slotVariant={slotVariant}\n                  />\n                )\n              })}\n            </div>\n          </div>\n        </div>\n      </AvailabilityDragContext.Provider>\n    </DndContext>\n  )\n}\n\ninterface DayColumnProps {\n  dayIndex: 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: (\n    id: string,\n    start: string,\n    end: string,\n    isComplete?: boolean\n  ) => void\n  onDelete: (id: string) => void\n  useAmPm: boolean\n  isDayDisabled?: boolean\n  slotClassName?: string\n  slotVariant?: \"default\" | \"success\" | \"destructive\"\n}\n\nfunction DayColumn({\n  dayIndex,\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  slotVariant = \"default\",\n}: DayColumnProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const [containerHeight, setContainerHeight] = React.useState(0)\n  const context = React.useContext(AvailabilityDragContext)\n  const { setNodeRef } = useDroppable({\n    id: `day-${dayIndex}`,\n    disabled: isDayDisabled,\n  })\n\n  function mergedRef(node: HTMLDivElement | null) {\n    containerRef.current = node\n    setContainerHeight(node?.clientHeight ?? 0)\n    setNodeRef(node)\n  }\n\n  const {\n    isCreating,\n    creationStart,\n    currentMouseY,\n    totalMinutes,\n    startOffset,\n    sortedConstraints,\n    handlePointerDown,\n  } = useCalendarCreation({\n    containerRef,\n    timeIncrements,\n    startTime,\n    endTime,\n    events,\n    disabledEvents,\n    onCreate,\n    dayIndex,\n    isDayDisabled,\n  })\n\n  const showGhost =\n    context?.activeId &&\n    context.overDayIndex === dayIndex &&\n    containerHeight > 0 &&\n    !isDayDisabled\n\n  const ghostStyle = React.useMemo(() => {\n    if (!showGhost || !context?.activeSpan || containerHeight <= 0) return null\n\n    const span = context.activeSpan\n    const pixelsPerMinute = containerHeight / totalMinutes\n    const deltaMinutesRaw = context.deltaY / pixelsPerMinute\n    const deltaMinutes =\n      Math.round(deltaMinutesRaw / timeIncrements) * timeIncrements\n    const originalStart = timeToMinutes(span.start_time)\n    const duration = timeToMinutes(span.end_time) - originalStart\n    const newStart = originalStart + deltaMinutes\n\n    return {\n      top: `${((newStart - startOffset) / totalMinutes) * 100}%`,\n      height: `${(duration / totalMinutes) * 100}%`,\n    }\n  }, [\n    context?.activeSpan,\n    context?.deltaY,\n    containerHeight,\n    showGhost,\n    startOffset,\n    timeIncrements,\n    totalMinutes,\n  ])\n\n  return (\n    <div\n      ref={mergedRef}\n      className={cn(\n        \"relative min-w-[100px] flex-1 touch-none border-r last:border-r-0\",\n        isDayDisabled && \"bg-muted/30\",\n        context?.activeId && \"z-10\"\n      )}\n      onPointerDown={handlePointerDown}\n    >\n      {isDayDisabled ? (\n        <div\n          className=\"pointer-events-none absolute inset-0 z-20 bg-muted/10\"\n          style={{\n            backgroundImage:\n              \"repeating-linear-gradient(45deg, transparent, transparent 5px, rgb(128 128 128 / 15%) 5px, rgb(128 128 128 / 15%) 10px)\",\n          }}\n        />\n      ) : null}\n\n      {disabledEvents.map((disabledEvent) => {\n        const startMinutes = timeToMinutes(disabledEvent.start_time)\n        const endMinutes = timeToMinutes(disabledEvent.end_time)\n        const duration = endMinutes - startMinutes\n\n        return (\n          <div\n            key={disabledEvent.id}\n            className=\"pointer-events-none absolute right-0 left-0 z-0 bg-muted/40\"\n            style={{\n              top: `${((startMinutes - startOffset) / totalMinutes) * 100}%`,\n              height: `${(duration / totalMinutes) * 100}%`,\n              backgroundImage:\n                \"repeating-linear-gradient(45deg, transparent, transparent 5px, rgb(128 128 128 / 15%) 5px, rgb(128 128 128 / 15%) 10px)\",\n            }}\n          />\n        )\n      })}\n\n      {ghostStyle ? (\n        <div\n          className={cn(\n            \"pointer-events-none absolute right-1 left-1 z-0 rounded-md border transition-all duration-100 ease-out\",\n            context?.isDropValid\n              ? \"border-foreground/30 bg-foreground/20\"\n              : \"border-destructive/50 bg-destructive/20\"\n          )}\n          style={ghostStyle}\n        />\n      ) : null}\n\n      {events.map((event) => {\n        const otherConstraints = sortedConstraints.filter(\n          (constraint) => constraint.id !== event.id\n        )\n        const eventStart = timeToMinutes(event.start_time)\n        const eventEnd = timeToMinutes(event.end_time)\n        const previousItem = otherConstraints\n          .filter(\n            (constraint) => timeToMinutes(constraint.end_time) <= eventStart\n          )\n          .pop()\n        const nextItem = otherConstraints.find(\n          (constraint) => timeToMinutes(constraint.start_time) >= eventEnd\n        )\n        const minStart = previousItem\n          ? timeToMinutes(previousItem.end_time)\n          : startOffset\n        const maxEnd = nextItem\n          ? timeToMinutes(nextItem.start_time)\n          : endTime * 60\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}\n            slotClassName={slotClassName}\n            slotVariant={slotVariant}\n          />\n        )\n      })}\n\n      {isCreating && creationStart !== null && currentMouseY !== null ? (\n        <div\n          className=\"pointer-events-none absolute right-0 left-0 z-20 mx-1 rounded border border-primary bg-primary/30\"\n          style={{\n            top: `${\n              ((Math.min(creationStart, currentMouseY) - startOffset) /\n                totalMinutes) *\n              100\n            }%`,\n            height: `${\n              (Math.abs(currentMouseY - creationStart) / totalMinutes) * 100\n            }%`,\n          }}\n        />\n      ) : null}\n    </div>\n  )\n}\n\ninterface DraggableTimeSpanProps {\n  span: TimeSpan\n  startTime: number\n  endTime: number\n  minStart: number\n  maxEnd: number\n  onResize: (\n    id: string,\n    start: string,\n    end: string,\n    isComplete?: boolean\n  ) => 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  slotVariant?: \"default\" | \"success\" | \"destructive\"\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  slotVariant = \"default\",\n}: DraggableTimeSpanProps) {\n  const { attributes, listeners, setNodeRef, transform } = useDraggable({\n    id: span.id,\n    data: span,\n    disabled: isLocked,\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  const style: React.CSSProperties = {\n    top: `${((startMinutes - startOffset) / totalMinutes) * 100}%`,\n    height: `${(durationMinutes / totalMinutes) * 100}%`,\n    transform:\n      isDragging && transform ? CSS.Translate.toString(transform) : undefined,\n    zIndex: isDragging ? 40 : 1,\n    opacity: isLocked ? 0.6 : 1,\n    cursor: isDragging ? \"grabbing\" : undefined,\n  }\n\n  function handleResizeStart(\n    event: React.PointerEvent,\n    edge: \"top\" | \"bottom\"\n  ) {\n    if (isLocked) return\n\n    event.stopPropagation()\n    event.preventDefault()\n\n    const target = event.target as HTMLElement\n    target.setPointerCapture(event.pointerId)\n\n    const initialY = event.clientY\n    const initialStart = startMinutes\n    const initialEnd = endMinutes\n\n    function handlePointerMove(pointerEvent: PointerEvent) {\n      if (!containerRef.current) return\n\n      const containerHeight = containerRef.current.clientHeight\n      const pixelsPerMinute = containerHeight / totalMinutes\n      const deltaY = pointerEvent.clientY - initialY\n      const deltaMinutes =\n        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        if (newStart < minStart) newStart = minStart\n        if (newStart >= newEnd - timeIncrements) {\n          newStart = newEnd - timeIncrements\n        }\n      } else {\n        newEnd += deltaMinutes\n        if (newEnd > maxEnd) newEnd = maxEnd\n        if (newEnd <= newStart + timeIncrements) {\n          newEnd = newStart + timeIncrements\n        }\n      }\n\n      onResize(span.id, minutesToTime(newStart), minutesToTime(newEnd), false)\n    }\n\n    function handlePointerUp(pointerEvent: PointerEvent) {\n      target.releasePointerCapture(pointerEvent.pointerId)\n\n      if (containerRef.current) {\n        const containerHeight = containerRef.current.clientHeight\n        const pixelsPerMinute = containerHeight / totalMinutes\n        const deltaY = pointerEvent.clientY - initialY\n        const deltaMinutes =\n          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) {\n            newStart = newEnd - timeIncrements\n          }\n        } else {\n          newEnd += deltaMinutes\n          if (newEnd > maxEnd) newEnd = maxEnd\n          if (newEnd <= newStart + timeIncrements) {\n            newEnd = newStart + timeIncrements\n          }\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  const slotVariantClassName =\n    slotVariant === \"success\"\n      ? \"border-emerald-400/60 bg-emerald-500/15 text-emerald-800 dark:text-emerald-200\"\n      : slotVariant === \"destructive\"\n        ? \"border-red-300! bg-red-50! text-red-600! dark:border-red-800/80! dark:bg-red-950/30! dark:text-red-300!\"\n        : \"\"\n\n  return (\n    <div\n      ref={setNodeRef}\n      style={style}\n      className={cn(\n        \"group absolute right-1 left-1 touch-none overflow-hidden rounded border p-3 text-xs shadow-sm\",\n        slotClassName,\n        slotVariantClassName,\n        isDragging && \"shadow-lg\",\n        isLocked && \"cursor-default border-dashed bg-muted/50 opacity-60\"\n      )}\n    >\n      {canResize ? (\n        <div\n          className=\"absolute top-0 right-0 left-0 z-10 -mt-2 h-4 cursor-row-resize\"\n          onPointerDown={(event) => handleResizeStart(event, \"top\")}\n        />\n      ) : null}\n      <div className=\"absolute top-0 right-1 left-1 h-1 rounded-t-sm bg-transparent group-hover:bg-foreground/20\" />\n\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      {canResize ? (\n        <div\n          className=\"absolute right-0 bottom-0 left-0 z-10 -mb-2 h-4 cursor-row-resize\"\n          onPointerDown={(event) => handleResizeStart(event, \"bottom\")}\n        />\n      ) : null}\n      <div className=\"absolute right-1 bottom-0 left-1 h-1 rounded-b-sm bg-transparent group-hover:bg-foreground/20\" />\n    </div>\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 =\n    duration ??\n    (timeToMinutes(span.end_time) - timeToMinutes(span.start_time)) / 60\n\n  return (\n    <div className=\"timespan-inner-area pointer-events-none relative flex h-full flex-col justify-between text-inherit\">\n      <div className=\"flex flex-col gap-0.5 text-inherit\">\n        <p className=\"truncate text-[10px] font-medium opacity-75\">\n          {span.label ?? \"Available slot\"}\n        </p>\n        <p className=\"leading-none font-semibold\">\n          {formatDisplayTime(span.start_time, useAmPm)}\n        </p>\n        <div className=\"flex items-center gap-0.5\">\n          <Clock className=\"size-2\" />\n          <p className=\"text-[10px] opacity-80\">\n            {calculatedDuration.toFixed(1).replace(\".0\", \"\")}h\n          </p>\n        </div>\n      </div>\n\n      {onDelete ? (\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"pointer-events-auto absolute top-0 right-0 z-20 -mt-1 -mr-1 size-5 hover:bg-foreground/5 dark:hover:bg-foreground/10\"\n          onPointerDown={(event) => {\n            event.stopPropagation()\n          }}\n          onClick={(event) => {\n            event.stopPropagation()\n            onDelete()\n          }}\n        >\n          <X className=\"size-3\" />\n        </Button>\n      ) : null}\n\n      <div className=\"mt-auto flex flex-col gap-1 text-inherit\">\n        {!onDelete ? <Settings className=\"size-3 opacity-50\" /> : null}\n        <p className=\"leading-none font-semibold text-inherit!\">\n          {formatDisplayTime(span.end_time, useAmPm)}\n        </p>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}