{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"calendar","title":"Calendar","description":"Locale-aware inline calendar with single, contiguous range and multiple date selection.","dependencies":["@internationalized/date@3.12.3","lucide-react@1.18.0","react-aria-components@1.20.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/locale","@siteplane/popover"],"files":[{"path":"packages/ui/src/components/calendar/calendar.tsx","content":"\"use client\";\n\nimport {\n  CalendarDate,\n  endOfMonth,\n  GregorianCalendar,\n  isSameDay,\n  isSameMonth,\n  isSameYear,\n  startOfMonth,\n  toCalendar,\n  toCalendarDate,\n} from \"@internationalized/date\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronsUpDownIcon,\n} from \"lucide-react\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\nimport {\n  Button as CalendarButtonPrimitive,\n  Calendar as CalendarPrimitive,\n  CalendarCell as CalendarCellPrimitive,\n  CalendarContext,\n  CalendarGrid as CalendarGridPrimitive,\n  CalendarGridBody as CalendarGridBodyPrimitive,\n  CalendarGridHeader as CalendarGridHeaderPrimitive,\n  CalendarHeaderCell as CalendarHeaderCellPrimitive,\n  CalendarMonthPicker as CalendarMonthPickerPrimitive,\n  type CalendarState,\n  CalendarStateContext,\n  type CalendarProps as CalendarPrimitiveProps,\n  CalendarYearPicker as CalendarYearPickerPrimitive,\n  type DateValue,\n  RangeCalendar as RangeCalendarPrimitive,\n  RangeCalendarContext,\n  type RangeCalendarState,\n  RangeCalendarStateContext,\n  type RangeValue,\n  useLocale,\n  useSlottedContext,\n} from \"react-aria-components\";\n\nimport { nativeDayTime, toNativeDay } from \"../../system/date/native-day\";\nimport { isInvalidAriaValue } from \"../../system/form/aria-invalid\";\nimport { normalizeFormattedText } from \"../../system/locale/formatted-text\";\nimport { SiteplaneLocaleProvider } from \"../../system/locale/siteplane-locale-provider\";\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\nimport { Button } from \"../button\";\nimport { Popover, PopoverPopup, PopoverTrigger } from \"../popover\";\n\nimport \"./calendar.css\";\n\nexport type CalendarSelectionMode = \"multiple\" | \"range\" | \"single\";\ntype PointCalendarSelectionMode = Exclude<CalendarSelectionMode, \"range\">;\n\n/**\n * A range of days, from its first day to its last. An end is unset while the\n * range is incomplete.\n */\nexport type DateRange = {\n  from?: Date | undefined;\n  to?: Date | undefined;\n};\n\n/**\n * Days that can't be chosen: one date, a test, or a rule. A rule matches the\n * days after `after`, the days before `before`, the days from `from` to `to`\n * (both included), and the weekdays in `dayOfWeek` (0 Sunday to 6 Saturday).\n */\nexport type CalendarDayMatcher =\n  | Date\n  | ((date: Date) => boolean)\n  | {\n      after?: Date | undefined;\n      before?: Date | undefined;\n      dayOfWeek?: readonly number[] | undefined;\n      from?: Date | undefined;\n      to?: Date | undefined;\n    };\n\n/**\n * The calendar options of Calendar and of every date picker's\n * `calendarProps`. Dates are native `Date` values; only their day counts.\n */\nexport type CalendarOptions = {\n  /**\n   * The month the calendar shows first. While it is set, it wins over the\n   * month of the selected date.\n   */\n  defaultMonth?: Date | undefined;\n  /**\n   * `true` disables the whole calendar. A matcher, or a list of matchers,\n   * makes the days it matches unavailable; they can't be chosen.\n   */\n  disabled?:\n    boolean | CalendarDayMatcher | readonly CalendarDayMatcher[] | undefined;\n  /** The last month the calendar allows. */\n  endMonth?: Date | undefined;\n  /** The first month the calendar allows. */\n  startMonth?: Date | undefined;\n  /**\n   * The day that counts as today, for an application whose today is not the\n   * device's, such as a demo fixed on one date. The calendar marks it, and\n   * without a selected date it shows its month first. Unset, today is the\n   * device's date.\n   */\n  todayDate?: Date | undefined;\n};\n\n/**\n * A quick choice of a range calendar: a label, and the range it selects.\n */\nexport type CalendarRangeShortcut = {\n  label: string;\n  value: { from: Date; to: Date };\n};\n\ntype CalendarBaseProps = CalendarOptions & {\n  \"aria-describedby\"?: string | undefined;\n  /** Marks the selected days as invalid. */\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"];\n  \"aria-label\"?: string | undefined;\n  \"aria-labelledby\"?: string | undefined;\n  className?: string | undefined;\n  /** Formats this calendar in another locale than the surrounding one. */\n  locale?: string | undefined;\n  /**\n   * The month the calendar shows, as any day in it. Set it from\n   * `onMonthChange` to control the month, for example for a Today button.\n   */\n  month?: Date | undefined;\n  /**\n   * Reports the first day of the month that takes focus, when the user moves\n   * to another month.\n   */\n  onMonthChange?: ((month: Date) => void) | undefined;\n  /** Keeps the selection as it is; the user can still move through months. */\n  readOnly?: boolean | undefined;\n  size?: SiteplaneControlSize | undefined;\n};\n\nexport type CalendarSingleProps = CalendarBaseProps & {\n  selectionMode?: \"single\" | undefined;\n  /** The selected day; `null` is no selection. */\n  value?: Date | null | undefined;\n  defaultValue?: Date | null | undefined;\n  onValueChange?: ((value: Date) => void) | undefined;\n};\n\nexport type CalendarMultipleProps = CalendarBaseProps & {\n  selectionMode: \"multiple\";\n  /** The selected days. */\n  value?: readonly Date[] | undefined;\n  defaultValue?: readonly Date[] | undefined;\n  onValueChange?: ((value: Date[]) => void) | undefined;\n};\n\nexport type CalendarRangeProps = CalendarBaseProps & {\n  selectionMode: \"range\";\n  /** The selected range; a range without both ends selects nothing. */\n  value?: DateRange | null | undefined;\n  defaultValue?: DateRange | null | undefined;\n  onValueChange?: ((value: { from: Date; to: Date }) => void) | undefined;\n  /**\n   * Shows consecutive months side by side, with one selection and one\n   * Previous and Next pair.\n   */\n  numberOfMonths?: number | undefined;\n  /**\n   * Quick choices such as \"Last week\", as links in the accent colour between\n   * the heading and the days. A choice selects its range and shows the month it\n   * starts in. One that starts or ends on a date the calendar does not allow\n   * (outside `startMonth` and `endMonth`, or unavailable) is disabled. A\n   * calendar with several months shows them once, above the months.\n   */\n  shortcuts?: readonly CalendarRangeShortcut[] | undefined;\n};\n\nexport type CalendarProps =\n  CalendarMultipleProps | CalendarRangeProps | CalendarSingleProps;\n\n/** The React Aria day of a native date; only its day counts. */\nexport function fromNativeDate(date: Date): CalendarDate {\n  return new CalendarDate(\n    date.getFullYear(),\n    date.getMonth() + 1,\n    date.getDate(),\n  );\n}\n\n/**\n * The native date, at local midnight, of a React Aria day in any calendar\n * system. A day that the locale shows in another calendar system, such as the\n * focused day of a Thai calendar, reads its year in the Gregorian calendar.\n */\nexport function toNativeDate(value: DateValue): Date {\n  return toNativeDay(\n    toCalendar(toCalendarDate(value), new GregorianCalendar()),\n  );\n}\n\nfunction matchesDay(date: Date, matcher: CalendarDayMatcher): boolean {\n  if (matcher instanceof Date) {\n    return nativeDayTime(matcher) === nativeDayTime(date);\n  }\n  if (typeof matcher === \"function\") return matcher(date);\n\n  const time = nativeDayTime(date);\n  const dayTime = (day: Date | undefined) =>\n    day === undefined ? undefined : nativeDayTime(day);\n  const after = dayTime(matcher.after);\n  const before = dayTime(matcher.before);\n  const from = dayTime(matcher.from);\n  const to = dayTime(matcher.to);\n\n  return (\n    (after !== undefined && time > after) ||\n    (before !== undefined && time < before) ||\n    (from !== undefined && to !== undefined && time >= from && time <= to) ||\n    (matcher.dayOfWeek?.includes(date.getDay()) ?? false)\n  );\n}\n\n/**\n * Whether `disabled` of the calendar options rules out `date`: every day of a\n * disabled calendar, and the days a matcher matches. Pickers check typed\n * dates with it, so typing and picking follow one rule.\n */\nexport function isCalendarDayDisabled(\n  date: Date,\n  disabled: CalendarOptions[\"disabled\"],\n): boolean {\n  if (disabled === undefined || disabled === false) return false;\n  if (disabled === true) return true;\n  const matchers: readonly CalendarDayMatcher[] = Array.isArray(disabled)\n    ? disabled\n    : [disabled as CalendarDayMatcher];\n\n  return matchers.some((matcher) => matchesDay(date, matcher));\n}\n\n/**\n * The first allowed day of `startMonth` and the last of `endMonth`, as React\n * Aria bounds. Unset options are left out.\n */\nexport function calendarMonthBounds({\n  endMonth,\n  startMonth,\n}: Pick<CalendarOptions, \"endMonth\" | \"startMonth\">): {\n  maxValue?: CalendarDate;\n  minValue?: CalendarDate;\n} {\n  return {\n    ...(endMonth === undefined\n      ? {}\n      : { maxValue: endOfMonth(fromNativeDate(endMonth)) }),\n    ...(startMonth === undefined\n      ? {}\n      : { minValue: startOfMonth(fromNativeDate(startMonth)) }),\n  };\n}\n\n/**\n * The React Aria props of the options and states every selection mode shares.\n * Unset options are left out, so a date picker's own calendar settings, which\n * reach the calendar through React Aria's context, still apply.\n * `hasSelection` tells whether the calendar opens on a selected date, its own\n * or the one of the date picker around it.\n */\nfunction useSharedCalendarProps(\n  {\n    \"aria-invalid\": ariaInvalid,\n    defaultMonth,\n    disabled,\n    endMonth,\n    month,\n    onMonthChange,\n    readOnly,\n    startMonth,\n    todayDate,\n  }: CalendarBaseProps,\n  hasSelection: boolean,\n  visibleMonths = 1,\n) {\n  // The day with keyboard focus and the months on screen, as the Calendar\n  // content last rendered them.\n  const focusedDateRef = useRef<CalendarDate | null>(null);\n  const visibleRangeRef = useRef<VisibleRange | null>(null);\n  const controlledMonth =\n    month === undefined ? undefined : fromNativeDate(month);\n  // While `month` is controlled, the focused day stays the Calendar's own\n  // inside that month, so the arrow keys move through its days; a `month` in\n  // another month moves the focus there.\n  const [focused, setFocused] = useState<CalendarDate | undefined>(\n    controlledMonth,\n  );\n  // A `month` the application moves to outside the months on screen shows\n  // first, as the month of a shortcut does. React Aria shows a focused month\n  // before the visible ones last, so the focus takes the steps that realign\n  // the visible months first (planVisibleRangeShift), one per commit, before\n  // it rests in `month`. A `month` already on screen stays where it is, and so\n  // does the month the calendar itself reported as the user moved, which\n  // React Aria shows already.\n  const [steps, setSteps] = useState<readonly CalendarDate[]>([]);\n  const reportedMonthRef = useRef<string | null>(null);\n  const monthKey = controlledMonth && startOfMonth(controlledMonth).toString();\n  const [shownMonthKey, setShownMonthKey] = useState(monthKey);\n  if (monthKey !== shownMonthKey) {\n    setShownMonthKey(monthKey);\n    const visible = visibleRangeRef.current;\n    setSteps(\n      controlledMonth &&\n        visible &&\n        visibleMonths > 1 &&\n        monthKey !== reportedMonthRef.current &&\n        !isMonthInRange(controlledMonth, visible)\n        ? planVisibleRangeShift(visible, visibleMonths, controlledMonth)\n        : [],\n    );\n  }\n  useLayoutEffect(() => {\n    if (steps.length > 0) setSteps((current) => current.slice(1));\n  }, [steps]);\n  const focusedValue =\n    controlledMonth === undefined\n      ? undefined\n      : (steps[0] ??\n        (focused !== undefined && isSameMonth(focused, controlledMonth)\n          ? focused\n          : controlledMonth));\n  const onFocusChange = (date: CalendarDate) => {\n    if (controlledMonth !== undefined) setFocused(date);\n\n    const previous = focusedDateRef.current;\n\n    if (previous && !isSameMonth(date, previous)) {\n      reportedMonthRef.current = startOfMonth(date).toString();\n      onMonthChange?.(toNativeDate(startOfMonth(date)));\n    }\n  };\n  const dayMatcher =\n    typeof disabled === \"boolean\" || disabled === undefined\n      ? undefined\n      : disabled;\n  // The month shown first: `defaultMonth`, else the selected date's, which\n  // React Aria shows by itself, else the month of `todayDate`. Without\n  // `todayDate`, React Aria shows the device's today.\n  const firstMonth = defaultMonth ?? (hasSelection ? undefined : todayDate);\n\n  return {\n    focusedDateRef,\n    visibleRangeRef,\n    props: {\n      ...(disabled === true ? { isDisabled: true } : {}),\n      ...(dayMatcher === undefined\n        ? {}\n        : {\n            isDateUnavailable: (date: DateValue) =>\n              isCalendarDayDisabled(toNativeDate(date), dayMatcher),\n          }),\n      ...(readOnly === undefined ? {} : { isReadOnly: readOnly }),\n      ...(ariaInvalid === undefined\n        ? {}\n        : { isInvalid: isInvalidAriaValue(ariaInvalid) }),\n      ...calendarMonthBounds({ endMonth, startMonth }),\n      ...(focusedValue === undefined ? {} : { focusedValue }),\n      ...(firstMonth === undefined || controlledMonth !== undefined\n        ? {}\n        : { defaultFocusedValue: fromNativeDate(firstMonth) }),\n      ...(controlledMonth === undefined && onMonthChange === undefined\n        ? {}\n        : { onFocusChange }),\n    },\n  };\n}\n\nexport function Calendar(props: CalendarRangeProps): React.ReactElement;\nexport function Calendar(props: CalendarMultipleProps): React.ReactElement;\nexport function Calendar(props: CalendarSingleProps): React.ReactElement;\nexport function Calendar({\n  locale,\n  ...props\n}: CalendarProps): React.ReactElement {\n  const calendar =\n    props.selectionMode === \"range\" ? (\n      <CalendarRangeRoot {...props} />\n    ) : (\n      <CalendarRoot {...props} />\n    );\n\n  return locale ? (\n    <SiteplaneLocaleProvider locale={locale}>\n      {calendar}\n    </SiteplaneLocaleProvider>\n  ) : (\n    calendar\n  );\n}\n\n// The props the Calendar maps to React Aria itself; the rest (the accessible\n// names and descriptions and data attributes) reach the React Aria root as\n// they are.\nconst calendarOwnProps = new Set<string>([\n  \"aria-invalid\",\n  \"className\",\n  \"defaultMonth\",\n  \"defaultValue\",\n  \"disabled\",\n  \"endMonth\",\n  \"locale\",\n  \"month\",\n  \"numberOfMonths\",\n  \"onMonthChange\",\n  \"onValueChange\",\n  \"readOnly\",\n  \"selectionMode\",\n  \"shortcuts\",\n  \"size\",\n  \"startMonth\",\n  \"todayDate\",\n  \"value\",\n]);\n\nfunction passThroughProps(\n  props: CalendarProps,\n): Partial<\n  Record<\"aria-describedby\" | \"aria-label\" | \"aria-labelledby\", string>\n> {\n  return Object.fromEntries(\n    Object.entries(props).filter(([key]) => !calendarOwnProps.has(key)),\n  );\n}\n\ntype PointCalendarPrimitiveProps = Omit<\n  CalendarPrimitiveProps<CalendarDate, PointCalendarSelectionMode>,\n  \"children\"\n>;\n\n// The selection of a single or multiple calendar in React Aria's days.\nfunction pointSelectionProps(\n  props: CalendarMultipleProps | CalendarSingleProps,\n): PointCalendarPrimitiveProps {\n  if (props.selectionMode === \"multiple\") {\n    const { defaultValue, onValueChange, value } = props;\n\n    return {\n      ...(value === undefined ? {} : { value: value.map(fromNativeDate) }),\n      ...(defaultValue === undefined\n        ? {}\n        : { defaultValue: defaultValue.map(fromNativeDate) }),\n      ...(onValueChange === undefined\n        ? {}\n        : {\n            onChange: (next: readonly DateValue[]) =>\n              onValueChange(next.map(toNativeDate)),\n          }),\n    } as PointCalendarPrimitiveProps;\n  }\n\n  const { defaultValue, onValueChange, value } = props;\n\n  return {\n    ...(value === undefined\n      ? {}\n      : { value: value ? fromNativeDate(value) : null }),\n    ...(defaultValue === undefined\n      ? {}\n      : { defaultValue: defaultValue ? fromNativeDate(defaultValue) : null }),\n    ...(onValueChange === undefined\n      ? {}\n      : {\n          onChange: (next: DateValue | null) => {\n            if (next) onValueChange(toNativeDate(next));\n          },\n        }),\n  } as PointCalendarPrimitiveProps;\n}\n\n/** The React Aria day of `todayDate`, unset without it. */\nfunction toTodayDate(todayDate: Date | undefined): CalendarDate | undefined {\n  return todayDate === undefined ? undefined : fromNativeDate(todayDate);\n}\n\nfunction CalendarRoot(props: CalendarMultipleProps | CalendarSingleProps) {\n  const resolvedSize = useResolvedControlSize(props.size);\n  // A date picker passes its selection to the calendar through React Aria's\n  // context, not through these props.\n  const pickerValue = useSlottedContext(CalendarContext)?.value;\n  const selection =\n    props.value !== undefined ? props.value : props.defaultValue;\n  const hasSelection =\n    (Array.isArray(selection) ? selection.length > 0 : Boolean(selection)) ||\n    Boolean(pickerValue);\n  const shared = useSharedCalendarProps(props, hasSelection);\n\n  return (\n    <CalendarPrimitive<CalendarDate, PointCalendarSelectionMode>\n      {...passThroughProps(props)}\n      {...shared.props}\n      {...pointSelectionProps(props)}\n      className={cn(\"sp-base-calendar\", \"sp-calendar\", props.className)}\n      data-foundation=\"calendar\"\n      data-size={resolvedSize}\n      data-slot=\"calendar\"\n      selectionMode={props.selectionMode ?? \"single\"}\n    >\n      {({ isDisabled, state }) => (\n        <CalendarContent\n          focusedDateRef={shared.focusedDateRef}\n          isDisabled={isDisabled}\n          size={resolvedSize}\n          state={state}\n          todayDate={toTodayDate(props.todayDate)}\n          visibleRangeRef={shared.visibleRangeRef}\n        />\n      )}\n    </CalendarPrimitive>\n  );\n}\n\nfunction toRangeValue(\n  range: DateRange | null | undefined,\n): RangeValue<CalendarDate> | null {\n  return range?.from && range.to\n    ? { end: fromNativeDate(range.to), start: fromNativeDate(range.from) }\n    : null;\n}\n\nfunction CalendarRangeRoot(props: CalendarRangeProps) {\n  const { defaultValue, numberOfMonths, onValueChange, shortcuts, value } =\n    props;\n  const resolvedSize = useResolvedControlSize(props.size);\n  // A date range picker passes its range through React Aria's context.\n  const pickerRange = useSlottedContext(RangeCalendarContext)?.value;\n  const hasSelection =\n    toRangeValue(value !== undefined ? value : defaultValue) !== null ||\n    Boolean(pickerRange?.start);\n  // `numberOfMonths={2}` shows consecutive months side by side in one range\n  // calendar: one shared selection, one Previous and Next pair, and a heading\n  // with its own month and year picker above each month.\n  const visibleMonths = Math.max(1, Math.trunc(numberOfMonths ?? 1) || 1);\n  const shared = useSharedCalendarProps(props, hasSelection, visibleMonths);\n\n  return (\n    <RangeCalendarPrimitive<CalendarDate>\n      {...passThroughProps(props)}\n      {...shared.props}\n      {...(value === undefined ? {} : { value: toRangeValue(value) })}\n      {...(defaultValue === undefined\n        ? {}\n        : { defaultValue: toRangeValue(defaultValue) })}\n      {...(onValueChange === undefined\n        ? {}\n        : {\n            onChange: (next: RangeValue<DateValue>) =>\n              onValueChange({\n                from: toNativeDate(next.start),\n                to: toNativeDate(next.end),\n              }),\n          })}\n      {...(visibleMonths > 1\n        ? {\n            pageBehavior: \"single\" as const,\n            visibleDuration: { months: visibleMonths },\n          }\n        : {})}\n      className={cn(\"sp-base-calendar\", \"sp-calendar\", props.className)}\n      data-foundation=\"calendar\"\n      data-selection-mode=\"range\"\n      data-size={resolvedSize}\n      data-slot=\"calendar\"\n      data-visible-months={visibleMonths > 1 ? visibleMonths : undefined}\n    >\n      {({ isDisabled, state }) => (\n        <CalendarContent\n          focusedDateRef={shared.focusedDateRef}\n          isDisabled={isDisabled}\n          shortcuts={shortcuts}\n          size={resolvedSize}\n          state={state}\n          todayDate={toTodayDate(props.todayDate)}\n          visibleMonths={visibleMonths}\n          visibleRangeRef={shared.visibleRangeRef}\n        />\n      )}\n    </RangeCalendarPrimitive>\n  );\n}\n\ntype PickerView = \"month\" | \"year\";\ntype CalendarNavigationDirection = \"next\" | \"previous\";\ntype CalendarNavigationMotion = {\n  direction: CalendarNavigationDirection;\n  key: number;\n  previousStart: CalendarState<PointCalendarSelectionMode>[\"visibleRange\"][\"start\"];\n};\ntype CalendarPickerMotionDirection =\n  CalendarNavigationDirection | \"drill-in\" | \"drill-out\";\ntype CalendarPickerItem = {\n  date: CalendarState<PointCalendarSelectionMode>[\"focusedDate\"];\n  formatted: string;\n  id: number;\n  isSelected: boolean;\n};\ntype CalendarPickerPage = {\n  ariaLabel: string | undefined;\n  heading: React.ReactNode;\n  items: CalendarPickerItem[];\n  view: PickerView;\n};\ntype CalendarPickerPageMotion = {\n  direction: CalendarPickerMotionDirection;\n  previousPage: CalendarPickerPage;\n};\ntype CalendarPickerHeadingMotion = {\n  direction: CalendarNavigationDirection;\n  previousHeading: React.ReactNode;\n};\n\nfunction isRangeCalendarState<M extends PointCalendarSelectionMode>(\n  state: CalendarState<M> | RangeCalendarState,\n): state is RangeCalendarState {\n  return \"setAnchorDate\" in state;\n}\n\nfunction CalendarContent<M extends PointCalendarSelectionMode>({\n  focusedDateRef,\n  isDisabled,\n  shortcuts,\n  size,\n  state,\n  todayDate,\n  visibleMonths = 1,\n  visibleRangeRef,\n}: {\n  focusedDateRef: React.RefObject<CalendarDate | null>;\n  isDisabled: boolean;\n  shortcuts?: readonly CalendarRangeShortcut[] | undefined;\n  size: SiteplaneControlSize;\n  state: CalendarState<M> | RangeCalendarState;\n  todayDate?: CalendarDate | undefined;\n  visibleMonths?: number;\n  visibleRangeRef: React.RefObject<VisibleRange | null>;\n}) {\n  const { locale } = useLocale();\n\n  // The Calendar tells a change of month by the day that had focus last, and\n  // places a controlled month by the months on screen.\n  useLayoutEffect(() => {\n    focusedDateRef.current = state.focusedDate;\n    visibleRangeRef.current = state.visibleRange;\n  });\n\n  const [navigationMotion, setNavigationMotion] =\n    useCalendarNavigationMotion(state);\n  const [pendingFocus, setPendingFocus] = useState<CalendarDate[]>([]);\n\n  // Moving the visible range to a month that overlaps it takes two focus moves:\n  // the first renders a realigned range, the second lands on the right month.\n  useLayoutEffect(() => {\n    const [next, ...rest] = pendingFocus;\n\n    if (next === undefined) return;\n\n    state.setFocusedDate(next);\n    setPendingFocus(rest);\n  }, [pendingFocus]);\n\n  // Shows `month` at position `index` of the visible range, then focuses\n  // `focus` when given.\n  const showMonth = (\n    index: number,\n    month: CalendarDate,\n    focus?: CalendarDate,\n  ) => {\n    const [first, ...rest] = planVisibleRangeShift(\n      state.visibleRange,\n      visibleMonths,\n      month.subtract({ months: index }),\n    );\n\n    if (first === undefined) {\n      if (focus) state.setFocusedDate(focus);\n\n      return;\n    }\n\n    state.setFocusedDate(first);\n    setPendingFocus(focus ? [...rest, focus] : rest);\n  };\n\n  // A shortcut selects its range and shows the month the range starts in, at\n  // the first position, with the start date focused. It also ends a range the\n  // user has begun, so the days no longer follow the pointer from its anchor.\n  const selectShortcut = (range: CalendarRangeShortcut[\"value\"]) => {\n    if (!isRangeCalendarState(state)) return;\n\n    const start = fromNativeDate(range.from);\n\n    state.setAnchorDate(null);\n    state.setValue({ end: fromNativeDate(range.to), start });\n    showMonth(0, start, start);\n  };\n\n  const isDateAllowed = (date: CalendarDate) =>\n    !state.isInvalid(date) && !state.isCellUnavailable(date);\n\n  const shortcutRow = shortcuts?.length ? (\n    <div\n      aria-label=\"Shortcuts\"\n      className=\"sp-calendar-shortcuts\"\n      data-slot=\"calendar-shortcuts\"\n      role=\"group\"\n    >\n      {shortcuts.map((shortcut) => (\n        <Button\n          disabled={\n            isDisabled ||\n            state.isReadOnly ||\n            !isDateAllowed(fromNativeDate(shortcut.value.from)) ||\n            !isDateAllowed(fromNativeDate(shortcut.value.to))\n          }\n          key={shortcut.label}\n          onClick={() => selectShortcut(shortcut.value)}\n          profile=\"inline\"\n          profileTone=\"accent\"\n          variant=\"link\"\n        >\n          {shortcut.label}\n        </Button>\n      ))}\n    </div>\n  ) : null;\n\n  const renderMonth = (index: number) => {\n    // Each month of a multi-month calendar picks its own month and year. The\n    // picker sees that month as its focused date, and what it selects moves the\n    // visible range so that the month shows at this position.\n    const pickerState =\n      visibleMonths > 1\n        ? {\n            ...state,\n            focusedDate: state.visibleRange.start.add({ months: index }),\n            setFocusedDate: (date: CalendarDate) => showMonth(index, date),\n          }\n        : state;\n\n    return (\n      <>\n        <div className=\"sp-calendar-header\" data-slot=\"calendar-header\">\n          {index === 0 ? (\n            <Button\n              data-slot=\"calendar-previous\"\n              render={<CalendarButtonPrimitive slot=\"previous\" />}\n              shape=\"square\"\n              variant=\"ghost\"\n            >\n              <ChevronLeftIcon aria-hidden=\"true\" />\n            </Button>\n          ) : null}\n          <CalendarMonthHeading\n            index={index}\n            isDisabled={isDisabled}\n            locale={locale}\n            navigationMotion={navigationMotion}\n            pickerState={pickerState}\n            size={size}\n            state={state}\n          />\n          {index === visibleMonths - 1 ? (\n            <Button\n              data-slot=\"calendar-next\"\n              render={<CalendarButtonPrimitive slot=\"next\" />}\n              shape=\"square\"\n              variant=\"ghost\"\n            >\n              <ChevronRightIcon aria-hidden=\"true\" />\n            </Button>\n          ) : null}\n        </div>\n        {visibleMonths < 2 ? shortcutRow : null}\n        <div className=\"sp-calendar-grid-frame\" data-slot=\"calendar-grid-frame\">\n          {navigationMotion ? (\n            <div\n              aria-hidden=\"true\"\n              data-direction={navigationMotion.direction}\n              data-motion-state=\"exit\"\n              className=\"sp-calendar-month-motion\"\n              data-slot=\"calendar-month-motion\"\n              inert\n              key={`grid-exit-${navigationMotion.key}`}\n              onAnimationEnd={(event) => {\n                if (event.currentTarget !== event.target) {\n                  return;\n                }\n\n                setNavigationMotion((current) =>\n                  current?.key === navigationMotion.key ? null : current,\n                );\n              }}\n            >\n              <CalendarGrid\n                offset={\n                  index + (navigationMotion.direction === \"next\" ? -1 : 1)\n                }\n                todayDate={todayDate}\n              />\n            </div>\n          ) : null}\n          <div\n            data-direction={navigationMotion?.direction}\n            data-motion-state={navigationMotion ? \"enter\" : undefined}\n            className=\"sp-calendar-month-motion\"\n            data-slot=\"calendar-month-motion\"\n            key={`grid-enter-${navigationMotion?.key ?? 0}`}\n          >\n            <CalendarGrid offset={index} todayDate={todayDate} />\n          </div>\n        </div>\n      </>\n    );\n  };\n\n  if (visibleMonths < 2) {\n    return renderMonth(0);\n  }\n\n  // Consecutive months share one range selection and one Previous and Next\n  // pair; each month has its own heading and month picker. The shortcuts have\n  // no single heading to sit under, so they show once above the months.\n  return (\n    <>\n      {shortcutRow}\n      <div className=\"sp-calendar-months\" data-slot=\"calendar-months\">\n        {Array.from({ length: visibleMonths }, (_, index) => (\n          <div\n            className=\"sp-calendar-month\"\n            data-slot=\"calendar-month\"\n            key={index}\n          >\n            {renderMonth(index)}\n          </div>\n        ))}\n      </div>\n    </>\n  );\n}\n\nfunction useCalendarNavigationMotion<M extends PointCalendarSelectionMode>(\n  state: CalendarState<M> | RangeCalendarState,\n) {\n  const [navigationMotion, setNavigationMotion] =\n    useState<CalendarNavigationMotion | null>(null);\n  const previousStartRef = useRef(state.visibleRange.start);\n  const motionKeyRef = useRef(0);\n  const currentStart = state.visibleRange.start;\n  const currentMonthKey = `${currentStart.calendar.identifier}:${currentStart.era}:${currentStart.year}:${currentStart.month}`;\n\n  useLayoutEffect(() => {\n    const previousStart = previousStartRef.current;\n    const isNext =\n      currentStart.add({ months: -1 }).compare(previousStart) === 0;\n    const isPrevious =\n      currentStart.add({ months: 1 }).compare(previousStart) === 0;\n\n    if (isNext || isPrevious) {\n      motionKeyRef.current += 1;\n      setNavigationMotion({\n        direction: isNext ? \"next\" : \"previous\",\n        key: motionKeyRef.current,\n        previousStart,\n      });\n    } else {\n      setNavigationMotion(null);\n    }\n\n    previousStartRef.current = currentStart;\n  }, [currentMonthKey]);\n\n  return [navigationMotion, setNavigationMotion] as const;\n}\n\n/** The months a calendar shows, from the first day to the last. */\ntype VisibleRange = { end: CalendarDate; start: CalendarDate };\n\n/** Whether the month of `date` is one of the months on screen. */\nfunction isMonthInRange(date: CalendarDate, visible: VisibleRange): boolean {\n  const month = startOfMonth(date);\n\n  return (\n    month.compare(startOfMonth(visible.start)) >= 0 &&\n    month.compare(visible.end) <= 0\n  );\n}\n\n/**\n * The dates to focus, one after the other, so that `months` consecutive months\n * start at `target`. React Aria realigns the visible range only when the focused\n * date leaves it: a later date starts the new range, an earlier date ends it. A\n * target that overlaps the visible range needs a hop out of it first. No dates\n * mean the range already starts there.\n */\nexport function planVisibleRangeShift(\n  visible: VisibleRange,\n  months: number,\n  target: CalendarDate,\n): CalendarDate[] {\n  const first = startOfMonth(target);\n\n  if (first.compare(startOfMonth(visible.start)) === 0) return [];\n\n  const last = first.add({ months: months - 1 });\n\n  // Entirely after the visible range: its first day starts the new range.\n  if (first.compare(visible.end) > 0) return [first];\n  // Entirely before it: a day of its last month ends the new range.\n  if (last.compare(visible.start) < 0) return [last];\n\n  // Overlapping: hop beyond the new range, then back to its last month (a later\n  // start) or to its first month (an earlier start).\n  return first.compare(visible.start) > 0\n    ? [first.add({ months }), last]\n    : [first.subtract({ months: 1 }), first];\n}\n\nfunction CalendarMonthHeading<M extends PointCalendarSelectionMode>({\n  index,\n  isDisabled,\n  locale,\n  navigationMotion,\n  pickerState,\n  size,\n  state,\n}: {\n  index: number;\n  isDisabled: boolean;\n  locale: string;\n  navigationMotion: CalendarNavigationMotion | null;\n  pickerState: CalendarState<M> | RangeCalendarState;\n  size: SiteplaneControlSize;\n  state: CalendarState<M> | RangeCalendarState;\n}) {\n  const [isPickerOpen, setIsPickerOpen] = useState(false);\n  const [pickerView, setPickerView] = useState<PickerView>(\"month\");\n  const pickerPortalContainerRef = useRef<HTMLElement | null>(null);\n  const heading = formatHeading(\n    state.visibleRange.start.add({ months: index }),\n    state,\n    locale,\n  );\n\n  return (\n    <Popover\n      onOpenChange={(open) => {\n        setIsPickerOpen(open);\n        if (open) setPickerView(\"month\");\n      }}\n      open={isPickerOpen}\n    >\n      <h2 className=\"sp-calendar-heading\" data-slot=\"calendar-heading\">\n        <PopoverTrigger\n          data-slot=\"calendar-heading-trigger\"\n          ref={(trigger) => {\n            pickerPortalContainerRef.current =\n              trigger?.closest<HTMLElement>(\"[data-rac][data-trigger]\") ?? null;\n          }}\n          render={\n            <Button\n              aria-label={`Choose month and year, ${heading}`}\n              disabled={isDisabled}\n              variant=\"ghost\"\n            />\n          }\n        >\n          <span\n            className=\"sp-calendar-heading-motion-frame\"\n            data-slot=\"calendar-heading-motion-frame\"\n          >\n            {navigationMotion ? (\n              <span\n                aria-hidden=\"true\"\n                data-direction={navigationMotion.direction}\n                data-motion-state=\"exit\"\n                className=\"sp-calendar-heading-motion\"\n                data-slot=\"calendar-heading-motion\"\n                key={`heading-exit-${navigationMotion.key}`}\n              >\n                {formatHeading(\n                  navigationMotion.previousStart.add({ months: index }),\n                  state,\n                  locale,\n                )}\n              </span>\n            ) : null}\n            <span\n              data-direction={navigationMotion?.direction}\n              data-motion-state={navigationMotion ? \"enter\" : undefined}\n              className=\"sp-calendar-heading-motion\"\n              data-slot=\"calendar-heading-motion\"\n              key={`heading-enter-${navigationMotion?.key ?? 0}`}\n            >\n              {heading}\n            </span>\n          </span>\n          <ChevronsUpDownIcon aria-hidden=\"true\" />\n        </PopoverTrigger>\n      </h2>\n      <PopoverPopup\n        align=\"center\"\n        className=\"sp-calendar-picker-popup\"\n        data-calendar-size={size}\n        portalProps={{ container: pickerPortalContainerRef }}\n        viewportPadding=\"compact\"\n      >\n        <CalendarPicker\n          isDisabled={isDisabled}\n          onClose={() => setIsPickerOpen(false)}\n          onViewChange={setPickerView}\n          state={pickerState}\n          view={pickerView}\n        />\n      </PopoverPopup>\n    </Popover>\n  );\n}\n\nfunction CalendarGrid({\n  offset = 0,\n  todayDate,\n}: {\n  offset?: number;\n  todayDate?: CalendarDate | undefined;\n}) {\n  return (\n    <CalendarGridPrimitive\n      className=\"sp-calendar-grid\"\n      data-slot=\"calendar-grid\"\n      {...(offset === 0 ? {} : { offset: { months: offset } })}\n      weekdayStyle=\"short\"\n    >\n      <CalendarGridHeaderPrimitive\n        className=\"sp-calendar-grid-header\"\n        data-slot=\"calendar-grid-header\"\n      >\n        {(day) => (\n          <CalendarHeaderCellPrimitive\n            className=\"sp-calendar-header-cell\"\n            data-slot=\"calendar-header-cell\"\n          >\n            {day}\n          </CalendarHeaderCellPrimitive>\n        )}\n      </CalendarGridHeaderPrimitive>\n      <CalendarGridBodyPrimitive\n        className=\"sp-calendar-grid-body\"\n        data-slot=\"calendar-grid-body\"\n      >\n        {(date) => (\n          <CalendarCellPrimitive\n            className=\"sp-calendar-cell\"\n            data-slot=\"calendar-cell\"\n            date={date}\n            // React Aria marks the device's today with `data-today`, which\n            // calendar.css draws. With `todayDate` the cell renders its own\n            // element, so the mark moves to that day.\n            {...(todayDate === undefined\n              ? {}\n              : {\n                  render: (cellProps) => (\n                    <div\n                      {...cellProps}\n                      data-today={isSameDay(date, todayDate) || undefined}\n                    />\n                  ),\n                })}\n          />\n        )}\n      </CalendarGridBodyPrimitive>\n    </CalendarGridPrimitive>\n  );\n}\n\nfunction CalendarPicker<M extends PointCalendarSelectionMode>({\n  isDisabled,\n  onClose,\n  onViewChange,\n  state,\n  view,\n}: {\n  isDisabled: boolean;\n  onClose: () => void;\n  onViewChange: (view: PickerView) => void;\n  state: CalendarState<M> | RangeCalendarState;\n  view: Exclude<PickerView, null>;\n}) {\n  const [pageMotion, setPageMotion] = useState<CalendarPickerPageMotion | null>(\n    null,\n  );\n  const [headingMotion, setHeadingMotion] =\n    useState<CalendarPickerHeadingMotion | null>(null);\n  const [pickerDate, setPickerDate] = useState(state.focusedDate);\n  const currentPageRef = useRef<CalendarPickerPage | null>(null);\n  const isMoving = pageMotion !== null || headingMotion !== null;\n  const startPageMotion = (\n    direction: CalendarPickerMotionDirection,\n    action: () => void,\n  ) => {\n    if (isMoving || !currentPageRef.current) return;\n\n    setPageMotion({ direction, previousPage: currentPageRef.current });\n    action();\n  };\n  const changePage = (direction: -1 | 1) => {\n    const motionDirection = direction === 1 ? \"next\" : \"previous\";\n\n    if (view === \"month\") {\n      if (isMoving || !currentPageRef.current) return;\n\n      setHeadingMotion({\n        direction: motionDirection,\n        previousHeading: currentPageRef.current.heading,\n      });\n      setPickerDate((current) => current.add({ years: direction }));\n      return;\n    }\n\n    startPageMotion(motionDirection, () => {\n      setPickerDate((current) => current.add({ years: 12 * direction }));\n    });\n  };\n  const changeView = (nextView: PickerView) =>\n    startPageMotion(nextView === \"year\" ? \"drill-in\" : \"drill-out\", () => {\n      onViewChange(nextView);\n    });\n  const renderCurrentPage = (\n    page: CalendarPickerPage,\n    onSelect: (item: CalendarPickerItem) => void,\n  ) => {\n    currentPageRef.current = page;\n\n    return (\n      <div\n        data-direction={pageMotion?.direction}\n        data-motion-state={pageMotion ? \"enter\" : undefined}\n        className=\"sp-calendar-picker-page\"\n        data-slot=\"calendar-picker-page\"\n      >\n        <CalendarPickerPageContent\n          headingMotion={page.view === \"month\" ? headingMotion : null}\n          isDisabled={isDisabled}\n          onHeadingMotionEnd={() => setHeadingMotion(null)}\n          onNext={() => changePage(1)}\n          onPrevious={() => changePage(-1)}\n          onSelect={onSelect}\n          onToggle={() => changeView(page.view === \"month\" ? \"year\" : \"month\")}\n          page={page}\n        />\n      </div>\n    );\n  };\n\n  const picker = (\n    <div\n      aria-label=\"Choose month and year\"\n      data-motion-active={isMoving || undefined}\n      className=\"sp-calendar-picker\"\n      data-slot=\"calendar-picker\"\n      data-view={view}\n      role=\"group\"\n    >\n      {pageMotion ? (\n        <div\n          aria-hidden=\"true\"\n          data-direction={pageMotion.direction}\n          data-motion-state=\"exit\"\n          className=\"sp-calendar-picker-page\"\n          data-slot=\"calendar-picker-page\"\n          inert\n          onAnimationEnd={(event) => {\n            if (event.currentTarget === event.target) setPageMotion(null);\n          }}\n        >\n          <CalendarPickerPageContent\n            isDisabled={false}\n            onHeadingMotionEnd={() => undefined}\n            onNext={() => undefined}\n            onPrevious={() => undefined}\n            onSelect={() => undefined}\n            onToggle={() => undefined}\n            page={pageMotion.previousPage}\n          />\n        </div>\n      ) : null}\n      {view === \"month\" ? (\n        <CalendarMonthPickerPrimitive format=\"short\">\n          {({ \"aria-label\": ariaLabel, items, onChange }) =>\n            renderCurrentPage(\n              {\n                ariaLabel,\n                heading: pickerDate.year,\n                items: items.map((item) => ({\n                  date: item.date,\n                  formatted: item.formatted,\n                  id: item.id,\n                  isSelected: isSameMonth(item.date, state.focusedDate),\n                })),\n                view: \"month\",\n              },\n              (item) => {\n                onChange(item.id);\n                state.setFocusedDate(item.date);\n                onClose();\n              },\n            )\n          }\n        </CalendarMonthPickerPrimitive>\n      ) : (\n        <CalendarYearPickerPrimitive visibleYears={12}>\n          {({ \"aria-label\": ariaLabel, items, onChange }) =>\n            renderCurrentPage(\n              {\n                ariaLabel,\n                heading: `${items.at(0)?.formatted} – ${items.at(-1)?.formatted}`,\n                items: items.map((item) => ({\n                  date: item.date,\n                  formatted: item.formatted,\n                  id: item.id,\n                  isSelected: isSameYear(item.date, state.focusedDate),\n                })),\n                view: \"year\",\n              },\n              (item) =>\n                startPageMotion(\"drill-out\", () => {\n                  onChange(item.id);\n                  state.setFocusedDate(item.date);\n                  onViewChange(\"month\");\n                }),\n            )\n          }\n        </CalendarYearPickerPrimitive>\n      )}\n    </div>\n  );\n\n  return \"selectionMode\" in state ? (\n    <CalendarStateContext.Provider\n      value={{\n        ...state,\n        focusedDate: pickerDate,\n        setFocusedDate: setPickerDate,\n      }}\n    >\n      {picker}\n    </CalendarStateContext.Provider>\n  ) : (\n    <RangeCalendarStateContext.Provider\n      value={{\n        ...state,\n        focusedDate: pickerDate,\n        setFocusedDate: setPickerDate,\n      }}\n    >\n      {picker}\n    </RangeCalendarStateContext.Provider>\n  );\n}\n\nfunction CalendarPickerPageContent({\n  headingMotion,\n  isDisabled,\n  onHeadingMotionEnd,\n  onNext,\n  onPrevious,\n  onSelect,\n  onToggle,\n  page,\n}: {\n  headingMotion?: CalendarPickerHeadingMotion | null;\n  isDisabled: boolean;\n  onHeadingMotionEnd: () => void;\n  onNext: () => void;\n  onPrevious: () => void;\n  onSelect: (item: CalendarPickerItem) => void;\n  onToggle: () => void;\n  page: CalendarPickerPage;\n}) {\n  const heading = headingMotion ? (\n    <span\n      className=\"sp-calendar-picker-heading-motion-frame\"\n      data-slot=\"calendar-picker-heading-motion-frame\"\n    >\n      <span\n        aria-hidden=\"true\"\n        data-direction={headingMotion.direction}\n        data-motion-state=\"exit\"\n        className=\"sp-calendar-picker-heading-motion\"\n        data-slot=\"calendar-picker-heading-motion\"\n        onAnimationEnd={(event) => {\n          if (event.currentTarget === event.target) onHeadingMotionEnd();\n        }}\n      >\n        {headingMotion.previousHeading}\n      </span>\n      <span\n        data-direction={headingMotion.direction}\n        data-motion-state=\"enter\"\n        className=\"sp-calendar-picker-heading-motion\"\n        data-slot=\"calendar-picker-heading-motion\"\n      >\n        {page.heading}\n      </span>\n    </span>\n  ) : (\n    page.heading\n  );\n\n  return (\n    <>\n      <CalendarPickerHeader\n        heading={heading}\n        isDisabled={isDisabled}\n        onNext={onNext}\n        onPrevious={onPrevious}\n        onToggle={onToggle}\n        view={page.view}\n      />\n      <div\n        aria-label={page.ariaLabel}\n        className=\"sp-calendar-picker-grid\"\n        data-slot=\"calendar-picker-grid\"\n        role=\"group\"\n      >\n        {page.items.map((item) => (\n          <Button\n            aria-pressed={item.isSelected}\n            data-selected={item.isSelected || undefined}\n            data-slot=\"calendar-picker-option\"\n            disabled={isDisabled}\n            key={item.id}\n            onClick={() => onSelect(item)}\n            variant={item.isSelected ? \"inverse\" : \"ghost\"}\n          >\n            {item.formatted}\n          </Button>\n        ))}\n      </div>\n    </>\n  );\n}\n\nfunction CalendarPickerHeader({\n  heading,\n  isDisabled,\n  onNext,\n  onPrevious,\n  onToggle,\n  view,\n}: {\n  heading: React.ReactNode;\n  isDisabled: boolean;\n  onNext: () => void;\n  onPrevious: () => void;\n  onToggle: () => void;\n  view: Exclude<PickerView, null>;\n}) {\n  return (\n    <div\n      className=\"sp-calendar-picker-header\"\n      data-slot=\"calendar-picker-header\"\n    >\n      <Button\n        aria-label={view === \"month\" ? \"Previous year\" : \"Previous years\"}\n        data-slot=\"calendar-picker-previous\"\n        disabled={isDisabled}\n        onClick={onPrevious}\n        variant=\"ghost\"\n      >\n        <ChevronLeftIcon aria-hidden=\"true\" />\n      </Button>\n      <Button\n        data-slot=\"calendar-picker-heading\"\n        disabled={isDisabled}\n        onClick={onToggle}\n        variant=\"ghost\"\n      >\n        {heading}\n        <ChevronsUpDownIcon aria-hidden=\"true\" />\n      </Button>\n      <Button\n        aria-label={view === \"month\" ? \"Next year\" : \"Next years\"}\n        data-slot=\"calendar-picker-next\"\n        disabled={isDisabled}\n        onClick={onNext}\n        variant=\"ghost\"\n      >\n        <ChevronRightIcon aria-hidden=\"true\" />\n      </Button>\n    </div>\n  );\n}\n\nfunction formatHeading<M extends PointCalendarSelectionMode>(\n  date: CalendarState<M>[\"visibleRange\"][\"start\"],\n  state: CalendarState<M> | RangeCalendarState,\n  locale: string,\n) {\n  const first = date.calendar.getFormattableMonth\n    ? date.calendar.getFormattableMonth(date)\n    : date;\n\n  return normalizeFormattedText(\n    new Intl.DateTimeFormat(locale, {\n      calendar: first.calendar.identifier,\n      month: \"long\",\n      timeZone: state.timeZone,\n      year: \"numeric\",\n    }).format(first.toDate(state.timeZone)),\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/calendar/calendar.tsx"},{"path":"packages/ui/src/components/calendar/calendar.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  .sp-calendar {\n    --calendar-cell-size: 2.25rem;\n\n    display: grid;\n    position: relative;\n    width: max-content;\n    color: var(--sp-rw-fg-primary);\n    font-size: var(--sp-rw-font-14);\n    line-height: var(--sp-rw-line-21);\n  }\n\n  .sp-calendar[data-size=\"xs\"] {\n    --calendar-cell-size: 1.75rem;\n  }\n\n  .sp-calendar[data-size=\"sm\"] {\n    --calendar-cell-size: 2rem;\n  }\n\n  .sp-calendar[data-size=\"lg\"] {\n    --calendar-cell-size: 2.5rem;\n  }\n\n  .sp-calendar[data-size=\"xl\"] {\n    --calendar-cell-size: 3rem;\n  }\n\n  .sp-calendar-header {\n    display: grid;\n    grid-template-columns: var(--calendar-cell-size) 1fr var(\n        --calendar-cell-size\n      );\n    align-items: center;\n    margin-bottom: var(--sp-rw-space-2);\n  }\n\n  /* Quick choices between the heading and the days: accent links spread over the\n   * width of the grid, with the grid's own spacing below. The row takes the width\n   * the days give the calendar and wraps within it, so long labels never widen the\n   * calendar. A calendar with several months shows the row once, centred above\n   * them. */\n  .sp-calendar-shortcuts {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n    gap: var(--sp-rw-space-1) var(--sp-rw-space-3);\n    margin-bottom: var(--sp-rw-space-2);\n    padding-inline: var(--sp-rw-space-2);\n    contain: inline-size;\n  }\n\n  .sp-calendar[data-visible-months] .sp-calendar-shortcuts {\n    justify-content: center;\n    column-gap: var(--sp-rw-space-6);\n  }\n\n  .sp-calendar-previous,\n  .sp-calendar-next {\n    width: var(--calendar-cell-size);\n    height: var(--calendar-cell-size);\n  }\n\n  /* A header holds the Previous button only on the first month and the Next\n   * button only on the last, so each part names its own column. */\n  .sp-calendar-previous {\n    grid-column: 1;\n  }\n\n  .sp-calendar-next {\n    grid-column: 3;\n  }\n\n  :is(.sp-calendar-previous, .sp-calendar-next) svg {\n    width: var(--sp-rw-space-4);\n    height: var(--sp-rw-space-4);\n  }\n\n  .sp-calendar-heading {\n    grid-column: 2;\n    margin: 0;\n    color: var(--sp-rw-fg-primary);\n    font-size: var(--sp-rw-font-14);\n    font-weight: 560;\n    text-align: center;\n  }\n\n  .sp-calendar-heading-trigger {\n    min-height: var(--calendar-cell-size);\n    gap: var(--sp-rw-space-1);\n    padding: 0 var(--sp-rw-space-2);\n    color: inherit;\n    font: inherit;\n  }\n\n  /* The heading icons take the tertiary text colour; their 16 px size comes\n   * from the Button they sit in. */\n  .sp-calendar-heading-trigger svg,\n  .sp-calendar-picker-heading svg {\n    color: var(--sp-rw-fg-tertiary);\n  }\n\n  .sp-calendar-heading-motion-frame {\n    display: inline-grid;\n    overflow: hidden;\n  }\n\n  .sp-calendar-heading-motion {\n    grid-area: 1 / 1;\n    white-space: nowrap;\n    will-change: transform;\n  }\n\n  .sp-calendar-picker {\n    position: relative;\n    width: 100%;\n    min-height: calc(var(--calendar-cell-size) * 5 + var(--sp-rw-space-7));\n    overflow: hidden;\n  }\n\n  .sp-calendar-picker-page {\n    box-sizing: border-box;\n    padding: calc(\n      var(--sp-semantic-state-focus-width) +\n        var(--sp-semantic-state-focus-offset)\n    );\n  }\n\n  .sp-calendar-picker[data-motion-active] {\n    pointer-events: none;\n  }\n\n  .sp-calendar-picker-page[data-motion-state=\"exit\"] {\n    position: absolute;\n    z-index: 1;\n    inset: 0;\n  }\n\n  .sp-calendar-picker-heading-motion-frame {\n    display: inline-grid;\n    overflow: hidden;\n  }\n\n  .sp-calendar-picker-heading-motion {\n    grid-area: 1 / 1;\n  }\n\n  :is(\n    .sp-calendar-picker-page,\n    .sp-calendar-picker-heading-motion\n  )[data-motion-state] {\n    animation-duration: var(--motion-duration-slow);\n    animation-fill-mode: both;\n    animation-timing-function: var(--motion-ease-standard);\n  }\n\n  :is(\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state=\"enter\"][data-direction=\"next\"] {\n    animation-name: sp-calendar-slide-in-from-right;\n  }\n\n  :is(\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state=\"exit\"][data-direction=\"next\"] {\n    animation-name: sp-calendar-slide-out-to-left;\n  }\n\n  :is(\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state=\"enter\"][data-direction=\"previous\"] {\n    animation-name: sp-calendar-slide-in-from-left;\n  }\n\n  :is(\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state=\"exit\"][data-direction=\"previous\"] {\n    animation-name: sp-calendar-slide-out-to-right;\n  }\n\n  .sp-calendar-picker-page[data-motion-state=\"enter\"][data-direction=\"drill-in\"] {\n    animation-name: sp-calendar-picker-enter-from-top;\n  }\n\n  .sp-calendar-picker-page[data-motion-state=\"exit\"][data-direction=\"drill-in\"] {\n    animation-name: sp-calendar-picker-exit-to-bottom;\n  }\n\n  .sp-calendar-picker-page[data-motion-state=\"enter\"][data-direction=\"drill-out\"] {\n    animation-name: sp-calendar-picker-enter-from-bottom;\n  }\n\n  .sp-calendar-picker-page[data-motion-state=\"exit\"][data-direction=\"drill-out\"] {\n    animation-name: sp-calendar-picker-exit-to-top;\n  }\n\n  .sp-calendar-picker-popup {\n    --calendar-cell-size: 2.25rem;\n\n    width: calc(var(--calendar-cell-size) * 6);\n    border-radius: var(--sp-rw-radius-surface);\n  }\n\n  .sp-calendar-picker-popup[data-calendar-size=\"sm\"] {\n    --calendar-cell-size: 2rem;\n  }\n\n  .sp-calendar-picker-popup[data-calendar-size=\"lg\"] {\n    --calendar-cell-size: 2.5rem;\n  }\n\n  .sp-calendar-picker-popup[data-calendar-size=\"xl\"] {\n    --calendar-cell-size: 3rem;\n  }\n\n  .sp-calendar-picker-header {\n    display: grid;\n    grid-template-columns: var(--calendar-cell-size) 1fr var(\n        --calendar-cell-size\n      );\n    align-items: center;\n    margin-bottom: var(--sp-rw-space-2);\n  }\n\n  :root [data-chassis][data-size].sp-calendar-picker-previous,\n  :root [data-chassis][data-size].sp-calendar-picker-next {\n    width: var(--calendar-cell-size);\n    height: var(--calendar-cell-size);\n    min-height: var(--calendar-cell-size);\n    padding: 0;\n  }\n\n  :root [data-chassis][data-size].sp-calendar-picker-heading {\n    height: var(--calendar-cell-size);\n    min-height: var(--calendar-cell-size);\n    gap: var(--sp-rw-space-1);\n    justify-self: center;\n    padding: 0 var(--sp-rw-space-2);\n    font: inherit;\n    font-weight: 560;\n  }\n\n  .sp-calendar-picker-grid {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: var(--sp-rw-space-1);\n  }\n\n  :root [data-chassis][data-size].sp-calendar-picker-option {\n    width: 100%;\n    min-width: 0;\n    height: var(--calendar-cell-size);\n    min-height: var(--calendar-cell-size);\n    padding: 0 var(--sp-rw-space-1);\n    font: inherit;\n  }\n\n  :root [data-chassis][data-size].sp-calendar-picker-option[data-selected] {\n    --sp-rw-button-fill: var(--sp-rw-bg-accent);\n    --sp-rw-button-text: var(--sp-rw-fg-on-accent);\n    --sp-rw-button-depth: none;\n    --sp-rw-button-interaction: transparent;\n    --sp-rw-button-spotlights: none;\n    --sp-rw-button-dome: none;\n    --sp-rw-button-rim: none;\n    --sp-rw-button-micro: none;\n\n    background: var(--sp-rw-bg-accent);\n    color: var(--sp-rw-fg-on-accent);\n    box-shadow: none;\n  }\n\n  @keyframes sp-calendar-picker-enter-from-top {\n    from {\n      transform: translateY(-100%);\n    }\n\n    to {\n      transform: translateY(0);\n    }\n  }\n\n  @keyframes sp-calendar-picker-exit-to-bottom {\n    from {\n      transform: translateY(0);\n    }\n\n    to {\n      transform: translateY(100%);\n    }\n  }\n\n  @keyframes sp-calendar-picker-enter-from-bottom {\n    from {\n      transform: translateY(100%);\n    }\n\n    to {\n      transform: translateY(0);\n    }\n  }\n\n  @keyframes sp-calendar-picker-exit-to-top {\n    from {\n      transform: translateY(0);\n    }\n\n    to {\n      transform: translateY(-100%);\n    }\n  }\n\n  .sp-calendar-grid-frame {\n    position: relative;\n    overflow: hidden;\n  }\n\n  .sp-calendar-month-motion {\n    width: 100%;\n    will-change: transform;\n  }\n\n  .sp-calendar-month-motion[data-motion-state=\"exit\"] {\n    position: absolute;\n    z-index: 1;\n    inset: 0;\n    pointer-events: none;\n  }\n\n  :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion\n    )[data-motion-state=\"enter\"][data-direction=\"next\"] {\n    animation: sp-calendar-slide-in-from-right var(--motion-duration-slow)\n      var(--motion-ease-standard) both;\n  }\n\n  :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion\n    )[data-motion-state=\"exit\"][data-direction=\"next\"] {\n    animation: sp-calendar-slide-out-to-left var(--motion-duration-slow)\n      var(--motion-ease-standard) both;\n  }\n\n  :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion\n    )[data-motion-state=\"enter\"][data-direction=\"previous\"] {\n    animation: sp-calendar-slide-in-from-left var(--motion-duration-slow)\n      var(--motion-ease-standard) both;\n  }\n\n  :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion\n    )[data-motion-state=\"exit\"][data-direction=\"previous\"] {\n    animation: sp-calendar-slide-out-to-right var(--motion-duration-slow)\n      var(--motion-ease-standard) both;\n  }\n\n  .sp-calendar-grid {\n    border-collapse: collapse;\n    border-spacing: 0;\n  }\n\n  /* Consecutive months share one selection and one Previous and Next pair; each\n   * month has its own header. A day of the neighbouring month shows in that\n   * month's own grid, not twice. */\n  .sp-calendar-months {\n    display: flex;\n    gap: var(--sp-rw-space-6);\n  }\n\n  .sp-calendar-month {\n    display: grid;\n    align-content: start;\n  }\n\n  .sp-calendar[data-visible-months] .sp-calendar-cell[data-outside-month] {\n    visibility: hidden;\n  }\n\n  .sp-calendar-grid-body td {\n    padding-block: 1px;\n  }\n\n  .sp-calendar-header-cell {\n    width: var(--calendar-cell-size);\n    height: calc(var(--calendar-cell-size) - var(--sp-rw-space-1));\n    padding: 0;\n    color: var(--sp-rw-fg-tertiary);\n    font-size: var(--sp-rw-font-12);\n    font-weight: 480;\n    text-align: center;\n  }\n\n  /* One transition for every selection mode: a picked day fades to the accent, and\n   * in a range the start, the end and the days between them fade the same way, on\n   * the same tokens. Reduced motion shortens it for all of them below. */\n  .sp-calendar-cell {\n    display: grid;\n    width: var(--calendar-cell-size);\n    height: var(--calendar-cell-size);\n    place-items: center;\n    border-radius: var(--sp-rw-radius-control);\n    padding: 0;\n    outline: none;\n    color: var(--sp-rw-fg-primary);\n    cursor: pointer;\n    text-align: center;\n    transition:\n      background-color var(--motion-duration-fast) var(--motion-ease-navigation),\n      box-shadow var(--motion-duration-fast) var(--motion-ease-navigation),\n      color var(--motion-duration-fast) var(--motion-ease-navigation),\n      opacity var(--motion-duration-fast) var(--motion-ease-navigation);\n  }\n\n  .sp-calendar-cell[data-hovered]:not([data-selected]) {\n    background: var(--state-hover);\n  }\n\n  .sp-calendar-cell[data-pressed]:not([data-selected]) {\n    background: var(--state-pressed);\n  }\n\n  .sp-calendar-cell[data-selected] {\n    background: var(--sp-rw-bg-accent);\n    color: var(--sp-rw-fg-on-accent);\n    font-weight: 560;\n  }\n\n  .sp-calendar[data-selection-mode=\"range\"]\n    .sp-calendar-cell[data-selected]:not([data-selection-start]):not(\n      [data-selection-end]\n    ) {\n    background: var(--sp-rw-bg-accent-translucent);\n    color: var(--sp-rw-fg-accent);\n  }\n\n  .sp-calendar-grid-body\n    td:has(> .sp-calendar-cell[data-selected]):has(\n      + td > .sp-calendar-cell[data-selected]\n    )\n    > .sp-calendar-cell[data-selected] {\n    border-start-end-radius: 0;\n    border-end-end-radius: 0;\n  }\n\n  .sp-calendar-grid-body\n    td:has(> .sp-calendar-cell[data-selected])\n    + td:has(> .sp-calendar-cell[data-selected])\n    > .sp-calendar-cell[data-selected] {\n    border-start-start-radius: 0;\n    border-end-start-radius: 0;\n  }\n\n  .sp-calendar-grid-body\n    tr:has(> td:last-child > .sp-calendar-cell[data-selected]):has(\n      + tr > td:first-child > .sp-calendar-cell[data-selected]\n    )\n    > td:last-child\n    > .sp-calendar-cell[data-selected] {\n    border-start-end-radius: 0;\n    border-end-end-radius: 0;\n  }\n\n  .sp-calendar-grid-body\n    tr:has(> td:last-child > .sp-calendar-cell[data-selected])\n    + tr\n    > td:first-child\n    > .sp-calendar-cell[data-selected] {\n    border-start-start-radius: 0;\n    border-end-start-radius: 0;\n  }\n\n  .sp-calendar-cell[data-focus-visible] {\n    outline: var(--sp-semantic-state-focus-width) solid\n      var(--sp-semantic-color-focus);\n    outline-offset: calc(var(--sp-semantic-state-focus-width) * -1);\n  }\n\n  .sp-calendar-cell[data-today]:not([data-selected]) {\n    box-shadow: inset 0 0 0 var(--sp-rw-border-default)\n      var(--sp-rw-border-translucent);\n    font-weight: 560;\n  }\n\n  .sp-calendar-cell[data-outside-month] {\n    color: var(--sp-rw-fg-quaternary);\n  }\n\n  /* A day that cannot be chosen shows not-allowed. A disabled day and a day\n   * outside the visible range also fade; an unavailable day is struck through\n   * instead. */\n  .sp-calendar-cell:is(\n    [data-disabled],\n    [data-outside-visible-range],\n    [data-unavailable]\n  ) {\n    cursor: not-allowed;\n  }\n\n  .sp-calendar-cell:is([data-disabled], [data-outside-visible-range]) {\n    opacity: var(--sp-rw-disabled-opacity-calendar-day);\n  }\n\n  .sp-calendar-cell[data-unavailable] {\n    color: var(--sp-rw-fg-tertiary);\n    text-decoration: line-through;\n  }\n\n  .sp-calendar[data-invalid] .sp-calendar-cell[data-selected] {\n    background: var(--sp-rw-bg-destructive);\n  }\n\n  /* A disabled Calendar fades once; its days, disabled with it, do not fade a\n   * second time. */\n  .sp-calendar[data-disabled] {\n    opacity: var(--sp-rw-disabled-opacity-action);\n  }\n\n  .sp-calendar[data-disabled] .sp-calendar-cell {\n    opacity: 1;\n  }\n\n  [dir=\"rtl\"] :is(.sp-calendar-previous, .sp-calendar-next) svg {\n    transform: rotate(180deg);\n  }\n\n  @media (max-width: 639px), (pointer: coarse) {\n    .sp-calendar[data-size=\"md\"],\n    .sp-calendar-picker-popup[data-calendar-size=\"md\"] {\n      --calendar-cell-size: 2.5rem;\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-calendar-previous,\n    .sp-calendar-next,\n    .sp-calendar-heading-trigger,\n    .sp-calendar-picker-previous,\n    .sp-calendar-picker-heading,\n    .sp-calendar-picker-next,\n    .sp-calendar-picker-option,\n    .sp-calendar-cell {\n      transition-duration: 1ms;\n    }\n\n    :is(\n        .sp-calendar-heading-motion,\n        .sp-calendar-month-motion,\n        .sp-calendar-picker-page,\n        .sp-calendar-picker-heading-motion\n      )[data-motion-state][data-direction] {\n      animation-duration: 1ms;\n    }\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-calendar-previous,\n  html[data-motion-reduce=\"true\"] .sp-calendar-next,\n  html[data-motion-reduce=\"true\"] .sp-calendar-heading-trigger,\n  html[data-motion-reduce=\"true\"] .sp-calendar-picker-previous,\n  html[data-motion-reduce=\"true\"] .sp-calendar-picker-heading,\n  html[data-motion-reduce=\"true\"] .sp-calendar-picker-next,\n  html[data-motion-reduce=\"true\"] .sp-calendar-picker-option,\n  html[data-motion-reduce=\"true\"] .sp-calendar-cell,\n  [data-sp-motion=\"reduced\"] .sp-calendar-previous,\n  [data-sp-motion=\"reduced\"] .sp-calendar-next,\n  [data-sp-motion=\"reduced\"] .sp-calendar-heading-trigger,\n  [data-sp-motion=\"reduced\"] .sp-calendar-picker-previous,\n  [data-sp-motion=\"reduced\"] .sp-calendar-picker-heading,\n  [data-sp-motion=\"reduced\"] .sp-calendar-picker-next,\n  [data-sp-motion=\"reduced\"] .sp-calendar-picker-option,\n  [data-sp-motion=\"reduced\"] .sp-calendar-cell {\n    transition-duration: 1ms;\n  }\n\n  html[data-motion-reduce=\"true\"]\n    :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion,\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state][data-direction],\n  [data-sp-motion=\"reduced\"]\n    :is(\n      .sp-calendar-heading-motion,\n      .sp-calendar-month-motion,\n      .sp-calendar-picker-page,\n      .sp-calendar-picker-heading-motion\n    )[data-motion-state][data-direction] {\n    animation-duration: 1ms;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/calendar/calendar.css"},{"path":"packages/ui/src/components/calendar/index.ts","content":"export { Calendar } from \"./calendar\";\nexport type {\n  CalendarDayMatcher,\n  CalendarMultipleProps,\n  CalendarOptions,\n  CalendarProps,\n  CalendarRangeProps,\n  CalendarRangeShortcut,\n  CalendarSelectionMode,\n  CalendarSingleProps,\n  DateRange,\n} from \"./calendar\";\n","type":"registry:ui","target":"~/src/siteplane/components/calendar/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/calendar.md","categories":["Date & color"],"meta":{"avoidWhen":["Avoid when a date field or picker already owns the complete date-entry workflow."],"description":"Locale-aware inline calendar with single, contiguous range and multiple date selection.","knownLimitations":["The month heading button, its picker and the group of range shortcuts use English accessible labels in every locale.","Values are native Date objects at local midnight, like the date pickers'; only the day counts, so persist it deliberately, for example as YYYY-MM-DD."],"providerNotes":["Use the locale prop for this calendar, or SiteplaneLocaleProvider for a shared locale. SiteplaneUIProvider configures visual defaults, not locale."],"releaseStatus":"stable","searchTerms":["calendar","date","range","selection"],"useWhen":["Use when dates should be selected from an always-visible calendar grid."]}}