{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "description": "The Material date grid, for single dates and ranges.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/button.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/calendar-utils.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/calendar.tsx",
      "content": "import { useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type KeyboardEvent } from \"react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport {\n  dateKey,\n  daysSinceWeekStart,\n  firstDayOfWeek,\n  isDateSelectable,\n  sameDay,\n} from \"./calendar-utils\"\n\ntype CalendarProps = Omit<ComponentProps<\"div\">, \"onSelect\"> & {\n  defaultMonth?: Date\n  disabled?: boolean\n  isDateUnavailable?: (date: Date) => boolean\n  locale?: string\n  max?: Date\n  min?: Date\n  onSelect: (date: Date) => void\n  range?: { start: Date | null; end: Date | null }\n  selected?: Date | null\n}\n\nfunction startOfMonth(date: Date) {\n  return new Date(date.getFullYear(), date.getMonth(), 1, 12)\n}\n\nfunction addDays(date: Date, amount: number) {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount, 12)\n}\n\nfunction addMonths(date: Date, amount: number) {\n  return new Date(date.getFullYear(), date.getMonth() + amount, 1, 12)\n}\n\nfunction addMonthsPreservingDay(date: Date, amount: number) {\n  const target = new Date(date.getFullYear(), date.getMonth() + amount, 1, 12)\n  const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0, 12).getDate()\n  target.setDate(Math.min(date.getDate(), lastDay))\n  return target\n}\n\nfunction Calendar({\n  className,\n  defaultMonth,\n  disabled = false,\n  isDateUnavailable,\n  locale = \"en-US\",\n  max,\n  min,\n  onSelect,\n  range,\n  ref,\n  selected,\n  ...props\n}: CalendarProps) {\n  const [month, setMonth] = useState(() => startOfMonth(defaultMonth ?? selected ?? new Date()))\n  const rootRef = useRef<HTMLDivElement>(null)\n  const pendingFocus = useRef<string | null>(null)\n  const today = useMemo(() => new Date(), [])\n\n  const monthLabel = new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(month)\n  const fullDate = new Intl.DateTimeFormat(locale, {\n    weekday: \"long\",\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n  })\n  const weekday = new Intl.DateTimeFormat(locale, { weekday: \"narrow\" })\n  // Where the grid starts, and therefore what the header row is labelled with:\n  // both derive from the same value, so the columns cannot rotate out from\n  // under their labels.\n  const weekStart = useMemo(() => firstDayOfWeek(locale), [locale])\n  const firstVisible = addDays(month, -daysSinceWeekStart(month, weekStart))\n  const days = Array.from({ length: 42 }, (_, index) => addDays(firstVisible, index))\n  const years = Array.from({ length: 21 }, (_, index) => month.getFullYear() - 10 + index)\n\n  useLayoutEffect(() => {\n    if (!pendingFocus.current) return\n    const key = pendingFocus.current\n    pendingFocus.current = null\n    rootRef.current?.querySelector<HTMLButtonElement>(`[data-date=\"${key}\"]`)?.focus()\n  }, [month])\n\n  const unavailable = (date: Date) =>\n    !isDateSelectable(date, { disabled, isDateUnavailable, max, min })\n  const focusDate = [selected, range?.end, range?.start, today].find(\n    (candidate) => candidate && days.some((date) => sameDay(date, candidate)) && !unavailable(candidate),\n  ) ?? days.find((date) => date.getMonth() === month.getMonth() && !unavailable(date))\n    ?? days.find((date) => !unavailable(date))\n\n  const moveFocus = (\n    date: Date,\n    direction: -1 | 1,\n    event: KeyboardEvent<HTMLButtonElement>,\n  ) => {\n    event.preventDefault()\n    let target = date\n    let attempts = 0\n    while (unavailable(target) && attempts < 3660) {\n      target = addDays(target, direction)\n      attempts += 1\n    }\n    if (unavailable(target)) return\n    pendingFocus.current = dateKey(target)\n    if (target.getMonth() !== month.getMonth() || target.getFullYear() !== month.getFullYear()) {\n      setMonth(startOfMonth(target))\n    } else {\n      requestAnimationFrame(() => {\n        rootRef.current?.querySelector<HTMLButtonElement>(`[data-date=\"${dateKey(target)}\"]`)?.focus()\n      })\n      pendingFocus.current = null\n    }\n  }\n\n  return (\n    <div\n      {...props}\n      ref={(node) => {\n        rootRef.current = node\n        if (typeof ref === \"function\") ref(node)\n        else if (ref) ref.current = node\n      }}\n      data-slot=\"calendar\"\n      // Kit docked picker: 360dp, 16dp corners, Surface Container High, 12dp grid inset.\n      className={cn(\"w-[360px] max-w-full rounded-m3-lg bg-m3-surface-container-high p-3 text-foreground shadow-m3-2\", className)}\n    >\n      <div className=\"flex h-14 items-center gap-2 px-1\">\n        <select\n          aria-label=\"Month\"\n          value={month.getMonth()}\n          disabled={disabled}\n          className=\"flex h-10 items-center gap-2 rounded-full bg-transparent pr-2 pl-3 text-m3-label-lg text-m3-on-surface-variant outline-none transition-colors duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast) hover:not-disabled:bg-m3-on-surface/8 focus-visible:ring-3 focus-visible:ring-m3-secondary disabled:text-m3-on-surface/38\"\n          onChange={(event) => setMonth(new Date(month.getFullYear(), Number(event.target.value), 1, 12))}\n        >\n          {Array.from({ length: 12 }, (_, index) => (\n            <option key={index} value={index}>\n              {new Intl.DateTimeFormat(locale, { month: \"long\" }).format(new Date(2026, index, 1))}\n            </option>\n          ))}\n        </select>\n        <select\n          aria-label=\"Year\"\n          value={month.getFullYear()}\n          disabled={disabled}\n          className=\"flex h-10 items-center gap-2 rounded-full bg-transparent pr-2 pl-3 text-m3-label-lg text-m3-on-surface-variant outline-none transition-colors duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast) hover:not-disabled:bg-m3-on-surface/8 focus-visible:ring-3 focus-visible:ring-m3-secondary disabled:text-m3-on-surface/38\"\n          onChange={(event) => setMonth(new Date(Number(event.target.value), month.getMonth(), 1, 12))}\n        >\n          {years.map((year) => <option key={year}>{year}</option>)}\n        </select>\n        <span className=\"flex-1\" />\n        <Button aria-label=\"Previous month\" size=\"icon-sm\" variant=\"ghost\" disabled={disabled} onClick={() => setMonth(addMonths(month, -1))}>\n          <ChevronLeftIcon aria-hidden=\"true\" />\n        </Button>\n        <Button aria-label=\"Next month\" size=\"icon-sm\" variant=\"ghost\" disabled={disabled} onClick={() => setMonth(addMonths(month, 1))}>\n          <ChevronRightIcon aria-hidden=\"true\" />\n        </Button>\n      </div>\n      {/* Kit grid: 48dp rows with a 40dp visual circle centred in each 48dp cell. */}\n      <div role=\"grid\" aria-label={monthLabel} className=\"grid grid-cols-7\">\n        <div role=\"row\" className=\"contents\">\n          {Array.from({ length: 7 }, (_, index) => (\n            <div key={index} role=\"columnheader\" aria-label={weekday.format(addDays(firstVisible, index))} className=\"flex h-12 items-center justify-center text-m3-label-md text-foreground\">\n              {weekday.format(addDays(firstVisible, index))}\n            </div>\n          ))}\n        </div>\n        {Array.from({ length: 6 }, (_, rowIndex) => (\n          <div key={rowIndex} role=\"row\" className=\"contents\">\n            {days.slice(rowIndex * 7, rowIndex * 7 + 7).map((date) => {\n              const outside = date.getMonth() !== month.getMonth()\n              const isSelected = sameDay(date, selected)\n              const isRangeStart = sameDay(date, range?.start)\n              const isRangeEnd = sameDay(date, range?.end)\n              const isInRange = Boolean(\n                range?.start && range.end && date > range.start && date < range.end,\n              )\n              const isToday = sameDay(date, today)\n              const isUnavailable = unavailable(date)\n              return (\n                <button\n                  key={dateKey(date)}\n                  type=\"button\"\n                  role=\"gridcell\"\n                  data-date={dateKey(date)}\n                  data-outside={outside || undefined}\n                  data-today={isToday || undefined}\n                  aria-label={fullDate.format(date)}\n                  aria-selected={isSelected || isRangeStart || isRangeEnd || isInRange}\n                  data-range-start={isRangeStart || undefined}\n                  data-range-end={isRangeEnd || undefined}\n                  data-in-range={isInRange || undefined}\n                  disabled={isUnavailable}\n                  tabIndex={sameDay(date, focusDate) ? 0 : -1}\n                  className={cn(\n                    \"relative isolate flex size-12 items-center justify-center text-m3-body-lg outline-none\",\n                    // The 40dp visual container.\n                    \"before:absolute before:size-10 before:rounded-full before:-z-10 before:transition-colors before:duration-(--m3-spring-effects-fast-duration) before:ease-(--m3-spring-effects-fast)\",\n                    \"hover:not-disabled:before:bg-m3-on-surface/8 focus-visible:before:ring-3 focus-visible:before:ring-m3-secondary\",\n                    outside && \"text-muted-foreground\",\n                    isToday && !isSelected && !isRangeStart && !isRangeEnd && \"text-m3-primary before:border before:border-m3-primary\",\n                    (isSelected || isRangeStart || isRangeEnd) && \"text-m3-on-primary before:bg-m3-primary hover:not-disabled:before:bg-m3-primary\",\n                    // Range band: painted across the full 48dp column so it is\n                    // continuous; endpoints fill only their inner half.\n                    (isInRange || (isRangeStart && range?.end) || (isRangeEnd && range?.start)) && \"after:absolute after:inset-y-1 after:-z-20 after:bg-m3-secondary-container\",\n                    isInRange && \"text-m3-on-secondary-container after:inset-x-0\",\n                    isRangeStart && range?.end && !isRangeEnd && \"after:left-1/2 after:right-0\",\n                    isRangeEnd && range?.start && !isRangeStart && \"after:left-0 after:right-1/2\",\n                    \"disabled:cursor-not-allowed disabled:text-m3-on-surface/38\",\n                  )}\n                  onClick={() => onSelect(date)}\n                  onKeyDown={(event) => {\n                    if (event.key === \"ArrowRight\") moveFocus(addDays(date, 1), 1, event)\n                    if (event.key === \"ArrowLeft\") moveFocus(addDays(date, -1), -1, event)\n                    if (event.key === \"ArrowDown\") moveFocus(addDays(date, 7), 1, event)\n                    if (event.key === \"ArrowUp\") moveFocus(addDays(date, -7), -1, event)\n                    // Home and End mean the ends of the *displayed* row, so\n                    // they follow the week start too — on a Monday-first\n                    // calendar, Home is Monday.\n                    if (event.key === \"Home\") moveFocus(addDays(date, -daysSinceWeekStart(date, weekStart)), 1, event)\n                    if (event.key === \"End\") moveFocus(addDays(date, 6 - daysSinceWeekStart(date, weekStart)), -1, event)\n                    if (event.key === \"PageDown\") moveFocus(addMonthsPreservingDay(date, 1), 1, event)\n                    if (event.key === \"PageUp\") moveFocus(addMonthsPreservingDay(date, -1), -1, event)\n                  }}\n                >\n                  {date.getDate()}\n                </button>\n              )\n            })}\n          </div>\n        ))}\n      </div>\n      <div className=\"flex justify-end pt-2\">\n        <Button variant=\"ghost\" size=\"sm\" disabled={unavailable(today)} onClick={() => { setMonth(startOfMonth(today)); onSelect(today) }}>\n          Today\n        </Button>\n      </div>\n    </div>\n  )\n}\n\nexport { Calendar, type CalendarProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}