{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "An accessible React calendar for Next.js and Tailwind CSS: pick a day or a range, with full keyboard support, disabled days and Intl names. No date library.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "lucide-react@^1.52.0",
    "motion@^14.0.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json",
    "https://ui.fewtone.com/r/button.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/calendar.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { blur, duration, ease, spring, swap } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type AnimationPlaybackControls,\n  animate,\n  type MotionValue,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type ComponentProps,\n  type KeyboardEvent,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\n\n/*\n * Calendar (roadmap, 2026-10-16: \"A month view to pick dates from\"). Plain Date and Intl, no date library.\n * - A grid of day buttons, the WAI-ARIA date grid: arrow keys move by day, Up and Down by week, PageUp and\n *   PageDown by month (Shift: by year), Home and End to the week's ends. One day is in the tab order\n *   (roving tabindex), and moving past the month's edge turns the page. The month's name is a polite live\n *   region, today is aria-current=\"date\", and picked days are aria-selected.\n * - Props follow react-day-picker's main ones (mode, selected, onSelect, month, onMonthChange,\n *   numberOfMonths, disabled, weekStartsOn, showOutsideDays, fixedWeeks), so shadcn code moves over.\n * - Motion (.agents/rules/motion.md):\n *   - Turning the page, the new days come through the blur and slide 12px in from the side you're going to,\n *     like a tab panel, and the month's name swaps through `swap`. The filter is cleared afterwards.\n *   - single: the filled circle travels from the old day to the new one (Radio Group's shared mark). At rest\n *     each day draws its own circle; on a change one shared circle moves between their centres (offsets,\n *     so zoom doesn't double the trip) on spring.snappy, then hands back in the same frame.\n *   - range: picking the end, the band grows from the day picked first to the one picked last, row after\n *     row, on one eased tween (transform and opacity only). At rest each day draws its own piece. While the end\n *     is being chosen, a fainter band shows the range it would make, following the pointer and the keyboard.\n *   - Nothing animates on first paint. With reduced motion the page only fades and the rest is instant.\n * - Today is only known in the browser: a server render (or a cached page) has its own today. So the server\n *   and the first render mark no day as today, and a calendar with no month and no selection stays\n *   invisible until the browser says which month it is. Pass `today` or `defaultMonth` to avoid both.\n */\n\nexport type DateRange = { from: Date | undefined; to?: Date | undefined };\n\n/** Days to disable, as in react-day-picker: a day, days, a range, before or after a day, weekdays, or a test */\nexport type Matcher =\n  | boolean\n  | Date\n  | DateRange\n  | { before: Date }\n  | { after: Date }\n  | { before: Date; after: Date }\n  | { dayOfWeek: number | number[] }\n  | ((date: Date) => boolean);\n\nexport type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Words the calendar says. The dates themselves come from Intl, in `locale`. */\nexport type CalendarLabels = { previousMonth: string; nextMonth: string; selected: string };\n\nconst defaultLabels: CalendarLabels = {\n  previousMonth: \"Previous month\",\n  nextMonth: \"Next month\",\n  selected: \"selected\",\n};\n\n/** How far the new month's days slide in, in px (the same as a tab panel) */\nconst SLIDE = 12;\n/** The most weeks a month can touch */\nconst WEEKS = 6;\n/** How many days the keyboard looks past disabled ones for one it can land on: a year */\nconst SEARCH = 366;\n/** A month drawn, invisibly, while the browser hasn't said which month it is */\nconst PLACEHOLDER = new Date(2000, 0, 1);\n/** The inline styles turning the page leaves on a grid, cleared when it's done */\nconst LEFTOVERS = [\"opacity\", \"transform\", \"filter\"];\n\n// Days are local days. Every helper builds a new Date from its parts, so time zones and daylight saving\n// never shift a day.\nconst dayOf = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());\nconst monthOf = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1);\nconst addDays = (date: Date, days: number) =>\n  new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);\n/** The same day in another month, kept inside it (31 January + 1 month is the end of February) */\nfunction addMonths(date: Date, months: number) {\n  const first = new Date(date.getFullYear(), date.getMonth() + months, 1);\n  const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();\n  return new Date(first.getFullYear(), first.getMonth(), Math.min(date.getDate(), last));\n}\n/** Months counted from year 0, to compare and count months */\nconst monthIndex = (date: Date) => date.getFullYear() * 12 + date.getMonth();\nconst pad = (value: number, length = 2) => String(value).padStart(length, \"0\");\n/** A day as YYYY-MM-DD: it sorts like the days themselves */\nconst keyOf = (date: Date) =>\n  `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\nfunction dateOf(key: string) {\n  const [year = 0, month = 1, day = 1] = key.split(\"-\").map(Number);\n  return new Date(year, month - 1, day);\n}\n\n/** The weeks a month's grid shows: whole weeks from weekStartsOn, six of them with fixedWeeks */\nfunction weeksOf(month: Date, weekStartsOn: Weekday, fixedWeeks: boolean) {\n  const offset = (month.getDay() - weekStartsOn + 7) % 7;\n  const days = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();\n  const count = fixedWeeks ? WEEKS : Math.ceil((offset + days) / 7);\n  return Array.from({ length: count }, (_, week) =>\n    Array.from({ length: 7 }, (_, day) => addDays(month, week * 7 + day - offset)),\n  );\n}\n\nfunction matches(date: Date, matcher: Matcher | Matcher[] | undefined): boolean {\n  if (matcher === undefined) return false;\n  if (Array.isArray(matcher)) return matcher.some((each) => matches(date, each));\n  if (typeof matcher === \"boolean\") return matcher;\n  if (typeof matcher === \"function\") return matcher(date);\n  const key = keyOf(date);\n  if (matcher instanceof Date) return key === keyOf(matcher);\n  if (\"dayOfWeek\" in matcher) {\n    const days = Array.isArray(matcher.dayOfWeek) ? matcher.dayOfWeek : [matcher.dayOfWeek];\n    return days.includes(date.getDay());\n  }\n  if (\"from\" in matcher) {\n    if (!matcher.from) return false;\n    const [start, end] = [keyOf(matcher.from), keyOf(matcher.to ?? matcher.from)].sort();\n    return key >= (start ?? key) && key <= (end ?? key);\n  }\n  const before = \"before\" in matcher ? keyOf(matcher.before) : null;\n  const after = \"after\" in matcher ? keyOf(matcher.after) : null;\n  // Both: the days between them, or, the wrong way round, the days outside them (react-day-picker's rule)\n  if (before !== null && after !== null)\n    return after < before ? key > after && key < before : key < before || key > after;\n  return (before !== null && key < before) || (after !== null && key > after);\n}\n\n/** Calls back at the next local midnight, so the marked day moves on with the clock */\nfunction subscribeToMidnight(onChange: () => void) {\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  const schedule = () => {\n    const now = new Date();\n    timer = setTimeout(\n      () => {\n        onChange();\n        schedule();\n      },\n      addDays(dayOf(now), 1).getTime() - now.getTime(),\n    );\n  };\n  schedule();\n  return () => clearTimeout(timer);\n}\nconst todayKey = () => keyOf(new Date());\n/** The server (and hydration) know no today, so their HTML always matches */\nconst noToday = () => null;\n\n/** A day's centre in the calendar, in layout pixels (offsets, so transforms and zoom don't count) */\nfunction centreOf(node: HTMLElement, root: HTMLElement) {\n  let x = node.offsetWidth / 2;\n  let y = node.offsetHeight / 2;\n  let element: HTMLElement | null = node;\n  while (element && element !== root) {\n    x += element.offsetLeft;\n    y += element.offsetTop;\n    element = element.offsetParent as HTMLElement | null;\n  }\n  return element === root ? { x, y } : null;\n}\n\nconst dayButton = (root: HTMLElement, key: string) =>\n  root.querySelector<HTMLElement>(`[data-slot=calendar-day][data-day=\"${key}\"]`);\n\ntype Point = { x: number; y: number };\n/** One row's stretch of the growing band: from its first day's centre to its last's */\ntype Segment = { start: Point; end: Point; offset: number; length: number };\ntype Band = { segments: Segment[]; total: number; size: number };\n/** What's picked, either mode: single keeps its day in `from` */\ntype Picked = { from?: Date; to?: Date };\n\nconst pickedOf = (value: Date | DateRange | undefined): Picked =>\n  value instanceof Date ? { from: value } : value?.from ? { from: value.from, to: value.to } : {};\n\ntype CalendarBaseProps = Omit<ComponentProps<\"div\">, \"onSelect\" | \"defaultValue\" | \"children\"> & {\n  /** The first month shown, when you control it */\n  month?: Date;\n  /** The first month shown at first. Defaults to the picked day's month, else today's. */\n  defaultMonth?: Date;\n  onMonthChange?: (month: Date) => void;\n  /** Months shown side by side */\n  numberOfMonths?: number;\n  /** Days that can't be picked */\n  disabled?: Matcher | Matcher[];\n  /** The earliest day that can be picked; the calendar doesn't page before its month */\n  min?: Date;\n  /** The latest day that can be picked; the calendar doesn't page past its month */\n  max?: Date;\n  /** 0 is Sunday */\n  weekStartsOn?: Weekday;\n  /** A BCP 47 language tag for the month and day names */\n  locale?: string;\n  /** Shows the days of the months around, muted */\n  showOutsideDays?: boolean;\n  /** Always six weeks, so the calendar keeps its height from month to month */\n  fixedWeeks?: boolean;\n  /** The day marked as today. Defaults to the browser's today, known after hydration. */\n  today?: Date;\n  /** single: clicking the picked day again keeps it instead of letting it go */\n  required?: boolean;\n  labels?: Partial<CalendarLabels>;\n};\n\nexport type CalendarSingleProps = CalendarBaseProps & {\n  mode?: \"single\";\n  /** The picked day, when you control it */\n  selected?: Date | undefined;\n  defaultSelected?: Date;\n  onSelect?: (date: Date | undefined, day: Date) => void;\n};\n\nexport type CalendarRangeProps = CalendarBaseProps & {\n  mode: \"range\";\n  /** The picked range, when you control it */\n  selected?: DateRange | undefined;\n  defaultSelected?: DateRange;\n  onSelect?: (range: DateRange | undefined, day: Date) => void;\n};\n\nexport type CalendarProps = CalendarSingleProps | CalendarRangeProps;\n\n/**\n * One row's stretch of the band while it grows: a cap where it starts, a body that stretches from there and\n * a cap riding its front. Rows fill one after another as `progress` goes from 0 to 1.\n */\nfunction BandSegment({\n  segment,\n  total,\n  size,\n  progress,\n}: {\n  segment: Segment;\n  /** Steps along the whole band, rows end to end */\n  total: number;\n  size: number;\n  progress: MotionValue<number>;\n}) {\n  const { start, end, offset, length } = segment;\n  const reach = useTransform(progress, (value) => {\n    const front = value * total;\n    return length > 0 ? Math.min(1, Math.max(0, (front - offset) / length)) : front >= offset ? 1 : 0;\n  });\n  const shown = useTransform(progress, (value) => (value * total >= offset ? 1 : 0));\n  const distance = end.x - start.x;\n  const ride = useTransform(reach, (value) => value * distance);\n  const cap = { left: start.x - size / 2, top: start.y - size / 2, width: size, height: size };\n\n  return (\n    <>\n      <motion.span className=\"absolute rounded-full bg-primary\" style={{ ...cap, opacity: shown }} />\n      {length > 0 ? (\n        <>\n          <motion.span\n            className=\"absolute bg-primary\"\n            style={{\n              left: Math.min(start.x, end.x),\n              top: cap.top,\n              width: Math.abs(distance),\n              height: size,\n              scaleX: reach,\n              originX: distance >= 0 ? 0 : 1,\n              opacity: shown,\n            }}\n          />\n          <motion.span\n            className=\"absolute rounded-full bg-primary\"\n            style={{ ...cap, x: ride, opacity: shown }}\n          />\n        </>\n      ) : null}\n    </>\n  );\n}\n\n/**\n * The band's tint: the primary colour, thinned, so it reads on any surface (the page, a card, a popover) and\n * clearly marks the range. The growing band tints its layer instead (BAND_LAYER), which comes out the same.\n */\nconst BAND = \"bg-primary/10 dark:bg-primary/15\";\nconst BAND_LAYER = \"opacity-10 dark:opacity-15\";\n/** The range you'd get, while you choose its end: the band's tint, fainter */\nconst PREVIEW = \"bg-primary/5 dark:bg-primary/8\";\n/** Today: a filled circle that stands clear of any surface, deepening under the pointer */\nconst TODAY = \"bg-primary/15 hover:bg-primary/20 dark:bg-primary/20 dark:hover:bg-primary/25\";\n\nconst dayVariants = [\n  \"absolute inset-0.5 flex cursor-pointer items-center justify-center rounded-full text-sm tabular-nums\",\n  \"transition-colors duration-(--duration-fast) ease-out\",\n  \"outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n  \"disabled:pointer-events-none disabled:opacity-(--opacity-disabled)\",\n];\n\n/**\n * A month view to pick a day (`mode=\"single\"`, the default) or a range (`mode=\"range\"`) from. Each month's\n * grid is named by its month; label the calendar from the page around it when it needs more.\n */\nfunction Calendar(props: CalendarProps) {\n  const {\n    mode = \"single\",\n    selected,\n    defaultSelected,\n    onSelect,\n    required = false,\n    month: monthProp,\n    defaultMonth,\n    onMonthChange,\n    numberOfMonths = 1,\n    disabled,\n    min,\n    max,\n    weekStartsOn = 0,\n    locale = \"en-US\",\n    showOutsideDays = true,\n    fixedWeeks = true,\n    today: todayProp,\n    labels: labelsProp,\n    className,\n    ref,\n    ...rest\n  } = props;\n  const id = useId();\n  const reduce = useReducedMotion() ?? false;\n  const labels = { ...defaultLabels, ...labelsProp };\n  const range = mode === \"range\";\n  const count = Math.max(1, Math.floor(numberOfMonths));\n  const root = useRef<HTMLDivElement>(null);\n  const monthsBox = useRef<HTMLDivElement>(null);\n\n  // What's picked, controlled or not\n  const controlled = \"selected\" in props;\n  const [own, setOwn] = useState<Picked>(() => pickedOf(defaultSelected));\n  const picked = controlled ? pickedOf(selected) : own;\n  const fromKey = picked.from ? keyOf(picked.from) : null;\n  const toKey = range && picked.to ? keyOf(picked.to) : null;\n  // The day the pointer (or the keyboard) is on, so a range being chosen can show where it would end\n  const [hoverKey, setHoverKey] = useState<string | null>(null);\n  const preview =\n    range && fromKey !== null && toKey === null && hoverKey !== null && hoverKey !== fromKey\n      ? hoverKey < fromKey\n        ? { lo: hoverKey, hi: fromKey }\n        : { lo: fromKey, hi: hoverKey }\n      : null;\n\n  // The month: yours, else the one paged to, else the picked day's, else today's\n  const browserToday = useSyncExternalStore(subscribeToMidnight, todayKey, noToday);\n  const today = todayProp ? dayOf(todayProp) : browserToday ? dateOf(browserToday) : undefined;\n  const todayDay = today ? keyOf(today) : null;\n  const [ownMonth, setOwnMonth] = useState<Date | undefined>(() => {\n    const start = defaultMonth ?? picked.from;\n    return start ? monthOf(start) : undefined;\n  });\n  const month = monthProp ? monthOf(monthProp) : (ownMonth ?? (today ? monthOf(today) : undefined));\n  const pending = month === undefined;\n  const first = month ?? PLACEHOLDER;\n  const firstIndex = monthIndex(first);\n  const lastIndex = firstIndex + count - 1;\n  // Each slot keeps its element from month to month, so its name can swap and its days slide in\n  const months = Array.from({ length: count }, (_, slot) => ({\n    slot,\n    date: new Date(first.getFullYear(), first.getMonth() + slot, 1),\n  }));\n  const inView = (date: Date) => monthIndex(date) >= firstIndex && monthIndex(date) <= lastIndex;\n\n  const minKey = min ? keyOf(min) : null;\n  const maxKey = max ? keyOf(max) : null;\n  const outOfBounds = (key: string) => (minKey !== null && key < minKey) || (maxKey !== null && key > maxKey);\n  const isDisabled = (date: Date) => outOfBounds(keyOf(date)) || matches(date, disabled);\n  const canGoBack = !pending && (!min || monthIndex(min) < firstIndex);\n  const canGoForward = !pending && (!max || monthIndex(max) > lastIndex);\n\n  const format = useMemo(\n    () => ({\n      caption: new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }),\n      day: new Intl.DateTimeFormat(locale, { day: \"numeric\" }),\n      label: new Intl.DateTimeFormat(locale, {\n        weekday: \"long\",\n        year: \"numeric\",\n        month: \"long\",\n        day: \"numeric\",\n      }),\n      weekday: new Intl.DateTimeFormat(locale, { weekday: \"short\" }),\n    }),\n    [locale],\n  );\n  // 7 January 2024 was a Sunday, so the week from it names each weekday\n  const weekdays = useMemo(\n    () =>\n      Array.from({ length: 7 }, (_, index) => {\n        const weekday = (weekStartsOn + index) % 7;\n        return { weekday, name: format.weekday.format(new Date(2024, 0, 7 + weekday)) };\n      }),\n    [format, weekStartsOn],\n  );\n\n  function goToMonth(next: Date) {\n    let target = monthOf(next);\n    // The months shown stay inside the limits\n    if (max && monthIndex(target) + count - 1 > monthIndex(max))\n      target = new Date(max.getFullYear(), max.getMonth() - count + 1, 1);\n    if (min && monthIndex(target) < monthIndex(min)) target = monthOf(min);\n    if (!pending && monthIndex(target) === firstIndex) return;\n    if (!monthProp) setOwnMonth(target);\n    onMonthChange?.(target);\n  }\n\n  // Roving focus: the day the keyboard is on, else the picked day, today, or the first day that can be picked\n  const [focused, setFocused] = useState<Date>();\n  const focusPending = useRef(false);\n  const tabbable = (() => {\n    for (const date of [focused, picked.from, today]) {\n      if (date && inView(date) && !isDisabled(date)) return keyOf(date);\n    }\n    for (const { date: shown } of months) {\n      for (let day = shown; day.getMonth() === shown.getMonth(); day = addDays(day, 1)) {\n        if (!isDisabled(day)) return keyOf(day);\n      }\n    }\n    return null;\n  })();\n\n  function pick(day: Date) {\n    if (isDisabled(day)) return;\n    setFocused(day);\n    const key = keyOf(day);\n    let next: Picked;\n    if (range) {\n      // The first click starts a range, the second ends it (either way round), and a third starts again\n      const { from, to } = picked;\n      if (!from || to) next = { from: day };\n      else if (key < keyOf(from)) next = { from: day, to: from };\n      else next = { from, to: day };\n    } else {\n      next = fromKey === key && !required ? {} : { from: day };\n    }\n    if (!controlled) setOwn(next);\n    if (range)\n      (onSelect as CalendarRangeProps[\"onSelect\"])?.(\n        next.from ? { from: next.from, to: next.to } : undefined,\n        day,\n      );\n    else (onSelect as CalendarSingleProps[\"onSelect\"])?.(next.from, day);\n  }\n\n  function move(from: Date, event: KeyboardEvent<HTMLButtonElement>) {\n    // Left and right follow the page's direction\n    const forward = getComputedStyle(event.currentTarget).direction === \"rtl\" ? -1 : 1;\n    const weekday = (from.getDay() - weekStartsOn + 7) % 7;\n    let target: Date;\n    // Which way to look past a disabled day\n    let step: number;\n    switch (event.key) {\n      case \"ArrowLeft\":\n        step = -forward;\n        target = addDays(from, step);\n        break;\n      case \"ArrowRight\":\n        step = forward;\n        target = addDays(from, step);\n        break;\n      case \"ArrowUp\":\n        step = -1;\n        target = addDays(from, -7);\n        break;\n      case \"ArrowDown\":\n        step = 1;\n        target = addDays(from, 7);\n        break;\n      case \"PageUp\":\n        step = -1;\n        target = addMonths(from, event.shiftKey ? -12 : -1);\n        break;\n      case \"PageDown\":\n        step = 1;\n        target = addMonths(from, event.shiftKey ? 12 : 1);\n        break;\n      case \"Home\":\n        step = 1;\n        target = addDays(from, -weekday);\n        break;\n      case \"End\":\n        step = -1;\n        target = addDays(from, 6 - weekday);\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n    // Inside the limits, then on past disabled days, a day at a time\n    if (min && keyOf(target) < keyOf(min)) {\n      target = dayOf(min);\n      step = 1;\n    }\n    if (max && keyOf(target) > keyOf(max)) {\n      target = dayOf(max);\n      step = -1;\n    }\n    for (let tries = 0; isDisabled(target); tries++) {\n      target = addDays(target, step);\n      if (tries > SEARCH || outOfBounds(keyOf(target))) return;\n    }\n    if (keyOf(target) === keyOf(from)) return;\n    setFocused(target);\n    focusPending.current = true;\n    if (!inView(target))\n      goToMonth(monthIndex(target) < firstIndex ? target : addMonths(monthOf(target), 1 - count));\n  }\n\n  // After a key moves the focus (and maybe the page), the day it landed on takes it\n  useLayoutEffect(() => {\n    if (!focusPending.current) return;\n    focusPending.current = false;\n    const key = focused ? keyOf(focused) : null;\n    if (key)\n      root.current\n        ?.querySelector<HTMLElement>(`[data-slot=calendar-day][data-day=\"${key}\"]:not([data-outside])`)\n        ?.focus();\n  });\n\n  // The shared circle (single) and the growing band (range)\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const progress = useMotionValue(0);\n  /** The travelling circle's diameter while it travels */\n  const [trip, setTrip] = useState<number | null>(null);\n  /** The day the circle is on its way to; its own circle waits until it lands */\n  const [arriving, setArriving] = useState<string | null>(null);\n  /** Whether the circle covers that day yet, so its number can turn light */\n  const [covered, setCovered] = useState(false);\n  const [band, setBand] = useState<Band | null>(null);\n  const running = useRef<AnimationPlaybackControls[]>([]);\n  const runId = useRef(0);\n  const last = useRef({ from: fromKey, to: toKey, page: pending ? null : firstIndex });\n\n  // Unmounting mid-trip stops the animations\n  useEffect(\n    () => () => {\n      for (const animation of running.current) animation.stop();\n    },\n    [],\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: only a new pick starts a trip or a growth\n  useLayoutEffect(() => {\n    const before = last.current;\n    const page = pending ? null : firstIndex;\n    last.current = { from: fromKey, to: toKey, page };\n    if (before.from === fromKey && before.to === toKey) return;\n    for (const animation of running.current) animation.stop();\n    running.current = [];\n    const id = ++runId.current;\n    const box = root.current;\n    const settle = () => {\n      setTrip(null);\n      setArriving(null);\n      setCovered(false);\n      setBand(null);\n    };\n    // A pick that arrives with a new page (or the first page) simply shows\n    if (reduce || !box || page === null || before.page !== page) {\n      settle();\n      return;\n    }\n\n    if (range) {\n      setTrip(null);\n      setArriving(null);\n      if (!fromKey || !toKey || fromKey === toKey) {\n        setBand(null);\n        return;\n      }\n      // The band grows from the day picked first: the end, when the end was picked before the start\n      const backward = before.to === null && before.from === toKey;\n      const days = Array.from(box.querySelectorAll<HTMLElement>(\"[data-slot=calendar-day]\")).filter((day) => {\n        const key = day.dataset.day ?? \"\";\n        return key >= fromKey && key <= toKey;\n      });\n      if (backward) days.reverse();\n      // One segment per row, measured centre to centre; `total` counts the steps along every row\n      const segments: Segment[] = [];\n      let total = 0;\n      let row: Element | null = null;\n      for (const day of days) {\n        const centre = centreOf(day, box);\n        if (!centre) continue;\n        const current = segments.at(-1);\n        const parent = day.closest(\"[role=row]\");\n        if (current && parent === row) {\n          current.end = centre;\n          current.length += 1;\n          total += 1;\n        } else {\n          segments.push({ start: centre, end: centre, offset: total, length: 0 });\n        }\n        row = parent;\n      }\n      if (total === 0) {\n        setBand(null);\n        return;\n      }\n      setBand({ segments, total, size: days[0]?.offsetWidth ?? 0 });\n      progress.jump(0);\n      const growth = animate(progress, 1, { duration: duration.slow, ease: ease.out });\n      running.current = [growth];\n      growth.then(() => {\n        if (id === runId.current) setBand(null);\n      });\n      return;\n    }\n\n    setBand(null);\n    const start = before.from ? dayButton(box, before.from) : null;\n    const end = fromKey ? dayButton(box, fromKey) : null;\n    const a = start ? centreOf(start, box) : null;\n    const b = end ? centreOf(end, box) : null;\n    if (!end || !a || !b) {\n      settle();\n      return;\n    }\n    const size = end.offsetWidth;\n    // Placed with jump(), which resets the speed (set() would count the jump as speed and overshoot). Mid-trip\n    // it carries on from where it is with its speed, so it curves to the new day.\n    if (trip === null) {\n      x.jump(a.x);\n      y.jump(a.y);\n    }\n    setTrip(size);\n    setArriving(fromKey);\n    setCovered(false);\n    // The day's number turns light once the circle covers it\n    const cover = () => {\n      if (Math.hypot(x.get() - b.x, y.get() - b.y) <= size / 2) setCovered(true);\n    };\n    const trips = [\n      animate(x, b.x, { ...spring.snappy, onUpdate: cover }),\n      animate(y, b.y, { ...spring.snappy, onUpdate: cover }),\n    ];\n    running.current = trips;\n    Promise.all(trips).then(() => {\n      if (id !== runId.current) return;\n      running.current = [];\n      settle();\n    });\n  }, [fromKey, toKey, firstIndex, pending]);\n\n  // The months' height, kept current, so a new month of another height can glide from it\n  const height = useRef(0);\n  useLayoutEffect(() => {\n    const box = monthsBox.current;\n    if (!box) return;\n    height.current = box.offsetHeight;\n    const observer = new ResizeObserver(() => {\n      height.current = box.offsetHeight;\n    });\n    observer.observe(box);\n    return () => observer.disconnect();\n  }, []);\n\n  // Turning the page: the new days come through the blur from the side you're going to\n  const lastPage = useRef<number | null>(pending ? null : firstIndex);\n  // biome-ignore lint/correctness/useExhaustiveDependencies: only turning the page slides the days in\n  useLayoutEffect(() => {\n    const before = lastPage.current;\n    const page = pending ? null : firstIndex;\n    lastPage.current = page;\n    const box = root.current;\n    const wrap = monthsBox.current;\n    if (!box || !wrap || before === null || page === null || before === page) return;\n    // From the right going forward, or from the left on a right-to-left page\n    const side = (page > before ? 1 : -1) * (getComputedStyle(box).direction === \"rtl\" ? -1 : 1);\n    const grids = Array.from(box.querySelectorAll<HTMLElement>(\"[data-slot=calendar-grid]\"));\n    const runs = grids.map((grid) => {\n      const entrance = animate(\n        grid,\n        reduce\n          ? { opacity: [0, 1] }\n          : { opacity: [0, 1], x: [side * SLIDE, 0], filter: [blur.hidden, blur.shown] },\n        { duration: duration.base, ease: ease.out },\n      );\n      // A filter left behind would leave a stacking context round the days\n      entrance.then(() => grid.style.removeProperty(\"filter\"));\n      return entrance;\n    });\n    // Without fixedWeeks a month can be a row taller or shorter, and the calendar glides to its new height.\n    // Inside a popover it doesn't: the popover glides itself (Popover), and two glides would go in two steps.\n    const start = height.current;\n    const end = wrap.offsetHeight;\n    if (!reduce && start > 0 && end > 0 && start !== end && !box.closest(\"[data-slot=popover-content]\")) {\n      wrap.style.overflow = \"hidden\";\n      const glide = animate(wrap, { height: [start, end] }, { duration: duration.slow, ease: ease.out });\n      glide.then(() => {\n        wrap.style.removeProperty(\"height\");\n        wrap.style.removeProperty(\"overflow\");\n      });\n      runs.push(glide);\n    }\n    // Paged again mid-entrance: stop, and leave nothing behind for the next one\n    return () => {\n      for (const run of runs) run.stop();\n      for (const grid of grids) for (const property of LEFTOVERS) grid.style.removeProperty(property);\n      wrap.style.removeProperty(\"height\");\n      wrap.style.removeProperty(\"overflow\");\n    };\n  }, [firstIndex, pending]);\n\n  const caption = (date: Date, slot: number) => {\n    const name = format.caption.format(date);\n    return (\n      <div\n        data-slot=\"calendar-caption\"\n        className=\"relative flex h-(--size-control-md) items-center justify-center\"\n      >\n        {slot === 0 ? (\n          <Button\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-previous\"\n            aria-label={labels.previousMonth}\n            // Still focusable at the limit, so the focus isn't lost when it can go no further\n            aria-disabled={!canGoBack || undefined}\n            onClick={() => {\n              if (canGoBack) goToMonth(new Date(first.getFullYear(), first.getMonth() - 1, 1));\n            }}\n            className=\"absolute start-0 aria-disabled:pointer-events-none aria-disabled:opacity-(--opacity-disabled)\"\n          >\n            <ChevronLeftIcon aria-hidden className=\"rtl:rotate-180\" />\n          </Button>\n        ) : null}\n        {/* The name is read out when the page turns; what's drawn swaps through the blur */}\n        <div aria-live=\"polite\" className=\"text-sm font-medium\">\n          <span id={`${id}-${slot}`} className=\"sr-only\">\n            {name}\n          </span>\n          <span aria-hidden className=\"grid\">\n            {pending ? null : (\n              <AnimatePresence initial={false}>\n                <motion.span\n                  key={name}\n                  {...(reduce\n                    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n                    : swap)}\n                  transition={\n                    reduce\n                      ? { duration: duration.fast }\n                      : { duration: duration.base, ease: ease.out, y: spring.snappy }\n                  }\n                  className=\"col-start-1 row-start-1 text-center whitespace-nowrap\"\n                >\n                  {name}\n                </motion.span>\n              </AnimatePresence>\n            )}\n          </span>\n        </div>\n        {slot === count - 1 ? (\n          <Button\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-next\"\n            aria-label={labels.nextMonth}\n            aria-disabled={!canGoForward || undefined}\n            onClick={() => {\n              if (canGoForward) goToMonth(new Date(first.getFullYear(), first.getMonth() + 1, 1));\n            }}\n            className=\"absolute end-0 aria-disabled:pointer-events-none aria-disabled:opacity-(--opacity-disabled)\"\n          >\n            <ChevronRightIcon aria-hidden className=\"rtl:rotate-180\" />\n          </Button>\n        ) : null}\n      </div>\n    );\n  };\n\n  /** One day: its cell, its piece of the band (or of the preview), and its button with its circle */\n  const cell = (day: Date, shown: Date, visible: boolean, joinsBefore: boolean, joinsAfter: boolean) => {\n    const key = keyOf(day);\n    // An empty cell holds the place of a day that isn't shown (outside days, or another shown month's)\n    if (!visible)\n      return (\n        // biome-ignore lint/a11y/useSemanticElements: a cell of the WAI-ARIA date grid; the grid is built from divs so its rows can lay out and slide freely\n        // biome-ignore lint/a11y/useFocusableInteractive: an empty cell, with nothing to focus\n        <div key={key} role=\"gridcell\" className=\"size-(--size-control-md)\" />\n      );\n    const outside = day.getMonth() !== shown.getMonth();\n    const endpoint = key === fromKey || key === toKey;\n    const middle = fromKey !== null && toKey !== null && key > fromKey && key < toKey;\n    const isPicked = endpoint || middle;\n    const waiting = arriving === key && !covered;\n    const off = isDisabled(day);\n    const isToday = key === todayDay;\n    const previewed = preview !== null && key >= preview.lo && key <= preview.hi;\n    // The day you're on while choosing a range's end already looks like the end it would be\n    const previewEnd = preview !== null && key === hoverKey;\n    const label = format.label.format(day);\n\n    return (\n      // biome-ignore lint/a11y/useSemanticElements: a cell of the WAI-ARIA date grid, built from divs like its rows\n      // biome-ignore lint/a11y/useFocusableInteractive: focus lives on the day's button inside the cell, with a roving tabindex\n      <div\n        key={key}\n        role=\"gridcell\"\n        aria-selected={isPicked || undefined}\n        className=\"relative size-(--size-control-md)\"\n      >\n        {/* The band at rest: each day in the range draws its piece, from its centre at the ends, rounded where the row breaks */}\n        {fromKey !== null && toKey !== null && fromKey !== toKey && isPicked && band === null ? (\n          <span\n            aria-hidden\n            data-slot=\"calendar-range\"\n            className={cn(\n              \"pointer-events-none absolute inset-y-0.5 -z-20\",\n              BAND,\n              key === fromKey ? \"start-1/2\" : \"start-0\",\n              key === toKey ? \"end-1/2\" : \"end-0\",\n              key !== fromKey && !joinsBefore && \"rounded-s-full\",\n              key !== toKey && !joinsAfter && \"rounded-e-full\",\n            )}\n          />\n        ) : null}\n        {/* The range it would make, while you choose its end: fainter, ending under the day you're on */}\n        {previewed && preview ? (\n          <span\n            aria-hidden\n            data-slot=\"calendar-preview\"\n            className={cn(\n              \"pointer-events-none absolute inset-y-0.5 -z-20\",\n              PREVIEW,\n              key === preview.lo ? \"start-1/2\" : \"start-0\",\n              key === preview.hi ? \"end-1/2\" : \"end-0\",\n              key !== preview.lo && !joinsBefore && \"rounded-s-full\",\n              key !== preview.hi && !joinsAfter && \"rounded-e-full\",\n            )}\n          />\n        ) : null}\n        <button\n          type=\"button\"\n          data-slot=\"calendar-day\"\n          data-day={key}\n          data-outside={outside ? \"\" : undefined}\n          data-today={isToday ? \"\" : undefined}\n          data-selected={isPicked ? \"\" : undefined}\n          data-range-start={range && key === fromKey ? \"\" : undefined}\n          data-range-end={key === toKey ? \"\" : undefined}\n          data-range-middle={middle ? \"\" : undefined}\n          disabled={off}\n          tabIndex={key === tabbable && !outside ? 0 : -1}\n          aria-label={isPicked ? `${label}, ${labels.selected}` : label}\n          aria-current={isToday ? \"date\" : undefined}\n          onClick={() => pick(day)}\n          onPointerEnter={() => setHoverKey(key)}\n          onFocus={() => {\n            setFocused(day);\n            setHoverKey(key);\n          }}\n          onKeyDown={(event) => move(day, event)}\n          className={cn(\n            dayVariants,\n            (endpoint && !waiting) || previewEnd\n              ? \"text-primary-foreground\"\n              : middle || previewed\n                ? \"text-foreground\"\n                : outside || off\n                  ? \"text-muted-foreground\"\n                  : \"text-foreground\",\n            // Today is a filled circle in heavier type; picked or in a range, it takes that look instead\n            isToday && \"font-semibold\",\n            isToday && !isPicked && !previewed && TODAY,\n            !isPicked && !previewEnd && !isToday && \"hover:bg-accent hover:text-accent-foreground\",\n            // Inside the band a day you point at deepens it, rather than covering it in grey\n            middle && \"hover:bg-primary/10\",\n          )}\n        >\n          {/* Shows and goes at once, handing over to and from the travelling circle */}\n          {(endpoint && arriving !== key) || previewEnd ? (\n            <span\n              aria-hidden\n              data-slot={previewEnd ? \"calendar-preview-end\" : undefined}\n              className=\"pointer-events-none absolute inset-0 -z-10 rounded-full bg-primary\"\n            />\n          ) : null}\n          {format.day.format(day)}\n        </button>\n      </div>\n    );\n  };\n\n  return (\n    <div\n      ref={(node) => {\n        root.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      data-slot=\"calendar\"\n      data-mode={mode}\n      // Until the browser says which month it is, the calendar holds its place without showing a wrong month\n      aria-hidden={pending || undefined}\n      className={cn(\"relative isolate w-fit p-4\", pending && \"invisible\", className)}\n      {...rest}\n    >\n      {band ? (\n        // The band while it grows, under every day (the cells aren't stacking contexts; the calendar is `isolate`)\n        <span\n          aria-hidden\n          data-slot=\"calendar-band\"\n          // Its pieces are solid and the layer is tinted as one, so where the caps overlap the body it never darkens\n          className={cn(\"pointer-events-none absolute inset-0 -z-20\", BAND_LAYER)}\n        >\n          {band.segments.map((segment) => (\n            <BandSegment\n              key={`${segment.start.x},${segment.start.y}`}\n              segment={segment}\n              total={band.total}\n              size={band.size}\n              progress={progress}\n            />\n          ))}\n        </span>\n      ) : null}\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: only clears the drawn preview; the days are the controls */}\n      <div\n        ref={monthsBox}\n        data-slot=\"calendar-months\"\n        // The preview follows the pointer and the keyboard, and goes when both have left\n        onPointerLeave={() => setHoverKey(null)}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget)) setHoverKey(null);\n        }}\n        className=\"flex flex-wrap justify-center gap-x-8 gap-y-6\"\n      >\n        {months.map(({ slot, date: shown }) => {\n          const weeks = weeksOf(shown, weekStartsOn, fixedWeeks);\n          // Outside days show unless another shown month already has them\n          const visible = (day: Date) =>\n            day.getMonth() === shown.getMonth() || (showOutsideDays && !inView(day));\n          return (\n            <div key={slot} data-slot=\"calendar-month\" className=\"flex flex-col gap-4\">\n              {caption(shown, slot)}\n              <div className=\"flex flex-col gap-1\">\n                {/* Each day's button says its weekday, so the names above the columns are only drawn */}\n                <div aria-hidden className=\"flex\">\n                  {weekdays.map(({ weekday, name }) => (\n                    <span\n                      key={weekday}\n                      className=\"flex h-8 w-(--size-control-md) items-center justify-center text-xs font-medium text-muted-foreground\"\n                    >\n                      {name}\n                    </span>\n                  ))}\n                </div>\n                {/* biome-ignore lint/a11y/useSemanticElements: the WAI-ARIA date grid, built from divs so its weeks can lay out as rows of circles and slide in as one */}\n                <div\n                  role=\"grid\"\n                  aria-labelledby={`${id}-${slot}`}\n                  aria-multiselectable={range || undefined}\n                  data-slot=\"calendar-grid\"\n                  className=\"flex flex-col\"\n                >\n                  {weeks.map((week) => {\n                    const shownDays = week.map(visible);\n                    return (\n                      // biome-ignore lint/a11y/useSemanticElements: a row of the WAI-ARIA date grid, built from divs like the grid\n                      // biome-ignore lint/a11y/useFocusableInteractive: focus lives on the day buttons inside the row\n                      <div key={keyOf(week[0] ?? shown)} role=\"row\" className=\"flex\">\n                        {week.map((day, column) =>\n                          cell(\n                            day,\n                            shown,\n                            shownDays[column] ?? false,\n                            shownDays[column - 1] ?? false,\n                            shownDays[column + 1] ?? false,\n                          ),\n                        )}\n                      </div>\n                    );\n                  })}\n                </div>\n              </div>\n            </div>\n          );\n        })}\n      </div>\n      {trip !== null ? (\n        // The travelling circle: a point at its centre, with the circle drawn round it, under the days' numbers\n        <motion.span\n          aria-hidden\n          data-slot=\"calendar-travelling-day\"\n          className=\"pointer-events-none absolute top-0 left-0 -z-10 size-0\"\n          style={{ x, y }}\n        >\n          <span\n            className=\"absolute rounded-full bg-primary\"\n            style={{ left: -trip / 2, top: -trip / 2, width: trip, height: trip }}\n          />\n        </motion.span>\n      ) : null}\n    </div>\n  );\n}\n\nexport { Calendar };\n"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/calendar"
}
