{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "dependencies": [
    "@phosphor-icons/react"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/button.json",
    "https://ui.hotfix.jobs/r/recipes.json",
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/calendar.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { selectionFocus } from \"@/lib/recipes\";\n\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react/dist/ssr\";\ntype Mode = \"single\" | \"range\" | \"multiple\";\n\nexport interface DateRange {\n  from?: Date;\n  to?: Date;\n}\n\ninterface BaseProps {\n  /** First day of the week (0 = Sunday). Default 0. */\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  /** Disable interaction. */\n  disabled?: boolean;\n  /** Disable specific dates. */\n  disabledDates?: (date: Date) => boolean;\n  /** Earliest selectable date. */\n  fromDate?: Date;\n  /** Latest selectable date. */\n  toDate?: Date;\n  /** Initial month to display (defaults to today or the selected value). */\n  defaultMonth?: Date;\n  className?: string;\n}\n\ntype SingleCalendarProps = BaseProps & {\n  mode?: \"single\";\n  value?: Date | null;\n  defaultValue?: Date | null;\n  onValueChange?: (value: Date | null) => void;\n};\n\ntype RangeCalendarProps = BaseProps & {\n  mode: \"range\";\n  value?: DateRange;\n  defaultValue?: DateRange;\n  onValueChange?: (value: DateRange) => void;\n};\n\ntype MultipleCalendarProps = BaseProps & {\n  mode: \"multiple\";\n  value?: Date[];\n  defaultValue?: Date[];\n  onValueChange?: (value: Date[]) => void;\n};\n\nexport type CalendarProps =\n  | SingleCalendarProps\n  | RangeCalendarProps\n  | MultipleCalendarProps;\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst MONTH_NAMES = [\n  \"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\n  \"July\", \"August\", \"September\", \"October\", \"November\", \"December\",\n];\nconst DAY_NAMES_SHORT = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\nfunction startOfDay(d: Date): Date {\n  const x = new Date(d);\n  x.setHours(0, 0, 0, 0);\n  return x;\n}\nfunction isSameDay(a: Date, b: Date): boolean {\n  return (\n    a.getFullYear() === b.getFullYear() &&\n    a.getMonth() === b.getMonth() &&\n    a.getDate() === b.getDate()\n  );\n}\nfunction isSameMonth(a: Date, b: Date): boolean {\n  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();\n}\nfunction addMonths(d: Date, n: number): Date {\n  const x = new Date(d);\n  x.setDate(1);\n  x.setMonth(x.getMonth() + n);\n  return x;\n}\nfunction isBefore(a: Date, b: Date): boolean {\n  return startOfDay(a).getTime() < startOfDay(b).getTime();\n}\nfunction isAfter(a: Date, b: Date): boolean {\n  return startOfDay(a).getTime() > startOfDay(b).getTime();\n}\nfunction inRange(d: Date, from: Date, to: Date): boolean {\n  const t = startOfDay(d).getTime();\n  return t >= startOfDay(from).getTime() && t <= startOfDay(to).getTime();\n}\n\nfunction buildMonthGrid(month: Date, weekStartsOn: number): Date[][] {\n  const first = new Date(month.getFullYear(), month.getMonth(), 1);\n  const startOffset = (first.getDay() - weekStartsOn + 7) % 7;\n  const start = new Date(first);\n  start.setDate(first.getDate() - startOffset);\n\n  const weeks: Date[][] = [];\n  const cursor = new Date(start);\n  for (let w = 0; w < 6; w++) {\n    const row: Date[] = [];\n    for (let d = 0; d < 7; d++) {\n      row.push(new Date(cursor));\n      cursor.setTime(cursor.getTime() + DAY_MS);\n    }\n    weeks.push(row);\n  }\n  return weeks;\n}\n\n/** Month grid with single, range, or multiple date selection. For an input-triggered popup, use `DatePicker`. */\nexport function Calendar(props: CalendarProps): React.ReactElement {\n  const {\n    mode = \"single\",\n    weekStartsOn = 0,\n    disabled,\n    disabledDates,\n    fromDate,\n    toDate,\n    defaultMonth,\n    className,\n  } = props as BaseProps & { mode: Mode };\n\n  const initialMonth = useMemo(() => {\n    if (defaultMonth) return startOfDay(defaultMonth);\n    if (mode === \"single\") {\n      const v =\n        (props as SingleCalendarProps).value ??\n        (props as SingleCalendarProps).defaultValue;\n      if (v) return startOfDay(v);\n    }\n    if (mode === \"range\") {\n      const v =\n        (props as RangeCalendarProps).value ??\n        (props as RangeCalendarProps).defaultValue;\n      if (v?.from) return startOfDay(v.from);\n    }\n    if (mode === \"multiple\") {\n      const v =\n        (props as MultipleCalendarProps).value ??\n        (props as MultipleCalendarProps).defaultValue;\n      if (v && v[0]) return startOfDay(v[0]);\n    }\n    return startOfDay(new Date());\n  }, [defaultMonth, mode, props]);\n\n  const [viewMonth, setViewMonth] = useState<Date>(initialMonth);\n  const today = startOfDay(new Date());\n\n  const [singleUncontrolled, setSingleUncontrolled] = useState<Date | null>(\n    mode === \"single\"\n      ? ((props as SingleCalendarProps).defaultValue ?? null)\n      : null,\n  );\n  const [rangeUncontrolled, setRangeUncontrolled] = useState<DateRange>(\n    mode === \"range\"\n      ? ((props as RangeCalendarProps).defaultValue ?? {})\n      : {},\n  );\n  const [multipleUncontrolled, setMultipleUncontrolled] = useState<Date[]>(\n    mode === \"multiple\"\n      ? ((props as MultipleCalendarProps).defaultValue ?? [])\n      : [],\n  );\n\n  const singleValue =\n    mode === \"single\"\n      ? ((props as SingleCalendarProps).value ?? singleUncontrolled)\n      : null;\n  const rangeValue = useMemo(\n    () =>\n      mode === \"range\"\n        ? ((props as RangeCalendarProps).value ?? rangeUncontrolled)\n        : ({} as DateRange),\n    [mode, props, rangeUncontrolled],\n  );\n  const multipleValue = useMemo(\n    () =>\n      mode === \"multiple\"\n        ? ((props as MultipleCalendarProps).value ?? multipleUncontrolled)\n        : [],\n    [mode, props, multipleUncontrolled],\n  );\n\n  const isDateDisabled = useCallback(\n    (date: Date): boolean => {\n      if (disabled) return true;\n      if (fromDate && isBefore(date, fromDate)) return true;\n      if (toDate && isAfter(date, toDate)) return true;\n      if (disabledDates?.(date)) return true;\n      return false;\n    },\n    [disabled, fromDate, toDate, disabledDates],\n  );\n\n  const onDayClick = useCallback(\n    (date: Date) => {\n      if (isDateDisabled(date)) return;\n      const d = startOfDay(date);\n\n      if (mode === \"single\") {\n        const next = singleValue && isSameDay(singleValue, d) ? null : d;\n        if ((props as SingleCalendarProps).value === undefined) {\n          setSingleUncontrolled(next);\n        }\n        (props as SingleCalendarProps).onValueChange?.(next);\n        return;\n      }\n\n      if (mode === \"range\") {\n        const { from, to } = rangeValue;\n        let next: DateRange;\n        if (!from || (from && to)) {\n          next = { from: d, to: undefined };\n        } else if (from && !to) {\n          if (isBefore(d, from)) {\n            next = { from: d, to: from };\n          } else if (isSameDay(d, from)) {\n            next = {};\n          } else {\n            next = { from, to: d };\n          }\n        } else {\n          next = { from: d };\n        }\n        if ((props as RangeCalendarProps).value === undefined) {\n          setRangeUncontrolled(next);\n        }\n        (props as RangeCalendarProps).onValueChange?.(next);\n        return;\n      }\n\n      // multiple\n      const exists = multipleValue.some((m) => isSameDay(m, d));\n      const next = exists\n        ? multipleValue.filter((m) => !isSameDay(m, d))\n        : [...multipleValue, d];\n      if ((props as MultipleCalendarProps).value === undefined) {\n        setMultipleUncontrolled(next);\n      }\n      (props as MultipleCalendarProps).onValueChange?.(next);\n    },\n    [\n      isDateDisabled,\n      mode,\n      singleValue,\n      rangeValue,\n      multipleValue,\n      props,\n    ],\n  );\n\n  const weeks = useMemo(\n    () => buildMonthGrid(viewMonth, weekStartsOn),\n    [viewMonth, weekStartsOn],\n  );\n  const dayNames = useMemo(() => {\n    const start = weekStartsOn;\n    return [...Array(7)].map((_, i) => DAY_NAMES_SHORT[(start + i) % 7]);\n  }, [weekStartsOn]);\n\n  const monthLabel = `${MONTH_NAMES[viewMonth.getMonth()]} ${viewMonth.getFullYear()}`;\n\n  const isSelected = (d: Date): boolean => {\n    if (mode === \"single\") return !!singleValue && isSameDay(d, singleValue);\n    if (mode === \"range\") {\n      const { from, to } = rangeValue;\n      if (from && isSameDay(d, from)) return true;\n      if (to && isSameDay(d, to)) return true;\n      return false;\n    }\n    return multipleValue.some((m) => isSameDay(d, m));\n  };\n\n  const isInRange = (d: Date): boolean => {\n    if (mode !== \"range\") return false;\n    const { from, to } = rangeValue;\n    if (!from || !to) return false;\n    return inRange(d, from, to);\n  };\n\n  return (\n    <div\n      data-slot=\"calendar\"\n      data-mode={mode}\n      className={cn(\n        \"w-fit select-none rounded-[var(--radius-12)] border border-hairline-soft bg-layer-1 p-3 text-ink\",\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"mb-2 flex items-center justify-between gap-2\">\n        <Button\n          variant=\"tertiary\"\n          size=\"sm\"\n          icon={<CaretLeft className=\"size-4\" />}\n          onClick={() => setViewMonth((m) => addMonths(m, -1))}\n          aria-label=\"Previous month\"\n        />\n        <div className=\"text-small font-medium tabular-nums text-ink\">\n          {monthLabel}\n        </div>\n        <Button\n          variant=\"tertiary\"\n          size=\"sm\"\n          icon={<CaretRight className=\"size-4\" />}\n          onClick={() => setViewMonth((m) => addMonths(m, 1))}\n          aria-label=\"Next month\"\n        />\n      </div>\n\n      {/* Day headers */}\n      <div className=\"grid grid-cols-7 gap-0.5 px-1 pb-1\">\n        {dayNames.map((d, i) => (\n          <div\n            key={i}\n            className=\"text-center text-micro text-ink-muted\"\n          >\n            {d}\n          </div>\n        ))}\n      </div>\n\n      {/* Days */}\n      <div className=\"grid grid-cols-7 gap-0.5\">\n        {weeks.flat().map((d, i) => {\n          const outside = !isSameMonth(d, viewMonth);\n          const disabledDay = isDateDisabled(d);\n          const selected = isSelected(d);\n          const inSelectedRange = isInRange(d);\n          const isToday = isSameDay(d, today);\n\n          return (\n            <button\n              key={i}\n              type=\"button\"\n              disabled={disabledDay}\n              onClick={() => onDayClick(d)}\n              aria-pressed={selected}\n              aria-label={d.toDateString()}\n              data-outside={outside || undefined}\n              data-today={isToday || undefined}\n              data-selected={selected || undefined}\n              data-in-range={inSelectedRange || undefined}\n              className={cn(\n                \"relative inline-flex size-9 items-center justify-center tabular-nums text-small transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n                \"rounded-[var(--radius-6)]\",\n                outside && \"text-ink-subtle\",\n                !outside && !selected && \"text-ink\",\n                !selected && !disabledDay && \"hover:bg-layer-hover\",\n                isToday &&\n                  !selected &&\n                  \"text-small font-medium ring-1 ring-inset ring-hairline-strong\",\n                selected &&\n                  \"bg-primary text-on-primary text-small font-medium hover:bg-primary-hover\",\n                inSelectedRange && !selected && \"bg-layer-hover\",\n                disabledDay && \"pointer-events-none opacity-30\",\n                selectionFocus,\n              )}\n            >\n              {d.getDate()}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/calendar.tsx"
    }
  ],
  "type": "registry:ui"
}