{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "Keyboard-friendly month calendar with selection and date constraints.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/calendar.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport type CalendarRangeValue = { from: Date; to?: Date }\n\nexport interface CalendarProps {\n  value?: Date | CalendarRangeValue\n  defaultValue?: Date | CalendarRangeValue\n  onValueChange?: (date: Date | CalendarRangeValue | undefined) => void\n  mode?: 'single' | 'range'\n  month?: Date\n  defaultMonth?: Date\n  onMonthChange?: (month: Date) => void\n  minDate?: Date\n  maxDate?: Date\n  disabled?: (date: Date) => boolean\n  showOutsideDays?: boolean\n  showMonthYearPicker?: boolean\n  className?: string\n}\n\nconst weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']\nconst monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']\n\nconst startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate())\nconst isSameDay = (left?: Date, right?: Date) => Boolean(left && right && startOfDay(left).getTime() === startOfDay(right).getTime())\nconst isBefore = (date: Date, boundary?: Date) => Boolean(boundary && startOfDay(date) < startOfDay(boundary))\nconst isAfter = (date: Date, boundary?: Date) => Boolean(boundary && startOfDay(date) > startOfDay(boundary))\n\nconst isRangeValue = (value: unknown): value is CalendarRangeValue =>\n  Boolean(value && typeof value === 'object' && 'from' in value)\n\nexport function Calendar({\n  value: controlledValue,\n  defaultValue,\n  onValueChange,\n  mode = 'single',\n  month: controlledMonth,\n  defaultMonth,\n  onMonthChange,\n  minDate,\n  maxDate,\n  disabled,\n  showOutsideDays = true,\n  showMonthYearPicker = true,\n  className,\n}: CalendarProps) {\n  const [internalValue, setInternalValue] = React.useState<Date | CalendarRangeValue | undefined>(defaultValue)\n  const [visibleMonth, setVisibleMonth] = React.useState<Date>(() => {\n    const base = defaultMonth ?? (defaultValue && !isRangeValue(defaultValue) ? defaultValue : new Date())\n    return new Date(base.getFullYear(), base.getMonth(), 1)\n  })\n  const [rangeSelecting, setRangeSelecting] = React.useState(false)\n  const [showPicker, setShowPicker] = React.useState(false)\n\n  const value = controlledValue ?? internalValue\n  const month = controlledMonth ?? visibleMonth\n  const firstDay = new Date(month.getFullYear(), month.getMonth(), 1)\n  const firstGridDay = new Date(month.getFullYear(), month.getMonth(), 1 - firstDay.getDay())\n  const days = Array.from({ length: 42 }, (_, index) => new Date(firstGridDay.getFullYear(), firstGridDay.getMonth(), firstGridDay.getDate() + index))\n\n  const emit = (next: Date | CalendarRangeValue | undefined) => {\n    setInternalValue(next)\n    onValueChange?.(next)\n  }\n\n  const changeMonth = (offset: number) => {\n    const nextMonth = new Date(month.getFullYear(), month.getMonth() + offset, 1)\n    setVisibleMonth(nextMonth)\n    onMonthChange?.(nextMonth)\n  }\n\n  const selectDate = (date: Date) => {\n    if (mode === 'range') {\n      const current = isRangeValue(value) ? value : undefined\n      if (!current || (current.from && current.to) || rangeSelecting) {\n        // Start a new range\n        emit({ from: date })\n        setRangeSelecting(true)\n      } else {\n        if (isBefore(date, current.from)) {\n          emit({ from: date, to: current.from })\n        } else {\n          emit({ from: current.from, to: date })\n        }\n        setRangeSelecting(false)\n      }\n      return\n    }\n\n    const nextValue = isSameDay(date, value as Date) ? undefined : date\n    emit(nextValue)\n  }\n\n  const isInRange = (date: Date): boolean => {\n    if (mode !== 'range') return false\n    const range = isRangeValue(value) ? value : undefined\n    if (!range?.from || !range.to) return false\n    return !isBefore(date, range.from) && !isAfter(date, range.to)\n  }\n\n  const pickMonth = (index: number) => {\n    const next = new Date(month.getFullYear(), index, 1)\n    setVisibleMonth(next)\n    onMonthChange?.(next)\n    setShowPicker(false)\n  }\n\n  const pickYear = (year: number) => {\n    const next = new Date(year, month.getMonth(), 1)\n    setVisibleMonth(next)\n    onMonthChange?.(next)\n  }\n\n  const pickerYear = month.getFullYear()\n\n  return (\n    <div className={cn('w-full max-w-sm rounded-2xl bg-rk-surface p-4 font-sans rk-border rk-shadow-md', className)}>\n      <div className=\"mb-4 flex items-center justify-between gap-3\">\n        {showMonthYearPicker ? (\n          <div className=\"relative flex items-center gap-1\">\n            <button\n              type=\"button\"\n              aria-label=\"Select month and year\"\n              aria-haspopup=\"listbox\"\n              aria-expanded={showPicker}\n              onClick={() => setShowPicker((s) => !s)}\n              className=\"flex items-center gap-1.5 rounded-full bg-rk-canvas px-3 py-1.5 font-display text-lg font-black rk-border-sm rk-shadow-sm hover:bg-rk-primary focus:outline-none focus:ring-4 focus:ring-black focus:ring-offset-2 cursor-pointer\"\n            >\n              <span aria-live=\"polite\">\n                {month.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}\n              </span>\n              <ChevronDown className={cn('h-4 w-4 stroke-[3] transition-transform', showPicker && 'rotate-180')} aria-hidden=\"true\" />\n            </button>\n\n            {showPicker && (\n              <div className=\"absolute left-0 top-full z-20 mt-2 rounded-2xl bg-rk-surface rk-border rk-shadow-lg p-3 font-sans\" role=\"listbox\" aria-label=\"Month and year picker\">\n                <div className=\"mb-2 flex items-center justify-between gap-2\">\n                  <button type=\"button\" aria-label=\"Previous year\" onClick={() => pickYear(pickerYear - 1)} className=\"flex h-7 w-7 items-center justify-center rounded-full rk-border-sm rk-shadow-xs hover:bg-rk-primary focus:outline-none focus:ring-2 focus:ring-black cursor-pointer\">\n                    <ChevronLeft className=\"h-3.5 w-3.5 stroke-[3]\" aria-hidden=\"true\" />\n                  </button>\n                  <span className=\"font-display text-sm font-black\">{pickerYear}</span>\n                  <button type=\"button\" aria-label=\"Next year\" onClick={() => pickYear(pickerYear + 1)} className=\"flex h-7 w-7 items-center justify-center rounded-full rk-border-sm rk-shadow-xs hover:bg-rk-primary focus:outline-none focus:ring-2 focus:ring-black cursor-pointer\">\n                    <ChevronRight className=\"h-3.5 w-3.5 stroke-[3]\" aria-hidden=\"true\" />\n                  </button>\n                </div>\n                <div className=\"grid grid-cols-3 gap-1.5\">\n                  {monthNames.map((name, index) => (\n                    <button\n                      key={name}\n                      type=\"button\"\n                      role=\"option\"\n                      aria-selected={month.getMonth() === index}\n                      onClick={() => pickMonth(index)}\n                      className={cn(\n                        'rounded-xl px-2 py-1.5 text-xs font-extrabold transition-colors focus:outline-none focus:ring-2 focus:ring-black cursor-pointer',\n                        month.getMonth() === index ? 'bg-rk-primary rk-border-sm rk-shadow-xs' : 'hover:bg-rk-canvas'\n                      )}\n                    >\n                      {name.slice(0, 3)}\n                    </button>\n                  ))}\n                </div>\n              </div>\n            )}\n          </div>\n        ) : (\n          <h2 className=\"font-display text-lg font-black\" aria-live=\"polite\">\n            {month.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}\n          </h2>\n        )}\n        <div className=\"flex gap-2\">\n          <button type=\"button\" aria-label=\"Previous month\" onClick={() => changeMonth(-1)} className=\"flex h-9 w-9 items-center justify-center rounded-full bg-rk-canvas rk-border-sm rk-shadow-sm hover:bg-rk-primary focus:outline-none focus:ring-4 focus:ring-black focus:ring-offset-2 cursor-pointer\">\n            <ChevronLeft className=\"h-4 w-4 stroke-[3]\" aria-hidden=\"true\" />\n          </button>\n          <button type=\"button\" aria-label=\"Next month\" onClick={() => changeMonth(1)} className=\"flex h-9 w-9 items-center justify-center rounded-full bg-rk-canvas rk-border-sm rk-shadow-sm hover:bg-rk-primary focus:outline-none focus:ring-4 focus:ring-black focus:ring-offset-2 cursor-pointer\">\n            <ChevronRight className=\"h-4 w-4 stroke-[3]\" aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n      <div role=\"grid\" aria-label={month.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })} className=\"grid grid-cols-7 gap-1\">\n        {weekdays.map((day) => <div key={day} role=\"columnheader\" className=\"py-1 text-center font-mono text-[10px] font-black uppercase text-rk-ink/50\">{day}</div>)}\n        {days.map((date) => {\n          const outside = date.getMonth() !== month.getMonth()\n          const unavailable = isBefore(date, minDate) || isAfter(date, maxDate) || disabled?.(date)\n          const isDisabled = unavailable || (outside && !showOutsideDays)\n          const range = isRangeValue(value) ? value : undefined\n          const isRangeStart = range?.from ? isSameDay(date, range.from) : false\n          const isRangeEnd = range?.to ? isSameDay(date, range.to) : false\n          const inRange = isInRange(date)\n          return (\n            <button\n              key={date.toISOString()}\n              type=\"button\"\n              role=\"gridcell\"\n              aria-label={date.toLocaleDateString(undefined, { dateStyle: 'full' })}\n              aria-selected={mode === 'single' ? isSameDay(date, value as Date) : isRangeStart}\n              aria-current={isSameDay(date, new Date()) ? 'date' : undefined}\n              disabled={isDisabled}\n              onClick={() => selectDate(date)}\n              className={cn(\n                'aspect-square rounded-xl p-1 text-sm font-extrabold transition-colors focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:pointer-events-none disabled:opacity-25',\n                outside ? 'text-rk-ink/30' : 'text-rk-ink hover:bg-rk-mint',\n                inRange && 'bg-rk-mint rounded-none',\n                isRangeStart && 'bg-rk-primary rk-border-sm rk-shadow-sm rounded-l-xl',\n                isRangeEnd && 'bg-rk-primary rk-border-sm rk-shadow-sm rounded-r-xl',\n                mode === 'single' && isSameDay(date, value as Date) && 'bg-rk-primary rk-border-sm rk-shadow-sm text-rk-ink'\n              )}\n            >\n              {date.getDate()}\n            </button>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/calendar.tsx"
    }
  ]
}