{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "Browse dates in a clear, compact month view.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/use-today.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/calendar/calendar.tsx",
      "content": "\"use client\";\n\nimport { useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport {\n  animate,\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport type { AnimationPlaybackControls, MotionValue, Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { useToday } from \"../lib/use-today\";\nimport { cn } from \"../lib/utils\";\n\nexport type CalendarDateMatcher = (date: Date) => boolean;\n\nexport interface CalendarProps {\n  value?: Date;\n  onChange?: (date: Date) => void;\n  month?: Date;\n  onMonthChange?: (month: Date) => void;\n  minDate?: Date;\n  maxDate?: Date;\n  disabledDates?: CalendarDateMatcher;\n  locale?: string;\n  className?: string;\n  /** Adds a Today button that slides back to the current month and selects today when it is available. */\n  showToday?: boolean;\n}\n\nconst startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());\nconst monthStart = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1);\nconst sameDay = (a?: Date, b?: Date) =>\n  Boolean(\n    a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(),\n  );\nconst sameMonth = (a?: Date, b?: Date) =>\n  Boolean(a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth());\nconst addDays = (date: Date, amount: number) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);\nconst addMonths = (date: Date, amount: number) => new Date(date.getFullYear(), date.getMonth() + amount, 1);\n/** Moves by whole months and keeps the day, clamped to the shorter month: January 31 plus one month is February 28. */\nconst shiftMonths = (date: Date, amount: number) =>\n  new Date(\n    date.getFullYear(),\n    date.getMonth() + amount,\n    Math.min(date.getDate(), new Date(date.getFullYear(), date.getMonth() + amount + 1, 0).getDate()),\n  );\nconst isBefore = (a: Date, b?: Date) => Boolean(b && startOfDay(a).getTime() < startOfDay(b).getTime());\nconst isAfter = (a: Date, b?: Date) => Boolean(b && startOfDay(a).getTime() > startOfDay(b).getTime());\nconst dateKey = (date: Date) =>\n  `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n/** Every month shows six weeks, so the grid keeps one height and never jumps while months change. */\nconst makeWeeks = (month: Date) => {\n  const start = addDays(month, -month.getDay());\n  return Array.from({ length: 6 }, (_, week) => Array.from({ length: 7 }, (_, day) => addDays(start, week * 7 + day)));\n};\n\nconst subscribeNothing = () => () => {};\nconst clientSnapshot = () => true;\nconst serverSnapshot = () => false;\n/** The viewer's local date (lib/use-today.ts, shared with date-range-picker). Re-exported so existing imports from calendar keep working. */\nexport { useToday } from \"../lib/use-today\";\n\nconst { spring, duration, ease } = motionTokens;\n\n/* Quiet header controls: a fill on hover, a short press, and a spring release. */\nconst headerButton =\n  \"grid h-8 place-items-center rounded-button bg-transparent text-ink-2 [transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),opacity_var(--duration-fast)_var(--ease-standard),scale_var(--duration-spring)_var(--ease-spring)] not-aria-disabled:hover:bg-hover not-aria-disabled:hover:text-ink not-aria-disabled:active:scale-[.96] not-aria-disabled:active:ease-standard not-aria-disabled:active:[transition-duration:var(--duration-fast),var(--duration-fast),var(--duration-fast),100ms] aria-disabled:cursor-default! aria-disabled:opacity-36 motion-reduce:transition-none motion-reduce:not-aria-disabled:active:scale-none\";\nconst weekGrid = \"grid grid-cols-[repeat(7,minmax(0,1fr))] gap-x-1\";\n/* The inner circle (::before) carries hover and press; the grid cell itself never moves, so the hit area stays put. */\nconst dayClass =\n  \"group/day relative grid aspect-square w-full min-w-0 place-items-center rounded-[50%] bg-transparent p-0 text-sm leading-none tabular-nums [-webkit-tap-highlight-color:transparent] [transition:color_var(--duration-fast)_var(--ease-standard)_var(--number-delay,0ms)] before:absolute before:inset-0 before:rounded-[inherit] before:bg-transparent before:content-[''] before:[transition:background-color_var(--duration-fast)_var(--ease-standard),scale_var(--duration-spring)_var(--ease-spring)] enabled:active:before:scale-[.94] enabled:active:before:ease-standard enabled:active:before:[transition-duration:var(--duration-fast),100ms] disabled:cursor-default motion-reduce:transition-none motion-reduce:before:transition-none motion-reduce:enabled:active:before:scale-none\";\nconst dayNumberClass =\n  \"relative [transition:opacity_var(--duration-fast)_var(--ease-standard)_var(--number-delay,0ms),scale_var(--duration-spring)_var(--ease-spring)] group-[:active:enabled]/day:scale-[.94] group-[:active:enabled]/day:ease-standard group-[:active:enabled]/day:[transition-duration:var(--duration-fast),100ms] motion-reduce:transition-none motion-reduce:group-[:active:enabled]/day:scale-none\";\n/* Today is a small dot under the number. It takes the number's colour, so it flips with it when the disc arrives. */\nconst todayDot =\n  \"after:absolute after:top-[calc(100%+4px)] after:left-1/2 after:-ml-0.5 after:size-1 after:rounded-[50%] after:bg-current after:content-['']\";\n/* The selected disc: hover deepens it and press scales the inner fill, so it never fights the glide. */\nconst discFill =\n  \"absolute inset-0 rounded-[50%] bg-accent [transition:background-color_var(--duration-fast)_var(--ease-standard),scale_var(--duration-spring)_var(--ease-spring)] group-has-[[aria-selected=true]:hover]/month:bg-accent-ink group-has-[[aria-selected=true]:active:enabled]/month:scale-[.94] group-has-[[aria-selected=true]:active:enabled]/month:ease-standard group-has-[[aria-selected=true]:active:enabled]/month:[transition-duration:var(--duration-fast),100ms] motion-reduce:transition-none motion-reduce:group-has-[[aria-selected=true]:active:enabled]/month:scale-none\";\n/** Months sit side by side on one strip. Rapid clicks retarget the same spring, so the strip never queues or stacks panes. */\nconst stripSpring = { type: \"spring\", visualDuration: 0.36, bounce: 0, restDelta: 0.002 } as const;\nconst monthIndex = (date: Date) => date.getFullYear() * 12 + date.getMonth();\nconst fromIndex = (index: number) => new Date(Math.floor(index / 12), ((index % 12) + 12) % 12, 1);\n\n/** The title crossfades in place: the old month leaves toward the side the strip travels to while the new one arrives from the other. */\nconst titleSlide: Variants = {\n  enter: (direction: number) => ({ opacity: 0, x: direction * 12 }),\n  center: { opacity: 1, x: 0, transition: { duration: duration.standard, ease: ease.enter } },\n  exit: (direction: number) => ({\n    opacity: 0,\n    x: direction * -12,\n    transition: { duration: 0.14, ease: ease.standard },\n  }),\n};\n/** Reduced motion swaps the title in place, with no travel and no blank frame. */\nconst titleFade: Variants = {\n  enter: { opacity: 1, x: 0 },\n  center: { opacity: 1, x: 0, transition: { duration: 0 } },\n  exit: { opacity: 0, transition: { duration: 0 } },\n};\n\n/**\n * The selected disc of one month. It is positioned by week and weekday rather than measured, so it glides on a straight\n * line between any two days, keeps gliding while the strip slides, and retargets mid-flight on a new pick.\n * It only grows in or fades out when the selection enters or leaves this month.\n */\nfunction SelectionDisc({ cell, reduced }: { cell: number; reduced: boolean }) {\n  const visible = cell >= 0;\n  const col = useMotionValue(visible ? cell % 7 : 0);\n  const row = useMotionValue(visible ? Math.floor(cell / 7) : 0);\n  const opacity = useMotionValue(visible ? 1 : 0);\n  const scale = useMotionValue(visible ? 1 : 0.6);\n  // Each step is one cell plus the 4px gap; percentages resolve against the disc's own size, which matches a cell.\n  const transform = useTransform(\n    () => `translate(calc(${col.get()} * (100% + 4px)), calc(${row.get()} * (100% + 4px))) scale(${scale.get()})`,\n  );\n  const hidden = useRef(!visible);\n  useLayoutEffect(() => {\n    if (!visible) {\n      hidden.current = true;\n      const fade = reduced ? { duration: 0 } : { duration: 0.14, ease: ease.standard };\n      const controls = [animate(opacity, 0, fade), animate(scale, 0.6, fade)];\n      return () => controls.forEach((control) => control.stop());\n    }\n    const nextCol = cell % 7,\n      nextRow = Math.floor(cell / 7);\n    const controls: AnimationPlaybackControls[] = [];\n    if (hidden.current || reduced) {\n      col.jump(nextCol);\n      row.jump(nextRow);\n    } else controls.push(animate(col, nextCol, spring.morph), animate(row, nextRow, spring.morph));\n    hidden.current = false;\n    controls.push(\n      animate(opacity, 1, reduced ? { duration: 0 } : { duration: duration.fast, ease: ease.enter }),\n      animate(scale, 1, reduced ? { duration: 0 } : spring.snappy),\n    );\n    return () => controls.forEach((control) => control.stop());\n  }, [cell, visible, reduced, col, row, opacity, scale]);\n  return (\n    <motion.span\n      className=\"pointer-events-none absolute top-0 left-0 aspect-square w-[calc((100%-24px)/7)] will-change-transform\"\n      style={{ transform, opacity }}\n      aria-hidden=\"true\"\n    >\n      <span className={discFill} />\n    </motion.span>\n  );\n}\n\n/* Every pane shares one grid cell and has six weeks, so the height never changes. */\nconst monthBody = \"group/month relative min-w-0 will-change-transform [grid-area:1/1] inert:pointer-events-none\";\n\n/** One month on the strip. Only the month being navigated to is focusable and exposed; the one sliding past is inert. */\nfunction MonthPane({\n  index,\n  position,\n  present,\n  children,\n}: {\n  index: number;\n  position: MotionValue<number>;\n  present: boolean;\n  children: ReactNode;\n}) {\n  const x = useTransform(position, (value) => `calc(${(index - value) * 100}% + ${(index - value) * 16}px)`);\n  const opacity = useTransform(position, (value) => 1 - Math.min(1, Math.abs(index - value)) * 0.6);\n  return (\n    <motion.div\n      className={monthBody}\n      style={{ x, opacity }}\n      data-present={present || undefined}\n      aria-hidden={present ? undefined : true}\n      inert={!present}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport function Calendar({\n  value,\n  onChange,\n  month: controlledMonth,\n  onMonthChange,\n  minDate,\n  maxDate,\n  disabledDates,\n  locale = \"en-US\",\n  className,\n  showToday = false,\n}: CalendarProps) {\n  const titleId = useId();\n  const today = useToday();\n  // Motion preference only counts after hydration, so server and client markup agree.\n  const hydrated = useSyncExternalStore(subscribeNothing, clientSnapshot, serverSnapshot);\n  const reducedMotion = (useReducedMotion() ?? false) && hydrated;\n  const [internalMonth, setInternalMonth] = useState(() => (value ? monthStart(value) : undefined));\n  const [focusedDate, setFocusedDate] = useState(value);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const focusRequest = useRef<string | null>(null);\n  // Without a value or month the calendar waits for the client's today, then settles on that month once.\n  const fallback = value ?? today;\n  const month = controlledMonth ? monthStart(controlledMonth) : (internalMonth ?? (fallback && monthStart(fallback)));\n  if (!controlledMonth && !internalMonth && month) setInternalMonth(month);\n  const monthTime = month?.getTime();\n  const monthKey = month ? dateKey(month) : \"pending\";\n  // Direction is read from the month itself, so arrows, keys, Today, and a controlled month all travel the same way.\n  const [shownMonth, setShownMonth] = useState(monthTime);\n  const [direction, setDirection] = useState(0);\n  if (shownMonth !== monthTime) {\n    setShownMonth(monthTime);\n    setDirection(shownMonth === undefined || monthTime === undefined ? 0 : monthTime > shownMonth ? 1 : -1);\n  }\n  const weeks = useMemo(() => (monthTime === undefined ? [] : makeWeeks(new Date(monthTime))), [monthTime]);\n  // The strip position is measured in months. The panes on either side of it render, plus the month being navigated to.\n  const target = month ? monthIndex(month) : 0;\n  const position = useMotionValue(target);\n  const [span, setSpan] = useState<[number, number]>([target, target]);\n  useMotionValueEvent(position, \"change\", (value) => {\n    const next: [number, number] = [Math.floor(value + 1e-3), Math.ceil(value - 1e-3)];\n    setSpan((current) => (current[0] === next[0] && current[1] === next[1] ? current : next));\n  });\n  const ready = month !== undefined;\n  const placed = useRef(ready);\n  useLayoutEffect(() => {\n    if (!ready) return;\n    const from = position.get();\n    if (from === target) return;\n    // The first month and reduced motion land in place; everything else slides on the strip.\n    if (!placed.current || reducedMotion) {\n      placed.current = true;\n      position.jump(target);\n      return;\n    }\n    // Long jumps (Today, a year with Shift) start one month away, so the strip never scrolls through the months between.\n    if (Math.abs(target - from) > 2) position.jump(target - Math.sign(target - from));\n    const controls = animate(position, target, stripSpring);\n    return () => controls.stop();\n  }, [ready, target, reducedMotion, position]);\n  const paneIndexes = Array.from(new Set([span[0], span[1], target])).sort((a, b) => a - b);\n  const formatter = useMemo(() => new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }), [locale]);\n  const weekdayFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: \"short\" }), [locale]);\n  const weekdays = useMemo(\n    () => Array.from({ length: 7 }, (_, index) => weekdayFormatter.format(new Date(2024, 0, 7 + index))),\n    [weekdayFormatter],\n  );\n  const monthLabel = month ? formatter.format(month) : \"\";\n  // The selected disc glides between days, so each number flips color as the disc actually passes it:\n  // the day it leaves waits longer on short hops, the day it lands on waits longer on long ones.\n  const valueKey = value ? dateKey(value) : \"\";\n  const [last, setLast] = useState({ key: valueKey, date: value });\n  const [handoff, setHandoff] = useState({ from: \"\", to: \"\", distance: 0 });\n  if (last.key !== valueKey) {\n    const days =\n      last.date && value ? Math.round((startOfDay(value).getTime() - startOfDay(last.date).getTime()) / 864e5) : 0;\n    const columns = last.date && value ? value.getDay() - last.date.getDay() : 0;\n    setLast({ key: valueKey, date: value });\n    setHandoff({ from: last.key, to: valueKey, distance: Math.hypot(columns, (days - columns) / 7) });\n  }\n  const numberDelay = (key: string): CSSProperties | undefined => {\n    if (!handoff.distance || reducedMotion || (key !== handoff.to && key !== handoff.from)) return undefined;\n    const delay =\n      key === handoff.to ? Math.min(260, 95 + 85 * Math.log(handoff.distance)) : Math.max(20, 130 / handoff.distance);\n    return { \"--number-delay\": `${Math.round(delay)}ms` } as CSSProperties;\n  };\n\n  const isDisabled = (date: Date) =>\n    isBefore(date, minDate) || isAfter(date, maxDate) || Boolean(disabledDates?.(date));\n  const previousMonth = month && addMonths(month, -1);\n  const nextMonth = month && addMonths(month, 1);\n  // While the month is still unknown the controls keep their resting look, so hydration does not flash them dim.\n  const previousDisabled = Boolean(previousMonth && minDate && previousMonth.getTime() < monthStart(minDate).getTime());\n  const nextDisabled = Boolean(nextMonth && maxDate && nextMonth.getTime() > monthStart(maxDate).getTime());\n  const todaySelectable = Boolean(today && !isDisabled(today) && onChange);\n  const todayIdle = Boolean(today && sameMonth(month, today) && (!todaySelectable || sameDay(value, today)));\n  // Roving tab stop: the day last focused, else the selection, else today, else the first open day of the month.\n  const tabbableKey = (() => {\n    const open = (date?: Date) => (date && sameMonth(date, month) && !isDisabled(date) ? dateKey(date) : \"\");\n    return (\n      open(focusedDate) ||\n      open(value) ||\n      open(today) ||\n      dateKey(weeks.flat().find((date) => sameMonth(date, month) && !isDisabled(date)) ?? new Date(0))\n    );\n  })();\n\n  const changeMonth = (target: Date) => {\n    const normalized = monthStart(target);\n    if (!controlledMonth) setInternalMonth(normalized);\n    onMonthChange?.(normalized);\n  };\n\n  /** Keyboard focus lands on the new day in the same frame the month starts to slide. */\n  useLayoutEffect(() => {\n    const key = focusRequest.current;\n    if (!key) return;\n    const button = viewportRef.current?.querySelector<HTMLButtonElement>(`[data-present] [data-date=\"${key}\"]`);\n    if (button) {\n      focusRequest.current = null;\n      button.focus({ preventScroll: true });\n    }\n  });\n\n  /** Clamps a keyboard move to the allowed range and steps past blocked days in the direction of travel. */\n  const moveFocus = (from: Date, target: Date, step: number) => {\n    let next =\n      isBefore(target, minDate) && minDate\n        ? startOfDay(minDate)\n        : isAfter(target, maxDate) && maxDate\n          ? startOfDay(maxDate)\n          : target;\n    for (let tries = 0; tries < 42 && isDisabled(next); tries += 1) next = addDays(next, step);\n    if (isDisabled(next) || sameDay(next, from)) return;\n    setFocusedDate(next);\n    focusRequest.current = dateKey(next);\n    if (!sameMonth(next, month)) changeMonth(next);\n  };\n\n  const onDayKeyDown = (event: KeyboardEvent<HTMLButtonElement>, date: Date) => {\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      if (!isDisabled(date)) onChange?.(date);\n      return;\n    }\n    const moves: Record<string, [Date, number]> = {\n      ArrowLeft: [addDays(date, -1), -1],\n      ArrowRight: [addDays(date, 1), 1],\n      ArrowUp: [addDays(date, -7), -1],\n      ArrowDown: [addDays(date, 7), 1],\n      Home: [addDays(date, -date.getDay()), 1],\n      End: [addDays(date, 6 - date.getDay()), -1],\n      PageUp: [shiftMonths(date, event.shiftKey ? -12 : -1), -1],\n      PageDown: [shiftMonths(date, event.shiftKey ? 12 : 1), 1],\n    };\n    const move = moves[event.key];\n    if (!move) return;\n    event.preventDefault();\n    moveFocus(date, move[0], move[1]);\n  };\n\n  const goToToday = () => {\n    if (!today || !month || todayIdle) return;\n    setFocusedDate(today);\n    if (!sameMonth(today, month)) changeMonth(today);\n    if (todaySelectable && !sameDay(value, today)) onChange?.(today);\n  };\n\n  // Six fixed weeks per month, so a day's cell is its distance from the first day on the grid.\n  const cellOf = (paneMonth: Date) => {\n    if (!value) return -1;\n    const first = addDays(paneMonth, -paneMonth.getDay());\n    const index = Math.round((startOfDay(value).getTime() - first.getTime()) / 864e5);\n    return index >= 0 && index < 42 ? index : -1;\n  };\n\n  const renderMonth = (paneMonth: Date, present: boolean) => (\n    <>\n      <SelectionDisc cell={cellOf(paneMonth)} reduced={reducedMotion} />\n      <div className=\"grid gap-y-1\" role=\"grid\" aria-label={formatter.format(paneMonth)}>\n        {makeWeeks(paneMonth).map((week) => (\n          <div key={dateKey(week[0]!)} className={weekGrid} role=\"row\">\n            {week.map((date) => {\n              const key = dateKey(date);\n              const selected = sameDay(date, value);\n              const isToday = sameDay(date, today);\n              const outside = !sameMonth(date, paneMonth);\n              const disabled = isDisabled(date);\n              return (\n                <button\n                  key={key}\n                  type=\"button\"\n                  role=\"gridcell\"\n                  data-date={key}\n                  aria-label={date.toLocaleDateString(locale, { dateStyle: \"full\" })}\n                  aria-selected={selected}\n                  aria-current={isToday ? \"date\" : undefined}\n                  tabIndex={present && key === tabbableKey ? 0 : -1}\n                  disabled={disabled}\n                  style={present ? numberDelay(key) : undefined}\n                  className={cn(\n                    dayClass,\n                    (selected || isToday) && \"font-medium\",\n                    disabled ? \"text-ink-3\" : selected ? \"text-on-accent\" : outside ? \"text-ink-3\" : \"text-ink\",\n                    !selected && \"enabled:hover:before:bg-hover\",\n                  )}\n                  onFocus={() => setFocusedDate(date)}\n                  onKeyDown={(event) => onDayKeyDown(event, date)}\n                  onClick={() => onChange?.(date)}\n                >\n                  <span\n                    className={cn(\n                      dayNumberClass,\n                      isToday && todayDot,\n                      disabled ? (outside ? \"opacity-30\" : \"opacity-[.42]\") : outside && !selected && \"opacity-80\",\n                    )}\n                  >\n                    {date.getDate()}\n                  </span>\n                </button>\n              );\n            })}\n          </div>\n        ))}\n      </div>\n    </>\n  );\n\n  return (\n    <section className={cn(\"w-[min(100%,328px)] min-w-0 text-ink\", className)} aria-labelledby={titleId}>\n      <div className=\"@container mb-3.5 grid min-h-[34px] grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1\">\n        <h2\n          id={titleId}\n          className=\"m-0 min-w-0 font-body text-base leading-body font-medium tracking-body @max-[260px]:col-span-full\"\n        >\n          <span className=\"sr-only\">{monthLabel}</span>\n          <span className=\"grid leading-body\" aria-hidden=\"true\">\n            <AnimatePresence initial={false} custom={direction}>\n              {month && (\n                <motion.span\n                  key={monthKey}\n                  className=\"inline-block justify-self-start whitespace-nowrap tabular-nums [grid-area:1/1]\"\n                  custom={direction}\n                  variants={reducedMotion || !direction ? titleFade : titleSlide}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                >\n                  {monthLabel}\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </span>\n        </h2>\n        <span className=\"sr-only\" aria-live=\"polite\">\n          {direction ? monthLabel : \"\"}\n        </span>\n        <div className=\"-mr-1.5 inline-flex flex-[0_0_auto] items-center gap-0.5 @max-[260px]:col-span-full @max-[260px]:justify-self-end\">\n          {showToday && (\n            <button\n              type=\"button\"\n              className={cn(headerButton, \"px-[11px] text-sm leading-none font-medium\")}\n              aria-disabled={todayIdle || undefined}\n              aria-label={today ? `Today, ${today.toLocaleDateString(locale, { dateStyle: \"full\" })}` : \"Today\"}\n              onClick={goToToday}\n            >\n              Today\n            </button>\n          )}\n          <button\n            type=\"button\"\n            className={cn(headerButton, \"w-8\")}\n            aria-label=\"Previous month\"\n            aria-disabled={previousDisabled || undefined}\n            onClick={() => {\n              if (!previousDisabled && previousMonth) changeMonth(previousMonth);\n            }}\n          >\n            <ChevronLeft size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n          </button>\n          <button\n            type=\"button\"\n            className={cn(headerButton, \"w-8\")}\n            aria-label=\"Next month\"\n            aria-disabled={nextDisabled || undefined}\n            onClick={() => {\n              if (!nextDisabled && nextMonth) changeMonth(nextMonth);\n            }}\n          >\n            <ChevronRight size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n      <div\n        className={cn(\n          weekGrid,\n          \"mb-1.5 text-center text-xs leading-6 font-medium text-ink-3 *:min-w-0 *:overflow-hidden\",\n        )}\n        aria-hidden=\"true\"\n      >\n        {weekdays.map((day, index) => (\n          <span key={`${day}-${index}`}>{day.slice(0, 2)}</span>\n        ))}\n      </div>\n      <div className=\"relative grid overflow-hidden [contain:paint]\" ref={viewportRef}>\n        {month ? (\n          paneIndexes.map((index) => (\n            <MonthPane key={index} index={index} position={position} present={index === target}>\n              {renderMonth(fromIndex(index), index === target)}\n            </MonthPane>\n          ))\n        ) : (\n          <div className={monthBody} aria-hidden=\"true\">\n            <div className=\"grid gap-y-1\">\n              {Array.from({ length: 6 }, (_, week) => (\n                <div key={week} className={weekGrid}>\n                  {Array.from({ length: 7 }, (_, day) => (\n                    <span key={day} className=\"aspect-square\" />\n                  ))}\n                </div>\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </section>\n  );\n}\n\nexport { addDays, addMonths, sameDay, startOfDay };\n",
      "type": "registry:component",
      "target": "@components/iris/calendar/calendar.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/calendar",
  "meta": {
    "kind": "component",
    "group": "Inputs",
    "export": "Calendar",
    "tags": [
      "date",
      "field"
    ],
    "docs": "https://irisui.dev/components/calendar"
  }
}
