{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "author": "@fma-ui/ui",
  "description": "A date grid for selecting a single date, multiple dates or a range. The header opens month and year grids for fast navigation, and `granularity` switches the selection to whole months or years. `unavailableRanges` blocks booked spans, and an already-selected value that becomes blocked travels to the nearest available date.",
  "dependencies": [
    "@internationalized/date@^3.12.4",
    "class-variance-authority@^0.7.1",
    "cn@^0.4.0",
    "lucide-react@^1.52.0",
    "react-aria-components@^1.21.1"
  ],
  "registryDependencies": [
    "@fma-ui/button",
    "@fma-ui/select",
    "@fma-ui/use-available-date-correction",
    "@fma-ui/date-granularity",
    "@fma-ui/interval-helpers",
    "@fma-ui/types"
  ],
  "files": [
    {
      "path": "src/core/calendar/calendar.tsx",
      "content": "\"use client\";\n\nimport {\n  type CalendarDate,\n  DateFormatter,\n  endOfYear,\n  isSameMonth,\n  startOfYear,\n  toCalendar,\n  toCalendarDate,\n  today,\n} from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { cn } from \"cn\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  Calendar as AriaCalendar,\n  CalendarGridHeader as AriaCalendarGridHeader,\n  RangeCalendar as AriaRangeCalendar,\n  CalendarCell,\n  type CalendarCellRenderProps,\n  CalendarGrid,\n  CalendarGridBody,\n  CalendarHeaderCell,\n  CalendarHeading,\n  CalendarMonthPicker,\n  type CalendarProps,\n  type CalendarState,\n  CalendarStateContext,\n  CalendarYearPicker,\n  type DateValue,\n  type RangeCalendarProps,\n  type RangeCalendarState,\n  RangeCalendarStateContext,\n  useLocale,\n} from \"react-aria-components\";\nimport { Button, buttonVariants } from \"@/core/button/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/core/select/select\";\nimport { useAvailableDateCorrection } from \"@/hooks/use-available-date-correction\";\nimport {\n  type DateGranularity,\n  isFullyUnavailable,\n  isSamePeriod,\n  type PeriodGranularity,\n  periodBounds,\n  periodEnd,\n} from \"@/lib/date-granularity\";\nimport { createIsWithinAnyInterval } from \"@/lib/interval-helpers\";\nimport type { Interval } from \"@/lib/types\";\n\n/**\n * What a selection represents. `\"day\"` is the regular day grid; `\"month\"` and\n * `\"year\"` replace it with a month (3×4) or year (3×4, one decade ± 1 year)\n * grid and commit the *period's first day* (`startOfMonth`/`startOfYear`).\n * In a `RangeCalendar` the committed `end` is the period's *last* day, so the\n * range stays inclusive (e.g. Mar–May → 03-01 … 05-31).\n */\ntype CalendarGranularity = DateGranularity;\ntype PeriodView = PeriodGranularity;\n\n// Views from the most detailed to the least. A calendar only offers the views\n// at or above its granularity: day → [day, month, year], month → [month, year].\nconst VIEW_ORDER: readonly CalendarGranularity[] = [\"day\", \"month\", \"year\"];\nconst PERIOD_COLUMNS = 3;\nconst NEVER_UNAVAILABLE = () => false;\nconst CELL_FOCUS_TARGET = '[role=\"gridcell\"] [tabindex=\"0\"]';\n\ntype AnyCalendarState = CalendarState<\"single\" | \"multiple\"> | RangeCalendarState;\ntype ButtonVariant = React.ComponentProps<typeof Button>[\"variant\"];\ntype RenderCell = (\n  renderProps: CalendarCellRenderProps & { defaultChildren: React.ReactNode },\n) => React.ReactNode;\n\ninterface CalendarExtraProps {\n  buttonVariant?: ButtonVariant;\n  /**\n   * Day view header. `\"grid\"` (default) renders the month and the year as\n   * buttons that open the month/year grids; `\"label\"` is a static heading;\n   * `\"dropdown\"` uses month/year selects.\n   */\n  captionLayout?: \"label\" | \"dropdown\" | \"grid\";\n  numberOfMonths?: number;\n  showWeekNumber?: boolean;\n  headerFormat?: Intl.DateTimeFormatOptions;\n  renderCell?: RenderCell;\n  granularity?: CalendarGranularity;\n  /**\n   * Inclusive from/to spans that can't be picked (e.g. already-booked days),\n   * on top of `minValue`/`maxValue` and `isDateUnavailable`. A committed value\n   * that becomes blocked later travels to the nearest available date (or is\n   * cleared when none is left) — see `useAvailableDateCorrection`.\n   */\n  unavailableRanges?: Interval<DateValue>[];\n  /** Always render 6 week rows, so the height doesn't change between months. */\n  fixedWeeks?: boolean;\n  /**\n   * Travel/clear a selected value that becomes blocked (default `true`). Turn\n   * off when another component owns that correction — e.g. `DatePicker`,\n   * which defers it until its field loses focus so typing isn't interrupted.\n   */\n  correctBlockedValue?: boolean;\n}\n\nconst ROOT_CLASSES =\n  \"group/calendar w-fit bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent\";\n\nconst cellVariants = cva(\n  \"group/day relative mt-2 aspect-square h-full w-full cursor-default rounded-(--cell-radius) p-0 text-center select-none [&:is(:last-child>[data-selected=true])>div]:rounded-r-(--cell-radius)\",\n  {\n    variants: {\n      showWeekNumber: {\n        false: \"[&:is(:first-child>[data-selected=true])>div]:rounded-l-(--cell-radius)\",\n        true: \"[&:is(:nth-child(2)>[data-selected=true])>div]:rounded-l-(--cell-radius)\",\n      },\n      isToday: {\n        true: \"rounded-(--cell-radius) bg-accent text-accent-foreground data-[selected=true]:rounded-none\",\n      },\n      isSelectionStart: {\n        true: \"relative isolate z-0 rounded-l-(--cell-radius) bg-accent after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-accent\",\n      },\n      isSelectionEnd: {\n        true: \"relative isolate z-0 rounded-r-(--cell-radius) bg-accent after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-accent\",\n      },\n      isUnavailable: {\n        true: \"text-muted-foreground opacity-50 [&>div]:line-through\",\n      },\n      isDisabled: {\n        true: \"text-muted-foreground opacity-50\",\n      },\n      isOutsideMonth: {\n        true: \"text-muted-foreground aria-selected:text-muted-foreground\",\n      },\n    },\n  },\n);\n\n// Month/year cells mirror the day cells' look: solid endpoints, accent middle\n// span (square, so a range reads as one band per row), accent \"today\" and hover.\nconst PERIOD_CELL_CLASSES = cn(\n  buttonVariants({ variant: \"ghost\" }),\n  // bg-clip-border: buttonVariants clips the fill inside its transparent\n  // border, which would leave 1px seams along a range band.\n  \"h-full min-h-(--cell-size) w-full rounded-(--cell-radius) bg-clip-border px-1 font-normal\",\n  \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent dark:hover:text-accent-foreground\",\n  \"data-[today=true]:bg-accent data-[today=true]:text-accent-foreground data-[outside=true]:text-muted-foreground\",\n  \"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\",\n  \"data-[unavailable=true]:pointer-events-none data-[unavailable=true]:line-through data-[unavailable=true]:opacity-50\",\n  \"data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground\",\n  \"data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground\",\n  \"data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-start=true]:not-data-[range-end=true]:rounded-e-none\",\n  \"data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-end=true]:not-data-[range-start=true]:rounded-s-none\",\n  // A band wrapping to the next row keeps the outer corners rounded.\n  \"[td:first-child>&]:rounded-s-(--cell-radius)! [td:last-child>&]:rounded-e-(--cell-radius)!\",\n);\n\nfunction navButtonClasses(variant: ButtonVariant) {\n  return cn(\n    buttonVariants({ variant }),\n    \"size-(--cell-size) p-0 select-none disabled:opacity-50 aria-disabled:opacity-50\",\n  );\n}\n\nfunction captionButtonClasses(variant: ButtonVariant) {\n  return cn(buttonVariants({ variant, size: \"sm\" }), \"h-(--cell-size) px-1.5 font-medium\");\n}\n\n// Month/year granularity compares whole periods: `minValue` 2026-03-15 must\n// still allow picking March, whose committed value is 2026-03-01.\nfunction useGranularityBounds(\n  granularity: CalendarGranularity,\n  minValue: DateValue | null | undefined,\n  maxValue: DateValue | null | undefined,\n) {\n  return React.useMemo(\n    () => periodBounds(granularity, minValue, maxValue),\n    [granularity, minValue, maxValue],\n  );\n}\n\n// `isDateUnavailable` OR \"inside one of `unavailableRanges`\". Stays\n// `undefined` when neither is set, so react-aria skips the check entirely.\nfunction useUnavailablePredicate<A extends unknown[]>(\n  isDateUnavailable: ((date: DateValue, ...rest: A) => boolean) | undefined,\n  unavailableRanges: Interval<DateValue>[] | undefined,\n) {\n  return React.useMemo(() => {\n    const isInRanges = createIsWithinAnyInterval(unavailableRanges);\n    if (!isInRanges) return isDateUnavailable;\n    if (!isDateUnavailable) return (date: DateValue) => isInRanges(date);\n    return (date: DateValue, ...rest: A) => isDateUnavailable(date, ...rest) || isInRanges(date);\n  }, [isDateUnavailable, unavailableRanges]);\n}\n\nfunction Calendar<T extends DateValue, M extends \"single\" | \"multiple\" = \"single\">({\n  buttonVariant,\n  captionLayout,\n  numberOfMonths = 1,\n  showWeekNumber,\n  headerFormat,\n  renderCell,\n  granularity = \"day\",\n  unavailableRanges,\n  fixedWeeks = true,\n  correctBlockedValue = true,\n  minValue,\n  maxValue,\n  isDateUnavailable,\n  className,\n  ...props\n}: Omit<CalendarProps<T, M>, \"visibleDuration\"> & CalendarExtraProps) {\n  const bounds = useGranularityBounds(granularity, minValue, maxValue);\n  const isUnavailable = useUnavailablePredicate(isDateUnavailable, unavailableRanges);\n\n  return (\n    <AriaCalendar\n      {...props}\n      data-slot=\"calendar\"\n      minValue={bounds.minValue}\n      maxValue={bounds.maxValue}\n      // Month/year cells evaluate unavailability per period (see PeriodGrid);\n      // handing the day predicate to react-aria there would make it reject\n      // the period's first day instead. An explicit \"never\" (not `undefined`)\n      // also keeps a parent DatePicker's context predicate from leaking in.\n      isDateUnavailable={granularity === \"day\" ? isUnavailable : NEVER_UNAVAILABLE}\n      weeksInMonth={fixedWeeks ? 6 : undefined}\n      visibleDuration={{ months: granularity === \"day\" ? numberOfMonths : 1 }}\n      className={(renderProps) =>\n        cn(ROOT_CLASSES, typeof className === \"function\" ? className(renderProps) : className)\n      }\n    >\n      <CalendarInner\n        buttonVariant={buttonVariant}\n        captionLayout={captionLayout}\n        numberOfMonths={numberOfMonths}\n        showWeekNumber={showWeekNumber}\n        headerFormat={headerFormat}\n        renderCell={renderCell}\n        granularity={granularity}\n        isPeriodUnavailable={isUnavailable}\n        correction={correctBlockedValue ? { minValue, maxValue, unavailableRanges } : null}\n      />\n    </AriaCalendar>\n  );\n}\n\nfunction RangeCalendar<T extends DateValue>({\n  buttonVariant,\n  captionLayout,\n  numberOfMonths = 1,\n  showWeekNumber,\n  headerFormat,\n  renderCell,\n  granularity = \"day\",\n  unavailableRanges,\n  fixedWeeks = true,\n  correctBlockedValue = true,\n  minValue,\n  maxValue,\n  isDateUnavailable,\n  onChange,\n  className,\n  ...props\n}: RangeCalendarProps<T> & CalendarExtraProps) {\n  const bounds = useGranularityBounds(granularity, minValue, maxValue);\n  const isUnavailable = useUnavailablePredicate(isDateUnavailable, unavailableRanges);\n  const isPeriodUnavailable = React.useMemo(\n    () => isUnavailable && ((date: DateValue) => isUnavailable(date, null)),\n    [isUnavailable],\n  );\n  // react-aria commits the clicked period's first day as `end`; widen it to\n  // the period's last day so the emitted range covers the whole period.\n  const handleChange = React.useMemo<typeof onChange>(() => {\n    if (!onChange || granularity === \"day\") return onChange;\n    return (value) => onChange({ start: value.start, end: periodEnd(value.end, granularity) });\n  }, [onChange, granularity]);\n\n  return (\n    <AriaRangeCalendar\n      {...props}\n      data-slot=\"calendar\"\n      onChange={handleChange}\n      minValue={bounds.minValue}\n      maxValue={bounds.maxValue}\n      isDateUnavailable={granularity === \"day\" ? isUnavailable : NEVER_UNAVAILABLE}\n      weeksInMonth={fixedWeeks ? 6 : undefined}\n      visibleDuration={{ months: granularity === \"day\" ? numberOfMonths : 1 }}\n      className={(renderProps) =>\n        cn(ROOT_CLASSES, typeof className === \"function\" ? className(renderProps) : className)\n      }\n    >\n      <CalendarInner\n        buttonVariant={buttonVariant}\n        captionLayout={captionLayout}\n        numberOfMonths={numberOfMonths}\n        showWeekNumber={showWeekNumber}\n        headerFormat={headerFormat}\n        renderCell={renderCell}\n        granularity={granularity}\n        isPeriodUnavailable={isPeriodUnavailable}\n        correction={correctBlockedValue ? { minValue, maxValue, unavailableRanges } : null}\n        isRange\n      />\n    </AriaRangeCalendar>\n  );\n}\n\ninterface CalendarInnerProps {\n  buttonVariant?: ButtonVariant;\n  captionLayout?: \"label\" | \"dropdown\" | \"grid\";\n  numberOfMonths: number;\n  showWeekNumber?: boolean;\n  headerFormat?: Intl.DateTimeFormatOptions;\n  renderCell?: RenderCell;\n  granularity: CalendarGranularity;\n  isPeriodUnavailable?: (date: DateValue) => boolean;\n  correction: {\n    minValue?: DateValue | null;\n    maxValue?: DateValue | null;\n    unavailableRanges?: Interval<DateValue>[];\n  } | null;\n  isRange?: boolean;\n}\n\nfunction CalendarInner(props: CalendarInnerProps) {\n  const calendarState = React.useContext(CalendarStateContext);\n  const rangeState = React.useContext(RangeCalendarStateContext);\n  const state = props.isRange ? rangeState : calendarState;\n  if (!state) return null;\n  return <CalendarViews {...props} state={state} rangeState={props.isRange ? rangeState : null} />;\n}\n\nfunction getSelectedDates(value: AnyCalendarState[\"value\"]): DateValue[] {\n  if (!value) return [];\n  if (\"start\" in value) return [value.start, value.end];\n  if (\"calendar\" in value) return [value];\n  return value.filter((date): date is CalendarDate => date !== null);\n}\n\n// The page (range of dates) a granularity's selection view currently shows.\nfunction getVisiblePage(state: AnyCalendarState, granularity: CalendarGranularity) {\n  if (granularity === \"day\") return state.visibleRange;\n  const { focusedDate } = state;\n  if (granularity === \"month\") {\n    return { start: startOfYear(focusedDate), end: endOfYear(focusedDate) };\n  }\n  const decadeStart = focusedDate.year - (focusedDate.year % 10);\n  return {\n    start: startOfYear(focusedDate.set({ year: decadeStart - 1 })),\n    end: endOfYear(focusedDate.set({ year: decadeStart + 10 })),\n  };\n}\n\nfunction CalendarViews({\n  state,\n  rangeState,\n  granularity,\n  captionLayout = \"grid\",\n  buttonVariant = \"ghost\",\n  numberOfMonths,\n  showWeekNumber = false,\n  headerFormat,\n  renderCell,\n  isPeriodUnavailable,\n  correction,\n  isRange,\n}: CalendarInnerProps & { state: AnyCalendarState; rangeState: RangeCalendarState | null }) {\n  const views = VIEW_ORDER.slice(VIEW_ORDER.indexOf(granularity));\n  const [view, setView] = React.useState<CalendarGranularity>(granularity);\n  const [prevGranularity, setPrevGranularity] = React.useState(granularity);\n  if (prevGranularity !== granularity) {\n    setPrevGranularity(granularity);\n    setView(granularity);\n  }\n\n  // Moving between views (or around a month/year grid with the keyboard)\n  // hides the element that had focus, so the next commit hands focus to the\n  // active view's focusable cell — the same roving-tabindex cell react-aria\n  // uses for the day grid.\n  const viewRefs = React.useRef<Partial<Record<CalendarGranularity, HTMLDivElement | null>>>({});\n  const focusIntentRef = React.useRef(false);\n  React.useLayoutEffect(() => {\n    if (!focusIntentRef.current) return;\n    focusIntentRef.current = false;\n    viewRefs.current[view]?.querySelector<HTMLElement>(CELL_FOCUS_TARGET)?.focus();\n  });\n\n  const showView = (next: CalendarGranularity, focusedDate?: CalendarDate) => {\n    if (focusedDate) state.setFocusedDate(focusedDate);\n    focusIntentRef.current = true;\n    setView(next);\n  };\n\n  const selectedDates = React.useMemo(() => getSelectedDates(state.value), [state.value]);\n\n  // react-aria only derives the visible page from `value` on mount: a\n  // controlled value changed from outside (e.g. typed into a paired input)\n  // would leave the grid on the old page. Re-page only when none of the\n  // selected dates is visible, so committing a range whose start sits on an\n  // earlier page doesn't yank the view back to it.\n  const valueKey = selectedDates.map(String).join(\"|\");\n  const prevValueKeyRef = React.useRef(valueKey);\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-page only when the selected dates change, not on every navigation\n  React.useEffect(() => {\n    if (prevValueKeyRef.current === valueKey) return;\n    prevValueKeyRef.current = valueKey;\n    const [first] = selectedDates;\n    if (!first) return;\n    const page = getVisiblePage(state, granularity);\n    const isVisible = selectedDates.some(\n      (date) => date.compare(page.start) >= 0 && date.compare(page.end) <= 0,\n    );\n    if (!isVisible) {\n      state.setFocusedDate(toCalendar(toCalendarDate(first), state.focusedDate.calendar));\n    }\n  }, [valueKey]);\n\n  useAvailableDateCorrection({\n    // Travel/clear works on days; month/year values are validated per period.\n    value: correction && granularity === \"day\" ? selectedDates : undefined,\n    min: correction?.minValue ?? undefined,\n    max: correction?.maxValue ?? undefined,\n    unavailableRanges: correction?.unavailableRanges,\n    // RangeCalendarState also carries the base state's `selectionMode: \"single\"`.\n    selectionMode: rangeState\n      ? \"range\"\n      : \"selectionMode\" in state\n        ? state.selectionMode\n        : undefined,\n    clearValue: () => state.setValue(null),\n    setValue: (dates) => {\n      if (rangeState) {\n        const [start, end = start] = dates;\n        if (start && end) rangeState.setValue({ start, end });\n        return;\n      }\n      if (!(\"selectionMode\" in state)) return;\n      const days = dates.map((date) => toCalendarDate(date));\n      state.setValue(state.selectionMode === \"multiple\" ? days : (days[0] ?? null));\n    },\n  });\n\n  const handlePeriodPick = (period: PeriodView, date: CalendarDate) => {\n    if (period === granularity) {\n      state.setFocusedDate(date);\n      state.selectDate(date);\n      return;\n    }\n    // Navigation only: keep the focused day/month and move to the picked\n    // period, one level down (year → month → day).\n    const { focusedDate } = state;\n    const next =\n      period === \"year\"\n        ? date.set({ month: focusedDate.month, day: focusedDate.day })\n        : date.set({ day: focusedDate.day });\n    showView(period === \"year\" ? \"month\" : \"day\", next);\n  };\n\n  const backToGranularity = view === granularity ? undefined : () => showView(granularity);\n\n  return (\n    <div className=\"grid\">\n      {views.map((v) => (\n        <div\n          key={v}\n          ref={(el) => {\n            viewRefs.current[v] = el;\n          }}\n          data-view={v}\n          // All views share one grid cell, so the container keeps the size of\n          // the largest and switching never shifts layout. Hidden with\n          // opacity (+ inert/aria-hidden), not `visibility`: the cells'\n          // `transition-all` interpolates the inherited `visibility`, which\n          // keeps them `hidden` for the first frame and makes focusing the\n          // newly shown view fail.\n          inert={v !== view}\n          aria-hidden={v !== view || undefined}\n          className={cn(\n            \"col-start-1 row-start-1 min-w-0 transition-opacity duration-150\",\n            v !== view && \"pointer-events-none opacity-0\",\n          )}\n        >\n          {v === \"day\" ? (\n            <DayView\n              captionLayout={captionLayout}\n              buttonVariant={buttonVariant}\n              numberOfMonths={numberOfMonths}\n              showWeekNumber={showWeekNumber}\n              headerFormat={headerFormat}\n              renderCell={renderCell}\n              isRange={isRange}\n              state={state}\n              onShowView={showView}\n            />\n          ) : (\n            <PeriodGrid\n              period={v}\n              state={state}\n              rangeState={rangeState}\n              selectedDates={selectedDates}\n              isSelectionView={v === granularity}\n              isPeriodUnavailable={isPeriodUnavailable}\n              buttonVariant={buttonVariant}\n              onPick={(date) => handlePeriodPick(v, date)}\n              onShowYears={v === \"month\" ? () => showView(\"year\") : undefined}\n              onEscape={backToGranularity}\n              onKeyboardMove={() => {\n                focusIntentRef.current = true;\n              }}\n            />\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction formatDate(\n  date: CalendarDate,\n  locale: string,\n  timeZone: string,\n  options: Intl.DateTimeFormatOptions,\n) {\n  return new DateFormatter(locale, {\n    ...options,\n    calendar: date.calendar.identifier,\n    timeZone,\n  }).format(date.toDate(timeZone));\n}\n\nfunction DayView({\n  captionLayout,\n  buttonVariant,\n  numberOfMonths,\n  showWeekNumber,\n  headerFormat,\n  renderCell,\n  isRange,\n  state,\n  onShowView,\n}: {\n  captionLayout: \"label\" | \"dropdown\" | \"grid\";\n  buttonVariant: ButtonVariant;\n  numberOfMonths: number;\n  showWeekNumber: boolean;\n  headerFormat?: Intl.DateTimeFormatOptions;\n  renderCell?: RenderCell;\n  isRange?: boolean;\n  state: AnyCalendarState;\n  onShowView: (view: CalendarGranularity, focusedDate?: CalendarDate) => void;\n}) {\n  const { locale } = useLocale();\n\n  return (\n    <div className=\"relative flex flex-col gap-4 md:flex-row\">\n      <header className=\"pointer-events-none absolute inset-x-0 top-0 z-10 flex w-full items-center justify-between gap-1 *:pointer-events-auto\">\n        <Button variant={buttonVariant} slot=\"previous\" className={navButtonClasses(buttonVariant)}>\n          <ChevronLeftIcon className=\"size-4 rtl:rotate-180\" />\n        </Button>\n        <Button variant={buttonVariant} slot=\"next\" className={navButtonClasses(buttonVariant)}>\n          <ChevronRightIcon className=\"size-4 rtl:rotate-180\" />\n        </Button>\n      </header>\n      {Array.from({ length: numberOfMonths }, (_, i) => {\n        const panelMonth = state.visibleRange.start.add({ months: i });\n        // Opening a grid from panel `i` keeps the focused day when it's\n        // already in that panel's month, otherwise starts from the panel.\n        const panelFocus = isSameMonth(state.focusedDate, panelMonth)\n          ? state.focusedDate\n          : panelMonth;\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: fixed-length sequential month offsets, i IS the identity\n          <div key={i} className=\"flex w-full flex-col gap-4\">\n            <div className=\"flex h-(--cell-size) w-full items-center justify-center gap-1 px-(--cell-size)\">\n              {captionLayout === \"dropdown\" ? (\n                <>\n                  <MonthDropdown format={headerFormat} />\n                  <YearDropdown format={headerFormat} />\n                </>\n              ) : captionLayout === \"grid\" ? (\n                <>\n                  <button\n                    type=\"button\"\n                    aria-label=\"Choose month\"\n                    className={captionButtonClasses(buttonVariant)}\n                    onClick={() => onShowView(\"month\", panelFocus)}\n                  >\n                    {formatDate(panelMonth, locale, state.timeZone, {\n                      month: headerFormat?.month ?? \"long\",\n                    })}\n                  </button>\n                  <button\n                    type=\"button\"\n                    aria-label=\"Choose year\"\n                    className={captionButtonClasses(buttonVariant)}\n                    onClick={() => onShowView(\"year\", panelFocus)}\n                  >\n                    {formatDate(panelMonth, locale, state.timeZone, {\n                      year: headerFormat?.year ?? \"numeric\",\n                    })}\n                  </button>\n                </>\n              ) : (\n                <CalendarHeading\n                  offset={{ months: i }}\n                  format={headerFormat}\n                  className=\"text-sm font-medium select-none\"\n                />\n              )}\n            </div>\n            <CalendarGrid className=\"w-full border-collapse\" offset={{ months: i }}>\n              <AriaCalendarGridHeader>\n                {(day) => (\n                  <CalendarHeaderCell className=\"rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none\">\n                    {day}\n                  </CalendarHeaderCell>\n                )}\n              </AriaCalendarGridHeader>\n              <CalendarGridBody>\n                {(date) => (\n                  <CalendarCell\n                    date={date}\n                    className={(renderProps) => cellVariants({ ...renderProps, showWeekNumber })}\n                  >\n                    {(renderProps) => (\n                      <div\n                        data-selected-single={renderProps.isSelected && !isRange}\n                        data-range-start={renderProps.isSelectionStart && isRange}\n                        data-range-end={renderProps.isSelectionEnd && isRange}\n                        data-range-middle={\n                          renderProps.isSelected &&\n                          !renderProps.isSelectionStart &&\n                          !renderProps.isSelectionEnd &&\n                          isRange\n                        }\n                        className={cn(\n                          buttonVariants({ variant: \"ghost\", size: \"icon\" }),\n                          \"relative isolate z-10 flex aspect-square h-full w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70\",\n                        )}\n                      >\n                        {renderCell ? renderCell(renderProps) : renderProps.defaultChildren}\n                      </div>\n                    )}\n                  </CalendarCell>\n                )}\n              </CalendarGridBody>\n            </CalendarGrid>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\ninterface PeriodCell {\n  date: CalendarDate;\n  label: string;\n  fullLabel: string;\n  isFocused: boolean;\n  isOutside: boolean;\n  isToday: boolean;\n  isDisabled: boolean;\n  isUnavailable: boolean;\n  isSelected: boolean;\n  isRangeStart: boolean;\n  isRangeEnd: boolean;\n}\n\n// Month (3×4 for the focused year) or year (3×4 for the focused decade ± 1\n// year, outer years muted) grid. Selection, range anchor/preview and focus all\n// go through the react-aria calendar state, so `value`/`onChange`, read-only,\n// min/max and the range's hover preview behave exactly like the day grid.\nfunction PeriodGrid({\n  period,\n  state,\n  rangeState,\n  selectedDates,\n  isSelectionView,\n  isPeriodUnavailable,\n  buttonVariant,\n  onPick,\n  onShowYears,\n  onEscape,\n  onKeyboardMove,\n}: {\n  period: PeriodView;\n  state: AnyCalendarState;\n  rangeState: RangeCalendarState | null;\n  selectedDates: DateValue[];\n  isSelectionView: boolean;\n  isPeriodUnavailable?: (date: DateValue) => boolean;\n  buttonVariant: ButtonVariant;\n  onPick: (date: CalendarDate) => void;\n  onShowYears?: () => void;\n  onEscape?: () => void;\n  onKeyboardMove: () => void;\n}) {\n  const { locale, direction } = useLocale();\n  const { focusedDate, minValue, maxValue, timeZone } = state;\n  const todayDate = today(timeZone);\n  const range = rangeState?.highlightedRange ?? null;\n  const decadeStart = focusedDate.year - (focusedDate.year % 10);\n  const firstCell =\n    period === \"month\"\n      ? startOfYear(focusedDate)\n      : startOfYear(focusedDate.set({ year: decadeStart - 1 }));\n  const cellCount =\n    period === \"month\" ? focusedDate.calendar.getMonthsInYear(focusedDate) : PERIOD_COLUMNS * 4;\n  const step = (n: number) => (period === \"month\" ? { months: n } : { years: n });\n  const page = period === \"month\" ? { years: 1 } : { years: 10 };\n\n  const isOutOfBounds = (start: CalendarDate, end: CalendarDate) =>\n    (minValue != null && end.compare(minValue) < 0) ||\n    (maxValue != null && start.compare(maxValue) > 0);\n\n  const cells: PeriodCell[] = Array.from({ length: cellCount }, (_, i) => {\n    const date = firstCell.add(step(i));\n    const end = periodEnd(date, period);\n    const isDisabled = state.isDisabled || isOutOfBounds(date, end);\n    const isUnavailable =\n      isSelectionView &&\n      !isDisabled &&\n      !!isPeriodUnavailable &&\n      isFullyUnavailable(date, end, isPeriodUnavailable);\n    const isPickable = !isDisabled && !isUnavailable;\n    const inRange =\n      isPickable && !!range && date.compare(range.end) <= 0 && end.compare(range.start) >= 0;\n    return {\n      date,\n      label: formatDate(\n        date,\n        locale,\n        timeZone,\n        period === \"month\" ? { month: \"short\" } : { year: \"numeric\" },\n      ),\n      fullLabel: formatDate(\n        date,\n        locale,\n        timeZone,\n        period === \"month\" ? { month: \"long\", year: \"numeric\" } : { year: \"numeric\" },\n      ),\n      isFocused: isSamePeriod(date, focusedDate, period),\n      isOutside: period === \"year\" && (date.year < decadeStart || date.year > decadeStart + 9),\n      isToday: isSamePeriod(date, todayDate, period),\n      isDisabled,\n      isUnavailable,\n      isSelected: rangeState\n        ? inRange\n        : isPickable && selectedDates.some((value) => isSamePeriod(value, date, period)),\n      isRangeStart: inRange && !!range && isSamePeriod(range.start, date, period),\n      isRangeEnd: inRange && !!range && isSamePeriod(range.end, date, period),\n    };\n  });\n  const rows = Array.from({ length: Math.ceil(cells.length / PERIOD_COLUMNS) }, (_, row) =>\n    cells.slice(row * PERIOD_COLUMNS, (row + 1) * PERIOD_COLUMNS),\n  );\n\n  const prevPageLast = periodEnd(firstCell.subtract(step(1)), period);\n  const nextPageFirst = firstCell.add(step(cellCount));\n  const isPrevDisabled =\n    state.isDisabled || (minValue != null && prevPageLast.compare(minValue) < 0);\n  const isNextDisabled =\n    state.isDisabled || (maxValue != null && nextPageFirst.compare(maxValue) > 0);\n\n  const pageLabel =\n    period === \"month\"\n      ? formatDate(focusedDate, locale, timeZone, { year: \"numeric\" })\n      : `${decadeStart} – ${decadeStart + 9}`;\n\n  const moveFocus = (date: CalendarDate) => {\n    onKeyboardMove();\n    state.setFocusedDate(date);\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    const forward = direction === \"rtl\" ? -1 : 1;\n    switch (event.key) {\n      case \"ArrowLeft\":\n        moveFocus(focusedDate.subtract(step(forward)));\n        break;\n      case \"ArrowRight\":\n        moveFocus(focusedDate.add(step(forward)));\n        break;\n      case \"ArrowUp\":\n        moveFocus(focusedDate.subtract(step(PERIOD_COLUMNS)));\n        break;\n      case \"ArrowDown\":\n        moveFocus(focusedDate.add(step(PERIOD_COLUMNS)));\n        break;\n      case \"Home\":\n        moveFocus(firstCell.set({ day: focusedDate.day }));\n        break;\n      case \"End\":\n        moveFocus(firstCell.add(step(cellCount - 1)).set({ day: focusedDate.day }));\n        break;\n      case \"PageUp\":\n        moveFocus(focusedDate.subtract(page));\n        break;\n      case \"PageDown\":\n        moveFocus(focusedDate.add(page));\n        break;\n      case \"Escape\":\n        if (!onEscape) return;\n        onEscape();\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n    event.stopPropagation();\n  };\n\n  return (\n    <div className=\"flex h-full flex-col gap-4\">\n      <div className=\"flex h-(--cell-size) items-center justify-between gap-1\">\n        <button\n          type=\"button\"\n          aria-label={period === \"month\" ? \"Previous year\" : \"Previous decade\"}\n          disabled={isPrevDisabled}\n          className={navButtonClasses(buttonVariant)}\n          onClick={() => state.setFocusedDate(focusedDate.subtract(page))}\n        >\n          <ChevronLeftIcon className=\"size-4 rtl:rotate-180\" />\n        </button>\n        {onShowYears ? (\n          <button\n            type=\"button\"\n            aria-label=\"Choose year\"\n            aria-live=\"polite\"\n            className={captionButtonClasses(buttonVariant)}\n            onClick={onShowYears}\n          >\n            {pageLabel}\n          </button>\n        ) : (\n          <span aria-live=\"polite\" className=\"text-sm font-medium tabular-nums select-none\">\n            {pageLabel}\n          </span>\n        )}\n        <button\n          type=\"button\"\n          aria-label={period === \"month\" ? \"Next year\" : \"Next decade\"}\n          disabled={isNextDisabled}\n          className={navButtonClasses(buttonVariant)}\n          onClick={() => state.setFocusedDate(focusedDate.add(page))}\n        >\n          <ChevronRightIcon className=\"size-4 rtl:rotate-180\" />\n        </button>\n      </div>\n      {/* ARIA grid pattern (roving tabindex + arrow keys), like react-aria's day grid table. */}\n      <table\n        // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: ARIA grid pattern\n        role=\"grid\"\n        aria-label={pageLabel}\n        aria-readonly={state.isReadOnly || undefined}\n        data-slot=\"calendar-period-grid\"\n        data-period={period}\n        className=\"h-full w-full min-w-[calc(var(--cell-size)*7)] flex-1 border-collapse\"\n        onKeyDown={handleKeyDown}\n      >\n        <tbody>\n          {rows.map((row) => (\n            <tr key={row[0]?.date.toString()}>\n              {row.map((cell) => {\n                const isBlocked = cell.isDisabled || cell.isUnavailable;\n                const isRangeMiddle = cell.isSelected && !cell.isRangeStart && !cell.isRangeEnd;\n                return (\n                  <td\n                    key={cell.date.toString()}\n                    // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: ARIA grid pattern\n                    role=\"gridcell\"\n                    aria-selected={cell.isSelected || undefined}\n                    aria-disabled={isBlocked || undefined}\n                    className=\"p-0 py-1\"\n                  >\n                    <button\n                      type=\"button\"\n                      tabIndex={cell.isFocused ? 0 : -1}\n                      aria-label={cell.fullLabel}\n                      aria-disabled={isBlocked || undefined}\n                      data-today={cell.isToday || undefined}\n                      data-outside={cell.isOutside || undefined}\n                      data-disabled={cell.isDisabled || undefined}\n                      data-unavailable={cell.isUnavailable || undefined}\n                      data-selected={(cell.isSelected && !rangeState) || undefined}\n                      data-range-start={(rangeState && cell.isRangeStart) || undefined}\n                      data-range-end={(rangeState && cell.isRangeEnd) || undefined}\n                      data-range-middle={(rangeState && isRangeMiddle) || undefined}\n                      className={PERIOD_CELL_CLASSES}\n                      onClick={() => {\n                        if (!isBlocked) onPick(cell.date);\n                      }}\n                      onMouseEnter={() => {\n                        if (isSelectionView && !isBlocked) rangeState?.highlightDate(cell.date);\n                      }}\n                    >\n                      {cell.label}\n                    </button>\n                  </td>\n                );\n              })}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\nfunction MonthDropdown({ format }: { format?: Intl.DateTimeFormatOptions }) {\n  return (\n    <CalendarMonthPicker format={format?.month}>\n      {(props) => (\n        <Select {...props} className=\"relative\">\n          <SelectTrigger>\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent className=\"min-w-0\">\n            <SelectGroup>\n              {props.items.map((item) => (\n                <SelectItem key={item.id} id={item.id}>\n                  {item.formatted}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n      )}\n    </CalendarMonthPicker>\n  );\n}\n\nfunction YearDropdown({ format }: { format?: Intl.DateTimeFormatOptions }) {\n  return (\n    <CalendarYearPicker format={format}>\n      {(props) => (\n        <Select {...props} className=\"relative\">\n          <SelectTrigger>\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent className=\"min-w-0\">\n            {props.items.map((item) => (\n              <SelectItem key={item.id} id={item.id}>\n                {item.formatted}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )}\n    </CalendarYearPicker>\n  );\n}\n\nexport type { CalendarGranularity };\nexport { Calendar, RangeCalendar };\n",
      "type": "registry:component",
      "target": "components/fma-ui/calendar.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "customized",
    "tags": [
      "form",
      "date",
      "react-aria"
    ]
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}