{
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "Calendar wraps React DayPicker for single-date, multiple-date, and date-range selection.",
  "registryDependencies": [
    "@siteplane/base",
    "@siteplane/popover"
  ],
  "dependencies": [
    "lucide-react@1.18.0",
    "react-day-picker@10.0.1"
  ],
  "files": [
    {
      "path": "src/components/ui/calendar.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronsUpDownIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  DayPicker,\n  useDayPicker,\n  type DateRange,\n  type Modifiers,\n  type RootProps,\n} from \"react-day-picker\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonClassNames =\n  \"relative flex size-(--cell-size) text-base sm:text-sm items-center justify-center rounded-lg cursor-pointer text-foreground not-in-data-selected:hover:bg-accent disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\";\n\nconst rangePreviewModifierClassNames = {\n  rangePreviewEnd: \"range-preview-end\",\n  rangePreviewMiddle: \"range-preview-middle\",\n  rangePreviewStart: \"range-preview-start\",\n} as const;\n\ntype RangePreview = {\n  from: Date;\n  to: Date;\n};\n\nfunction isDateRangeValue(value: unknown): value is DateRange {\n  return typeof value === \"object\" && value !== null && \"from\" in value;\n}\n\nfunction getCalendarDayTime(date: Date) {\n  return new Date(\n    date.getFullYear(),\n    date.getMonth(),\n    date.getDate(),\n  ).getTime();\n}\n\nfunction compareCalendarDays(first: Date, second: Date) {\n  return getCalendarDayTime(first) - getCalendarDayTime(second);\n}\n\nfunction isSameCalendarDay(first: Date, second: Date) {\n  return compareCalendarDays(first, second) === 0;\n}\n\nfunction isCalendarDayInRange(\n  date: Date,\n  range: RangePreview,\n  excludeEnds = false,\n) {\n  const day = getCalendarDayTime(date);\n  const from = getCalendarDayTime(range.from);\n  const to = getCalendarDayTime(range.to);\n\n  return excludeEnds ? day > from && day < to : day >= from && day <= to;\n}\n\nfunction getRangePreview(\n  selectedRange: DateRange | undefined,\n  previewDate: Date | undefined,\n): RangePreview | null {\n  if (!selectedRange?.from || selectedRange.to || !previewDate) {\n    return null;\n  }\n\n  const compare = compareCalendarDays(selectedRange.from, previewDate);\n\n  if (compare === 0) {\n    return null;\n  }\n\n  return compare < 0\n    ? { from: selectedRange.from, to: previewDate }\n    : { from: previewDate, to: selectedRange.from };\n}\n\nfunction shouldClearRangePreviewOnLeave(event: React.MouseEvent) {\n  const root = event.currentTarget.closest('[data-slot=\"calendar\"]');\n  const nextTarget = event.relatedTarget;\n\n  return (\n    !root ||\n    typeof Node === \"undefined\" ||\n    !(nextTarget instanceof Node) ||\n    !root.contains(nextTarget)\n  );\n}\n\nfunction getCalendarMotionMs(root: HTMLElement) {\n  const value = getComputedStyle(root)\n    .getPropertyValue(\"--motion-duration-slow\")\n    .trim();\n  const match = /^([\\d.]+)(ms|s)$/.exec(value);\n\n  if (!match) {\n    return 320;\n  }\n\n  const amount = Number(match[1]);\n\n  if (!Number.isFinite(amount)) {\n    return 320;\n  }\n\n  return match[2] === \"s\" ? amount * 1000 : amount;\n}\n\nfunction setCalendarRootRef(\n  rootRef: React.Ref<HTMLDivElement> | undefined,\n  node: HTMLDivElement | null,\n) {\n  if (!rootRef) {\n    return;\n  }\n\n  if (typeof rootRef === \"function\") {\n    rootRef(node);\n    return;\n  }\n\n  (rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n}\n\nfunction CalendarMotionRoot({ rootRef, ...props }: RootProps) {\n  const localRef = React.useRef<HTMLDivElement | null>(null);\n  const handleRootRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      localRef.current = node;\n      setCalendarRootRef(rootRef, node);\n    },\n    [rootRef],\n  );\n\n  React.useLayoutEffect(() => {\n    const root = localRef.current;\n    const content = root?.firstElementChild as HTMLElement | null;\n\n    if (!root || !content || !(\"ResizeObserver\" in window)) {\n      return;\n    }\n\n    let previousContentHeight = content.getBoundingClientRect().height;\n    let previousRootHeight = root.getBoundingClientRect().height;\n    let lastHeightMotionSignal = root.getAttribute(\"data-calendar-height-motion\");\n    let cleanupTimer: number | undefined;\n\n    const observer = new ResizeObserver((entries) => {\n      const nextContentHeight =\n        entries[0]?.borderBoxSize?.[0]?.blockSize ??\n        content.getBoundingClientRect().height;\n      const nextRootHeight = root.getBoundingClientRect().height;\n      const heightMotionSignal = root.getAttribute(\"data-calendar-height-motion\");\n      const shouldAnimateHeight = heightMotionSignal !== lastHeightMotionSignal;\n      const rootHeightChanged =\n        Math.abs(nextRootHeight - previousRootHeight) >= 1;\n      const shouldClipOverflow = nextRootHeight < previousRootHeight;\n\n      if (\n        Math.abs(nextContentHeight - previousContentHeight) < 1 ||\n        !rootHeightChanged ||\n        !shouldAnimateHeight ||\n        document.documentElement.dataset.motionReduce === \"true\"\n      ) {\n        window.clearTimeout(cleanupTimer);\n        cleanupTimer = undefined;\n        lastHeightMotionSignal = heightMotionSignal;\n        previousContentHeight = nextContentHeight;\n        previousRootHeight = nextRootHeight;\n        root.style.height = \"\";\n        root.style.overflow = \"\";\n        return;\n      }\n\n      lastHeightMotionSignal = heightMotionSignal;\n      window.clearTimeout(cleanupTimer);\n      root.style.height = `${previousRootHeight}px`;\n      root.style.overflow = shouldClipOverflow ? \"hidden\" : \"visible\";\n      root.getBoundingClientRect();\n      root.style.height = `${nextRootHeight}px`;\n\n      cleanupTimer = window.setTimeout(() => {\n        root.style.height = \"\";\n        root.style.overflow = \"\";\n      }, getCalendarMotionMs(root) + 40);\n\n      previousContentHeight = nextContentHeight;\n      previousRootHeight = nextRootHeight;\n    });\n\n    observer.observe(content);\n\n    return () => {\n      window.clearTimeout(cleanupTimer);\n      observer.disconnect();\n    };\n  }, []);\n\n  return <div {...props} ref={handleRootRef} />;\n}\n\ntype CalendarCaptionBridgeProps = React.HTMLAttributes<HTMLDivElement> & {\n  calendarMonth?: unknown;\n  displayIndex?: number;\n};\n\ntype CalendarNavigationDirection = \"next\" | \"previous\";\n\ntype CalendarTitleMotion = {\n  direction: CalendarNavigationDirection;\n  key: string;\n  previousMonth: Date;\n};\n\nfunction CalendarCaptionBridge({\n  calendarMonth: _calendarMonth,\n  displayIndex: _displayIndex,\n  onGoToMonth,\n  ...props\n}: CalendarCaptionBridgeProps & {\n  onGoToMonth: (goToMonth: (date: Date) => void) => void;\n}) {\n  const { goToMonth } = useDayPicker();\n  void _calendarMonth;\n  void _displayIndex;\n\n  React.useLayoutEffect(() => {\n    onGoToMonth(goToMonth);\n  }, [goToMonth, onGoToMonth]);\n\n  return <div {...props} />;\n}\n\nfunction getCalendarNavigationDirection(\n  previous: Date,\n  next: Date,\n): CalendarNavigationDirection | null {\n  const previousIndex = previous.getFullYear() * 12 + previous.getMonth();\n  const nextIndex = next.getFullYear() * 12 + next.getMonth();\n\n  if (previousIndex === nextIndex) {\n    return null;\n  }\n\n  return nextIndex > previousIndex ? \"next\" : \"previous\";\n}\n\nfunction isCalendarTitleMotionEnabled() {\n  return (\n    typeof document !== \"undefined\" &&\n    document.documentElement.dataset.motionReduce !== \"true\"\n  );\n}\n\n/**\n * Monatsbeschriftung mit ausdruecklicher Locale.\n *\n * Die Locale muss benannt werden, sonst nimmt `toLocaleString` die des Systems -\n * Server und Client sind sich dann uneinig (\"Sep\" vs. \"Sept\") und React meldet\n * einen Hydration-Mismatch. Fest verdrahtetes `en-US` vermied den Mismatch,\n * schrieb aber auch in einer deutschen Oberflaeche \"July 2026\". Der Code kommt\n * darum aus der uebergebenen react-day-picker-Locale: uebersetzt und trotzdem\n * auf beiden Seiten identisch.\n */\nfunction formatCalendarMonthTitle(date: Date, locale: string) {\n  return date.toLocaleString(locale, {\n    month: \"long\",\n    year: \"numeric\",\n  });\n}\n\nfunction resolveCalendarLocale(\n  locale: React.ComponentProps<typeof DayPicker>[\"locale\"],\n): string {\n  return locale?.code ?? \"en-US\";\n}\n\nexport function Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  components: userComponents,\n  modifiers: userModifiers,\n  modifiersClassNames: userModifiersClassNames,\n  mode = \"single\",\n  animate = true,\n  drilldown = true,\n  captionLayout,\n  month: monthProp,\n  defaultMonth,\n  onDayMouseEnter,\n  onDayMouseLeave,\n  onMonthChange,\n  startMonth,\n  endMonth,\n  ...props\n}: React.ComponentProps<typeof DayPicker> & {\n  drilldown?: boolean;\n}): React.ReactElement {\n  const [rangePreviewHover, setRangePreviewHover] = React.useState<{\n    anchorTime: number;\n    date: Date;\n  } | null>(null);\n  const selected = \"selected\" in props ? props.selected : undefined;\n  const selectedRange =\n    mode === \"range\" && isDateRangeValue(selected) ? selected : undefined;\n  const selectedRangeFromTime = selectedRange?.from\n    ? getCalendarDayTime(selectedRange.from)\n    : undefined;\n  const hasPendingRange = Boolean(\n    mode === \"range\" && selectedRange?.from && !selectedRange.to,\n  );\n  const rangePreviewDate =\n    hasPendingRange &&\n    rangePreviewHover !== null &&\n    rangePreviewHover.anchorTime === selectedRangeFromTime\n      ? rangePreviewHover.date\n      : undefined;\n  const rangePreview = getRangePreview(selectedRange, rangePreviewDate);\n\n  const handleDayMouseEnter = (\n    date: Date,\n    dayModifiers: Modifiers,\n    event: React.MouseEvent,\n  ) => {\n    if (hasPendingRange && !dayModifiers.disabled && !dayModifiers.hidden) {\n      setRangePreviewHover({\n        anchorTime: selectedRangeFromTime ?? getCalendarDayTime(date),\n        date,\n      });\n    }\n\n    onDayMouseEnter?.(date, dayModifiers, event);\n  };\n\n  const handleDayMouseLeave = (\n    date: Date,\n    dayModifiers: Modifiers,\n    event: React.MouseEvent,\n  ) => {\n    if (shouldClearRangePreviewOnLeave(event)) {\n      setRangePreviewHover(null);\n    }\n\n    onDayMouseLeave?.(date, dayModifiers, event);\n  };\n\n  const rangePreviewModifiers = rangePreview\n    ? {\n        rangePreviewEnd: (date: Date) => isSameCalendarDay(date, rangePreview.to),\n        rangePreviewMiddle: (date: Date) =>\n          isCalendarDayInRange(date, rangePreview, true),\n        rangePreviewStart: (date: Date) =>\n          isSameCalendarDay(date, rangePreview.from),\n      }\n    : undefined;\n\n  const defaultClassNames = {\n    button_next: buttonClassNames,\n    button_previous: buttonClassNames,\n    caption_label:\n      \"text-base sm:text-sm font-medium flex items-center gap-2 h-full\",\n    day: \"size-(--cell-size) text-sm py-px\",\n    day_button: cn(\n      buttonClassNames,\n      \"in-data-disabled:pointer-events-none in-[.range-middle]:rounded-none in-[.range-preview-middle]:rounded-none in-[.range-end:not(.range-start)]:rounded-s-none in-[.range-preview-end:not(.range-preview-start)]:rounded-s-none in-[.range-start:not(.range-end)]:rounded-e-none in-[.range-preview-start:not(.range-preview-end)]:rounded-e-none in-[.range-middle]:in-data-selected:bg-accent in-[.range-preview-middle]:!bg-foreground/6 in-data-selected:bg-primary in-[.range-preview-end:not([data-selected])]:!bg-foreground/10 in-[.range-preview-start:not([data-selected])]:!bg-foreground/10 in-data-selected:font-semibold in-[.range-preview-end]:font-semibold in-[.range-preview-start]:font-semibold in-[.range-middle]:in-data-selected:text-foreground in-[.range-preview-middle]:!text-foreground in-data-disabled:text-muted-foreground/72 in-data-outside:text-muted-foreground/72 in-data-selected:in-data-outside:text-primary-foreground in-data-selected:text-primary-foreground in-[.range-preview-end:not([data-selected])]:!text-foreground in-[.range-preview-start:not([data-selected])]:!text-foreground in-data-disabled:line-through outline-none transition-[color,background-color,border-radius,box-shadow] duration-(--motion-duration-fast) ease-(--motion-ease-apple-out) focus-visible:z-1 focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n    ),\n    dropdown: \"absolute bg-popover inset-0 cursor-pointer opacity-0\",\n    dropdown_root:\n      \"relative has-focus:border-ring has-focus:ring-ring/50 has-focus:ring-[3px] border border-input shadow-xs/5 rounded-lg px-[calc(--spacing(3)-1px)] h-9 sm:h-8 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-me-1\",\n    dropdowns:\n      \"w-full flex items-center text-base sm:text-sm justify-center h-(--cell-size) gap-1.5 *:[span]:font-medium\",\n    hidden: \"invisible\",\n    month: \"w-full\",\n    month_caption:\n      \"relative mx-(--cell-size) px-1 mb-1 flex h-(--cell-size) items-center justify-center z-2\",\n    months: \"relative flex flex-col sm:flex-row gap-2\",\n    nav: \"absolute top-0 flex w-full justify-between z-1\",\n    outside:\n      \"text-muted-foreground data-selected:bg-accent/50 data-selected:text-muted-foreground\",\n    range_end: \"range-end\",\n    range_middle: \"range-middle\",\n    range_start: \"range-start\",\n    today:\n      \"not-data-selected:*:font-semibold *:after:pointer-events-none *:after:absolute *:after:bottom-1 *:after:start-1/2 *:after:z-1 *:after:size-[3px] *:after:-translate-x-1/2 *:after:rounded-full *:after:bg-primary [&.range-preview-end:not([data-selected])>*]:after:bg-foreground/40 [&.range-preview-start:not([data-selected])>*]:after:bg-foreground/40 [&[data-selected]:not(.range-middle)>*]:after:bg-background [&[data-disabled]>*]:after:bg-foreground/30 *:after:transition-colors\",\n    week_number:\n      \"size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72\",\n    weekday:\n      \"size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72\",\n  };\n  const mergedClassNames: typeof defaultClassNames = Object.keys(\n    defaultClassNames,\n  ).reduce(\n    (acc, key) => {\n      const userClass = classNames?.[key as keyof typeof classNames];\n      const baseClass =\n        defaultClassNames[key as keyof typeof defaultClassNames];\n\n      acc[key as keyof typeof defaultClassNames] = userClass\n        ? cn(baseClass, userClass)\n        : baseClass;\n\n      return acc;\n    },\n    { ...defaultClassNames } as typeof defaultClassNames,\n  );\n\n  const defaultComponents = {\n    ...(animate ? { Root: CalendarMotionRoot } : {}),\n    Chevron: ({\n      className,\n      orientation,\n      ...props\n    }: {\n      className?: string;\n      orientation?: \"left\" | \"right\" | \"up\" | \"down\";\n    }): React.ReactElement => {\n      if (orientation === \"left\") {\n        return (\n          <ChevronLeftIcon\n            className={cn(className, \"rtl:rotate-180\")}\n            {...props}\n            aria-hidden=\"true\"\n          />\n        );\n      }\n\n      if (orientation === \"right\") {\n        return (\n          <ChevronRightIcon\n            className={cn(className, \"rtl:rotate-180\")}\n            {...props}\n            aria-hidden=\"true\"\n          />\n        );\n      }\n\n      return (\n        <ChevronsUpDownIcon\n          className={className}\n          {...props}\n          aria-hidden=\"true\"\n        />\n      );\n    },\n  };\n\n  const mergedComponents = {\n    ...defaultComponents,\n    ...userComponents,\n  };\n\n  const dayPickerProps = {\n    className: cn(\n      \"w-fit [--cell-size:--spacing(10)] sm:[--cell-size:--spacing(9)]\",\n      animate && \"siteplane-calendar-month-motion\",\n      mode === \"multiple\" && \"siteplane-calendar-multiple\",\n      className,\n    ),\n    classNames: mergedClassNames,\n    components: mergedComponents,\n    \"data-slot\": \"calendar\",\n    formatters: {\n      // Ausdrueckliche Locale: locale-abhaengiges \"default\" verursacht\n      // Hydration-Mismatch (z.B. Server \"Sep\" vs. Client \"Sept\").\n      formatMonthDropdown: (date: Date) =>\n        date.toLocaleString(resolveCalendarLocale(props.locale), {\n          month: \"short\",\n        }),\n    } as React.ComponentProps<typeof DayPicker>[\"formatters\"],\n    animate,\n    modifiers: rangePreviewModifiers\n      ? { ...(userModifiers ?? {}), ...rangePreviewModifiers }\n      : userModifiers,\n    modifiersClassNames: {\n      ...(userModifiersClassNames ?? {}),\n      ...rangePreviewModifierClassNames,\n    },\n    mode,\n    onDayMouseEnter: handleDayMouseEnter,\n    onDayMouseLeave: handleDayMouseLeave,\n    selected,\n    showOutsideDays,\n    ...props,\n  };\n\n  // Drilldown ist Default. Bei dropdown-Caption (eigene Navigation) oder explizit\n  // drilldown={false} faellt es auf die reduzierte Variante zurueck (nur aktueller\n  // Monat + Pfeil-Navigation) - z.B. fuer Terminbuchung in den naechsten Wochen.\n  const useDrilldown =\n    drilldown && (captionLayout === undefined || captionLayout === \"label\");\n\n  // Dropdown-Caption ohne explizite Range bekommt einen sinnvollen Jahres-Bereich\n  // (sonst hat das Jahr-Dropdown keine Auswahl), zentriert um den angezeigten Monat.\n  const isDropdown =\n    typeof captionLayout === \"string\" && captionLayout.startsWith(\"dropdown\");\n  const refYear = (monthProp ?? defaultMonth ?? new Date()).getFullYear();\n  const resolvedStartMonth =\n    startMonth ?? (isDropdown ? new Date(refYear - 10, 0, 1) : undefined);\n  const resolvedEndMonth =\n    endMonth ?? (isDropdown ? new Date(refYear + 10, 11, 1) : undefined);\n\n  if (!useDrilldown) {\n    return (\n      <DayPicker\n        {...(dayPickerProps as React.ComponentProps<typeof DayPicker>)}\n        {...(captionLayout === undefined ? {} : { captionLayout })}\n        {...(defaultMonth === undefined ? {} : { defaultMonth })}\n        {...(resolvedEndMonth === undefined ? {} : { endMonth: resolvedEndMonth })}\n        {...(monthProp === undefined ? {} : { month: monthProp })}\n        {...(onMonthChange === undefined ? {} : { onMonthChange })}\n        {...(resolvedStartMonth === undefined\n          ? {}\n          : { startMonth: resolvedStartMonth })}\n      />\n    );\n  }\n\n  return (\n    <CalendarDrilldown\n      dayPickerProps={dayPickerProps as React.ComponentProps<typeof DayPicker>}\n      {...(defaultMonth === undefined ? {} : { defaultMonth })}\n      {...(endMonth === undefined ? {} : { endMonth })}\n      {...(monthProp === undefined ? {} : { month: monthProp })}\n      {...(onMonthChange === undefined ? {} : { onMonthChange })}\n      {...(startMonth === undefined ? {} : { startMonth })}\n    />\n  );\n}\n\n// Drilldown-Navigation auf dem bestehenden Day-Grid: eigener Header mit Pfeilen\n// und einem Label, das ein Popover (nach oben) mit Monats- und Jahres-/Dekaden-\n// Auswahl oeffnet. Der Kalender bleibt sichtbar; die Hoehe animiert und das Popover\n// folgt im Lockstep. month wird controlled/uncontrolled gemanagt; alle uebrigen\n// DayPicker-Props (mode, selected, onSelect, ...) gehen unveraendert durch.\nfunction CalendarDrilldown({\n  dayPickerProps,\n  month: monthProp,\n  defaultMonth,\n  onMonthChange,\n  startMonth,\n  endMonth,\n}: {\n  dayPickerProps: React.ComponentProps<typeof DayPicker>;\n  month?: Date;\n  defaultMonth?: Date;\n  onMonthChange?: (month: Date) => void;\n  startMonth?: Date;\n  endMonth?: Date;\n}): React.ReactElement {\n  // react-day-picker navigiert intern (uncontrolled) - nur so animiert JEDER\n  // Monatswechsel (controlled month animiert nur den ersten). goToMonth wird per\n  // Bridge-Slot (useDayPicker) nach aussen gereicht; displayMonth spiegelt den\n  // aktuellen Monat fuer Header-Label + Picker.\n  const navRef = React.useRef<((date: Date) => void) | null>(null);\n  const [displayMonth, setDisplayMonth] = React.useState<Date>(\n    () => monthProp ?? defaultMonth ?? new Date(),\n  );\n  const [titleMotion, setTitleMotion] =\n    React.useState<CalendarTitleMotion | null>(null);\n  const [heightMotionSignal, setHeightMotionSignal] = React.useState(0);\n  const handleMonthChange = (next: Date) => {\n    setDisplayMonth((previous) => {\n      const direction = getCalendarNavigationDirection(previous, next);\n\n      if (dayPickerProps.animate && direction && isCalendarTitleMotionEnabled()) {\n        setHeightMotionSignal((signal) => signal + 1);\n        setTitleMotion({\n          direction,\n          key: `${previous.getFullYear()}-${previous.getMonth()}-${next.getFullYear()}-${next.getMonth()}`,\n          previousMonth: previous,\n        });\n      } else {\n        setTitleMotion(null);\n      }\n\n      return next;\n    });\n    onMonthChange?.(next);\n  };\n  const go = (target: Date) => navRef.current?.(target);\n\n  // Bridge im Caption-Slot: reicht react-day-pickers interne Navigation (goToMonth)\n  // nach aussen UND behaelt das (leere) Caption-Element samt data-animated-caption,\n  // damit dessen animationend das Animations-Cleanup ausloest. Ohne dieses Element\n  // animiert nur der erste Monatswechsel.\n  const handleGoToMonth = React.useCallback(\n    (goToMonth: (date: Date) => void) => {\n      navRef.current = goToMonth;\n    },\n    [],\n  );\n\n  const CaptionBridge = React.useCallback(\n    (props: CalendarCaptionBridgeProps) => (\n      <CalendarCaptionBridge {...props} onGoToMonth={handleGoToMonth} />\n    ),\n    [handleGoToMonth],\n  );\n\n  const [open, setOpen] = React.useState(false);\n  const [pickerView, setPickerView] = React.useState<\"months\" | \"years\">(\n    \"months\",\n  );\n\n  const year = displayMonth.getFullYear();\n  const monthIndex = displayMonth.getMonth();\n  const yearPageStart = Math.floor(year / 12) * 12;\n  const monthLocale = resolveCalendarLocale(dayPickerProps.locale);\n  const title = formatCalendarMonthTitle(displayMonth, monthLocale);\n  const monthNames = Array.from({ length: 12 }, (_, i) =>\n    new Date(2020, i, 1).toLocaleString(monthLocale, { month: \"short\" }),\n  );\n\n  const stepMonth = (delta: number) => go(new Date(year, monthIndex + delta, 1));\n  const stepPicker = (delta: number) =>\n    go(\n      new Date(\n        pickerView === \"months\" ? year + delta : year + delta * 12,\n        monthIndex,\n        1,\n      ),\n    );\n\n  const navButton =\n    \"flex size-(--cell-size) cursor-pointer items-center justify-center rounded-lg text-foreground transition-colors hover:bg-accent [&_svg]:size-4 [&_svg]:opacity-80\";\n  const pickerNavButton =\n    \"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-foreground transition-colors hover:bg-accent [&_svg]:size-4 [&_svg]:opacity-80\";\n  const gridButton =\n    \"flex cursor-pointer items-center justify-center rounded-lg py-1.5 text-sm transition-colors hover:bg-accent\";\n  const gridSelected =\n    \"bg-primary font-semibold text-primary-foreground hover:bg-primary\";\n  // Both panels share the same grid slot and swipe vertically: months leave\n  // downward while years enter from above, and vice versa.\n  const drillPanel =\n    \"siteplane-drill-panel col-start-1 row-start-1 grid grid-cols-3 gap-1 transition-[translate,opacity] [transition-duration:var(--motion-duration-calendar-drill)] [transition-timing-function:var(--motion-ease-apple-out)] motion-reduce:transition-none\";\n\n  return (\n    <div className=\"w-fit [--cell-size:--spacing(10)] [--motion-duration-slow:440ms] sm:[--cell-size:--spacing(9)]\">\n      <div className=\"mb-1 flex h-(--cell-size) items-center justify-between gap-1\">\n        <button\n          aria-label=\"Previous month\"\n          className={navButton}\n          onClick={() => stepMonth(-1)}\n          type=\"button\"\n        >\n          <ChevronLeftIcon />\n        </button>\n\n        <Popover\n          onOpenChange={(next) => {\n            setOpen(next);\n            if (next) {\n              setPickerView(\"months\");\n            }\n          }}\n          open={open}\n        >\n          <PopoverTrigger className=\"flex h-(--cell-size) cursor-pointer items-center gap-1 overflow-hidden rounded-lg px-2 font-medium text-base transition-colors hover:bg-accent sm:text-sm\">\n            <span className=\"grid min-w-0 overflow-hidden\">\n              {titleMotion ? (\n                <>\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"siteplane-calendar-title-motion col-start-1 row-start-1 whitespace-nowrap\"\n                    data-direction={titleMotion.direction}\n                    data-motion-state=\"exit\"\n                    key={`exit-${titleMotion.key}`}\n                    onAnimationEnd={(event) => {\n                      if (event.currentTarget !== event.target) {\n                        return;\n                      }\n\n                      setTitleMotion((current) =>\n                        current?.key === titleMotion.key ? null : current,\n                      );\n                    }}\n                  >\n                    {formatCalendarMonthTitle(\n                      titleMotion.previousMonth,\n                      monthLocale,\n                    )}\n                  </span>\n                  <span\n                    className=\"siteplane-calendar-title-motion col-start-1 row-start-1 whitespace-nowrap\"\n                    data-direction={titleMotion.direction}\n                    data-motion-state=\"enter\"\n                    key={`enter-${titleMotion.key}`}\n                  >\n                    {title}\n                  </span>\n                </>\n              ) : (\n                <span className=\"col-start-1 row-start-1 whitespace-nowrap\">\n                  {title}\n                </span>\n              )}\n            </span>\n            <ChevronsUpDownIcon className=\"size-3.5 opacity-60\" />\n          </PopoverTrigger>\n          <PopoverContent align=\"center\" side=\"top\">\n            <div className=\"w-48\">\n              <div className=\"mb-1 flex h-8 items-center justify-between gap-1\">\n                <button\n                  aria-label=\"Previous page\"\n                  className={pickerNavButton}\n                  onClick={() => stepPicker(-1)}\n                  type=\"button\"\n                >\n                  <ChevronLeftIcon />\n                </button>\n                <button\n                  className=\"flex h-8 cursor-pointer items-center gap-1 whitespace-nowrap rounded-lg px-2 font-medium text-sm transition-colors hover:bg-accent\"\n                  onClick={() =>\n                    setPickerView((v) => (v === \"months\" ? \"years\" : \"months\"))\n                  }\n                  type=\"button\"\n                >\n                  {pickerView === \"months\"\n                    ? String(year)\n                    : `${yearPageStart} – ${yearPageStart + 11}`}\n                  <ChevronsUpDownIcon className=\"size-3.5 opacity-60\" />\n                </button>\n                <button\n                  aria-label=\"Next page\"\n                  className={pickerNavButton}\n                  onClick={() => stepPicker(1)}\n                  type=\"button\"\n                >\n                  <ChevronRightIcon />\n                </button>\n              </div>\n\n              <div className=\"grid overflow-hidden\">\n                <div\n                  aria-hidden={pickerView !== \"months\"}\n                  className={cn(\n                    drillPanel,\n                    pickerView === \"months\"\n                      ? \"translate-y-0 opacity-100\"\n                      : \"pointer-events-none translate-y-full opacity-0\",\n                  )}\n                >\n                  {monthNames.map((name, i) => (\n                    <button\n                      className={cn(\n                        gridButton,\n                        i === monthIndex ? gridSelected : \"text-foreground\",\n                      )}\n                      key={name}\n                      onClick={() => {\n                        go(new Date(year, i, 1));\n                        setOpen(false);\n                      }}\n                      type=\"button\"\n                    >\n                      {name}\n                    </button>\n                  ))}\n                </div>\n                <div\n                  aria-hidden={pickerView !== \"years\"}\n                  className={cn(\n                    drillPanel,\n                    pickerView === \"years\"\n                      ? \"translate-y-0 opacity-100\"\n                      : \"pointer-events-none -translate-y-full opacity-0\",\n                  )}\n                >\n                  {Array.from({ length: 12 }, (_, i) => yearPageStart + i).map(\n                    (y) => (\n                      <button\n                        className={cn(\n                          gridButton,\n                          y === year ? gridSelected : \"text-foreground\",\n                        )}\n                        key={y}\n                        onClick={() => {\n                          go(new Date(y, monthIndex, 1));\n                          setPickerView(\"months\");\n                        }}\n                        type=\"button\"\n                      >\n                        {y}\n                      </button>\n                    ),\n                  )}\n                </div>\n              </div>\n            </div>\n          </PopoverContent>\n        </Popover>\n\n        <button\n          aria-label=\"Next month\"\n          className={navButton}\n          onClick={() => stepMonth(1)}\n          type=\"button\"\n        >\n          <ChevronRightIcon />\n        </button>\n      </div>\n\n      <DayPicker\n        {...dayPickerProps}\n        classNames={{\n          ...dayPickerProps.classNames,\n          // Hide the caption visually without display:none. Its animation must\n          // still finish so react-day-picker can run animationend cleanup.\n          month_caption: cn(\n            dayPickerProps.classNames?.month_caption,\n            \"mb-0 h-0 overflow-hidden\",\n          ),\n        }}\n        components={{ ...dayPickerProps.components, MonthCaption: CaptionBridge }}\n        data-calendar-height-motion={heightMotionSignal}\n        {...(monthProp ?? defaultMonth\n          ? { defaultMonth: monthProp ?? defaultMonth }\n          : {})}\n        {...(endMonth === undefined ? {} : { endMonth })}\n        hideNavigation\n        onMonthChange={handleMonthChange}\n        {...(startMonth === undefined ? {} : { startMonth })}\n      />\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "form-controls"
  ],
  "docs": "# Calendar\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nCalendar wraps React DayPicker for single-date, multiple-date, and date-range\nselection. Compose it with Popover and Button when a compact date field is\nneeded.\n\n## Import\n\n`import type { DateRange, DropdownProps } from \"react-day-picker\";`\n\n`import { Calendar } from \"@/components/ui/calendar\";`\n\n`import { Popover, PopoverPopup, PopoverTrigger } from \"@/components/ui/popover\";`\n\n## Public API\n\n- Use DayPicker `mode=\"single\"`, `\"multiple\"`, or `\"range\"` with the\n  corresponding selected-value type.\n- Outside days are visible by default; set `showOutsideDays={false}` to hide\n  adjacent-month days.\n- Use DayPicker `disabled` matchers for unavailable dates.\n- `captionLayout` supports `\"label\"`, `\"dropdown\"`,\n  `\"dropdown-months\"`, and `\"dropdown-years\"`. Bound dropdown years with\n  `startMonth` and `endMonth`.\n- `numberOfMonths` renders responsive multi-month calendars.\n- `month` with `onMonthChange` controls the visible month; use\n  `defaultMonth` for uncontrolled initial state.\n- Custom `modifiers` and `modifiersClassNames` are merged with the native\n  range-preview modifiers.\n- `classNames` is merged slot by slot with Siteplane defaults rather than\n  replacing the full DayPicker class map.\n\n## Public Motion\n\n- Month navigation and drilldown titles use central Calendar motion tokens.\n- Range hover preview changes modifiers without changing the committed\n  selection.\n- Popover date pickers use Popover motion; do not add local durations or\n  transforms.\n- Reduced motion is handled by the global motion contract.\n\n## Public Accessibility\n\n- Preserve React DayPicker grid roles, keyboard navigation, focus management,\n  and day labels.\n- Give the calendar a visible context or an accessible label.\n- Explain disabled date rules in nearby text when they are not self-evident.\n- A Popover date picker needs a clearly named trigger whose visible text reflects\n  the current value or placeholder.\n\n## Public Agent Guidance\n\n- Keep Calendar as the source of truth for date selection.\n- Use DayPicker props through the wrapper; do not rebuild its grid or navigation.\n- Preserve native classes, range-preview merging, focus behavior, and motion.\n- Use the canonical `PopoverPopup` name in new code.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/form-controls/calendar)\n\n- [Registry source](https://ui.siteplane.io/r/calendar.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/form-controls/calendar",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
