{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "author": "Tori Bryan",
  "description": "A month grid for picking a day or a range, with full keyboard use; pages side by side, scrolls on phones, or fills a page with a month of events.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://fibo.toribryan.com/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/calendar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  addDays,\n  addMonths,\n  clampDate,\n  countDays,\n  differenceInMonths,\n  endOfWeek,\n  endOfMonth,\n  formatDate,\n  formatDateRange,\n  formatTime,\n  formatTimeRange,\n  getMonthWeeks,\n  isSameDay,\n  isSameMonth,\n  orderRange,\n  startOfDay,\n  startOfMonth,\n  startOfWeek,\n  toDateKey,\n  type DateRange,\n  type DraftRange,\n  type WeekStart,\n} from \"@/lib/dates\"\nimport { mergeRefs } from \"@/lib/merge-refs\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * Today is read on the client only. The server snapshot is null, so markup\n * rendered on a server never depends on its clock or time zone, and the\n * subscription wakes once at local midnight so the marker moves on its own.\n */\nfunction subscribeToMidnight(onChange: () => void) {\n  let timer: ReturnType<typeof setTimeout>\n  const schedule = () => {\n    const now = new Date()\n    const midnight = addDays(startOfDay(now), 1)\n    timer = setTimeout(\n      () => {\n        onChange()\n        schedule()\n      },\n      midnight.getTime() - now.getTime() + 50\n    )\n  }\n  schedule()\n  return () => clearTimeout(timer)\n}\n\nconst getToday = () => startOfDay(new Date()).getTime()\nconst getServerToday = () => null\n\n/** Local midnight today, or null while rendering on the server. */\nfunction useToday(): Date | null {\n  const time = React.useSyncExternalStore(\n    subscribeToMidnight,\n    getToday,\n    getServerToday\n  )\n  return React.useMemo(() => (time === null ? null : new Date(time)), [time])\n}\n\n// A year either side covers most bookings and reports without a scroll\n// position that runs for meters.\nconst SCROLL_SPAN = 12\n\ntype CalendarType = \"paged\" | \"scroll\" | \"month\"\ntype CalendarSize = \"default\" | \"lg\"\n\n// Seven day cells: 36px each, or 44px at the large size.\nconst MONTH_WIDTH: Record<CalendarSize, number> = { default: 252, lg: 308 }\n\n// A month cell keeps to three lines of events: two cards, then \"+N more\".\nconst MONTH_CARDS = 2\nconst MONTH_DOTS = 3\n\n/** One event on one day, for the month type. */\ntype CalendarEvent = {\n  /** Unique among the events given. */\n  id: string\n  /** What the event is called. */\n  title: string\n  /** When it starts. The event shows on this day. */\n  start: Date\n  /** When it ends, on the same day. */\n  end?: Date\n  /** Shows \"All day\" in place of a time. */\n  allDay?: boolean\n}\n\ntype CalendarLabels = {\n  /** Names the previous month button. */\n  previous: string\n  /** Names the next month button. */\n  next: string\n  /** The button that returns to today's month. */\n  today: string\n  /** Ends a month cell's cards when it has more events than fit. */\n  more: (count: number) => string\n  /** Fills the selected day's list when it has no events. */\n  noEvents: string\n  /** Stands in for the time of an all-day event. */\n  allDay: string\n  /** Opens a day's spoken summary of its events. */\n  events: (count: number) => string\n}\n\nconst DEFAULT_LABELS: CalendarLabels = {\n  previous: \"Previous month\",\n  next: \"Next month\",\n  today: \"Today\",\n  more: (count) => `+${count} more`,\n  noEvents: \"No events\",\n  allDay: \"All day\",\n  events: (count) => `${count} ${count === 1 ? \"event\" : \"events\"}`,\n}\n\nlet warnedRangeMonth = false\n\ntype CalendarSharedProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"onChange\" | \"defaultValue\" | \"children\"\n> & {\n  /** Day cells at 36 pixels, or 44 for touch. Paged and scroll types. */\n  size?: CalendarSize\n  /** Months shown side by side in the paged type. */\n  months?: 1 | 2\n  /** The first month shown, when you control it. */\n  month?: Date\n  /** The first month shown at first. Defaults to the selected day's month, then today's. */\n  defaultMonth?: Date\n  /** Called with the first month shown when it changes. */\n  onMonthChange?: (month: Date) => void\n  /** The earliest day that can be picked. */\n  minDate?: Date | null\n  /** The latest day that can be picked. */\n  maxDate?: Date | null\n  /** Return true for a day that can't be picked, such as a weekend. */\n  isDateDisabled?: (date: Date) => boolean\n  /** The first column: 0 for Sunday, 1 for Monday. */\n  weekStartsOn?: WeekStart\n  /** A BCP 47 locale for month and weekday names, day numbers and times. */\n  locale?: string\n  /** Month type: the events to show, each on the day it starts. */\n  events?: CalendarEvent[]\n  /** Month type: makes the selected day's events buttons, called with the one pressed. */\n  onEventClick?: (event: CalendarEvent) => void\n  /** Month type: actions at the end of the header, such as a New event button. Other types ignore them. */\n  children?: React.ReactNode\n  /** Wording the calendar writes for itself, for translation. */\n  labels?: Partial<CalendarLabels>\n}\n\ntype CalendarSingleProps = CalendarSharedProps & {\n  /** \"paged\" shows whole months with previous and next buttons; \"scroll\" stacks a year either side in one vertical scroll, for phones; \"month\" fills its parent with one month of events. */\n  type?: CalendarType\n  /** \"single\" picks one day; \"range\" picks a start and an end. */\n  mode?: \"single\"\n  /** The selected day, when you control it. */\n  value?: Date | null\n  /** The selected day at first, when the calendar keeps its own state. */\n  defaultValue?: Date | null\n  /** Called with the day picked. */\n  onChange?: (date: Date) => void\n}\n\ntype CalendarRangeProps = CalendarSharedProps & {\n  /** \"paged\" or \"scroll\". The month type picks one day only; given it anyway, a range calendar draws the paged type and warns in the console. */\n  type?: Exclude<CalendarType, \"month\">\n  /** \"single\" picks one day; \"range\" picks a start and an end. */\n  mode: \"range\"\n  /** The range, when you control it. `to` is missing between the two picks. */\n  value?: DraftRange | null\n  /** The range at first, when the calendar keeps its own state. */\n  defaultValue?: DraftRange | null\n  /** Called after each pick: with only `from` after the first, with both after the second. */\n  onChange?: (range: DraftRange) => void\n}\n\ntype CalendarProps = CalendarSingleProps | CalendarRangeProps\n\nfunction Calendar(props: CalendarProps) {\n  const {\n    mode = \"single\",\n    value,\n    defaultValue,\n    onChange,\n    type: typeProp = \"paged\",\n    size = \"default\",\n    months: monthsProp = 1,\n    month: monthProp,\n    defaultMonth,\n    onMonthChange,\n    minDate,\n    maxDate,\n    isDateDisabled,\n    weekStartsOn = 0,\n    locale = \"en-US\",\n    events,\n    onEventClick,\n    children,\n    labels,\n    className,\n    style,\n    ref,\n    ...rest\n  } = props\n  const range = mode === \"range\"\n  // A month of events picks one day, and a range caller's onChange expects\n  // a range, so a range keeps the paged grid rather than losing its contract.\n  const rangeMonth = range && typeProp === \"month\"\n  const type: CalendarType = rangeMonth ? \"paged\" : typeProp\n  const monthCount = type === \"month\" ? 1 : monthsProp\n  const text = { ...DEFAULT_LABELS, ...labels }\n  React.useEffect(() => {\n    if (!rangeMonth || warnedRangeMonth) return\n    warnedRangeMonth = true\n    console.warn(\n      'Calendar: type=\"month\" picks one day, so mode=\"range\" draws the paged type instead.'\n    )\n  }, [rangeMonth])\n  const today = useToday()\n  const root = React.useRef<HTMLDivElement>(null)\n  // A caller's ref would otherwise replace the one keyboard focus moves by.\n  const rootRef = React.useMemo(() => mergeRefs(root, ref), [ref])\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n  const focusPending = React.useRef(false)\n\n  const [ownValue, setOwnValue] = React.useState<Date | DraftRange | null>(\n    defaultValue ?? null\n  )\n  const current = value !== undefined ? value : ownValue\n  const selection: DraftRange | null =\n    current == null\n      ? null\n      : current instanceof Date\n        ? { from: startOfDay(current), to: startOfDay(current) }\n        : current\n  const anchor = range && selection && !selection.to ? selection.from : null\n\n  const [ownMonth, setOwnMonth] = React.useState<Date | null>(() => {\n    const start = defaultMonth ?? selection?.from\n    return start ? startOfMonth(start) : null\n  })\n  // Without a month or a selection, open on today, kept inside the bounds so\n  // the first page is never wholly disabled.\n  const month = monthProp\n    ? startOfMonth(monthProp)\n    : (ownMonth ??\n      (today ? startOfMonth(clampDate(today, minDate, maxDate)) : null))\n  const [slide, setSlide] = React.useState<\"next\" | \"previous\" | null>(null)\n\n  const [ownFocus, setOwnFocus] = React.useState<Date | null>(null)\n  const [hovered, setHovered] = React.useState<Date | null>(null)\n\n  const minMonth = minDate ? startOfMonth(minDate) : null\n  const maxMonth = maxDate ? startOfMonth(maxDate) : null\n\n  const shownMonths: Date[] = []\n  if (month) {\n    if (type === \"scroll\") {\n      let first = addMonths(month, -SCROLL_SPAN)\n      let last = addMonths(month, SCROLL_SPAN)\n      if (minMonth && first < minMonth) first = minMonth\n      if (maxMonth && last > maxMonth) last = maxMonth\n      for (let m = first; m <= last; m = addMonths(m, 1)) shownMonths.push(m)\n    } else {\n      for (let i = 0; i < monthCount; i++) shownMonths.push(addMonths(month, i))\n    }\n  }\n  const firstShown = shownMonths[0]\n  const lastShown = shownMonths[shownMonths.length - 1]\n  const isShown = (day: Date | null | undefined): day is Date =>\n    !!day &&\n    !!firstShown &&\n    !!lastShown &&\n    differenceInMonths(day, firstShown) >= 0 &&\n    differenceInMonths(day, lastShown) <= 0\n\n  const outOfBounds = (day: Date) =>\n    (!!minDate && day < startOfDay(minDate)) ||\n    (!!maxDate && day > startOfDay(maxDate))\n  const isDisabled = (day: Date) => outOfBounds(day) || !!isDateDisabled?.(day)\n\n  // One day in the grid holds the tab stop. It falls back through the\n  // selection and today to the first day on screen that can be picked, then\n  // to any day on screen, so the grid always has one.\n  const firstEnabled = (() => {\n    if (!firstShown || !lastShown) return null\n    const end = addMonths(lastShown, 1)\n    for (let day = firstShown; day < end; day = addDays(day, 1)) {\n      if (!isDisabled(day)) return day\n    }\n    return firstShown\n  })()\n  const focused =\n    [ownFocus, selection?.from, selection?.to, today].find(isShown) ??\n    firstEnabled\n\n  // While the second day is being chosen, the day under the pointer or the\n  // keyboard previews where the range would end.\n  const previewEnd = anchor && hovered ? hovered : null\n  const shown: DateRange | null = selection\n    ? selection.to\n      ? orderRange(selection.from, selection.to)\n      : previewEnd\n        ? orderRange(selection.from, previewEnd)\n        : { from: selection.from, to: selection.from }\n    : null\n\n  function changeMonth(next: Date, direction: \"next\" | \"previous\") {\n    setSlide(direction)\n    if (!monthProp) setOwnMonth(next)\n    onMonthChange?.(next)\n  }\n\n  function commit(next: Date | DraftRange) {\n    if (value === undefined) setOwnValue(next)\n    ;(onChange as ((next: Date | DraftRange) => void) | undefined)?.(next)\n  }\n\n  function pick(day: Date) {\n    if (isDisabled(day)) return\n    setOwnFocus(day)\n    // A month view shows the ends of the months either side; picking one of\n    // those days turns the page to it, as moving there by keyboard does.\n    if (type === \"month\" && month && !isSameMonth(day, month)) {\n      changeMonth(startOfMonth(day), day < month ? \"previous\" : \"next\")\n    }\n    if (!range) commit(day)\n    else if (!anchor) commit({ from: day })\n    else {\n      setHovered(null)\n      commit(orderRange(anchor, day))\n    }\n  }\n\n  function moveFocus(target: Date) {\n    let next = clampDate(target, minDate, maxDate)\n    if (type === \"scroll\" && firstShown && lastShown) {\n      next = clampDate(next, firstShown, addMonths(lastShown, 1))\n      if (differenceInMonths(next, lastShown) > 0) next = addDays(next, -1)\n    }\n    focusPending.current = true\n    setOwnFocus(next)\n    if (type !== \"scroll\" && month) {\n      const offset = differenceInMonths(next, month)\n      if (offset < 0) changeMonth(startOfMonth(next), \"previous\")\n      else if (offset > monthCount - 1)\n        changeMonth(addMonths(startOfMonth(next), 1 - monthCount), \"next\")\n    }\n  }\n\n  function onGridKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n    if (!focused) return\n    const moves: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(focused, -1),\n      ArrowRight: () => addDays(focused, 1),\n      ArrowUp: () => addDays(focused, -7),\n      ArrowDown: () => addDays(focused, 7),\n      Home: () => startOfWeek(focused, weekStartsOn),\n      End: () => endOfWeek(focused, weekStartsOn),\n      PageUp: () => addMonths(focused, event.shiftKey ? -12 : -1),\n      PageDown: () => addMonths(focused, event.shiftKey ? 12 : 1),\n    }\n    const move = moves[event.key]\n    if (!move) return\n    event.preventDefault()\n    moveFocus(move())\n  }\n\n  // Keyboard moves land focus once the target day has rendered, which may\n  // be in a month that only just slid in.\n  const focusedKey = focused ? toDateKey(focused) : \"\"\n  const monthKey = month ? toDateKey(month) : \"\"\n  React.useEffect(() => {\n    if (!focusPending.current) return\n    focusPending.current = false\n    root.current\n      ?.querySelector<HTMLElement>(\n        `[data-slot=\"calendar-day\"][data-date=\"${focusedKey}\"]:not([data-outside])`\n      )\n      ?.focus()\n  }, [focusedKey, monthKey])\n\n  // A scroll calendar opens on the month holding the focused day, not on\n  // the oldest month a year back. A hidden scroller, such as one in a closed\n  // tab, has no layout and loses its position, so it scrolls again each time\n  // it's shown. Today is only known after hydration, so with no month to\n  // open on it scrolls once more when the months arrive, and never again\n  // while someone is scrolling.\n  const hasMonth = !!month\n  React.useLayoutEffect(() => {\n    const scroller = scrollRef.current\n    if (!scroller) return\n    const scroll = () => {\n      const target = scroller.querySelector<HTMLElement>(\"[data-focus-month]\")\n      if (target) scroller.scrollTop = target.offsetTop\n    }\n    let hidden = scroller.clientHeight === 0\n    if (!hidden) scroll()\n    const observer = new ResizeObserver(() => {\n      if (scroller.clientHeight === 0) hidden = true\n      else if (hidden) {\n        hidden = false\n        scroll()\n      }\n    })\n    observer.observe(scroller)\n    return () => observer.disconnect()\n  }, [hasMonth])\n\n  const formats = React.useMemo(\n    () => ({\n      month: new Intl.DateTimeFormat(locale, {\n        month: \"long\",\n        year: \"numeric\",\n      }),\n      day: new Intl.DateTimeFormat(locale, { day: \"numeric\" }),\n      full: new Intl.DateTimeFormat(locale, {\n        weekday: \"long\",\n        month: \"long\",\n        day: \"numeric\",\n        year: \"numeric\",\n      }),\n      weekdayShort: new Intl.DateTimeFormat(locale, { weekday: \"short\" }),\n      weekdayLong: new Intl.DateTimeFormat(locale, { weekday: \"long\" }),\n    }),\n    [locale]\n  )\n  // Any week works for the names; this one starts on a Sunday.\n  const weekdays = Array.from({ length: 7 }, (_, i) => {\n    const day = new Date(2026, 0, 4 + ((i + weekStartsOn) % 7))\n    return {\n      short: formats.weekdayShort.format(day),\n      long: formats.weekdayLong.format(day),\n    }\n  })\n\n  let status = \"\"\n  if (anchor) {\n    status = `Start ${formatDate(anchor, locale)}. Choose an end date.`\n  } else if (range && selection?.to) {\n    const picked = orderRange(selection.from, selection.to)\n    const days = countDays(picked)\n    status = `${formatDateRange(picked, locale)}, ${days} ${days === 1 ? \"day\" : \"days\"}`\n  }\n\n  const atMin = !!minMonth && !!month && month <= minMonth\n  const atMax = !!maxMonth && !!lastShown && lastShown >= maxMonth\n\n  if (type === \"month\") {\n    return (\n      <CalendarMonthView\n        ref={rootRef}\n        month={month}\n        slide={slide}\n        weekStartsOn={weekStartsOn}\n        weekdays={weekdays}\n        formats={formats}\n        locale={locale}\n        text={text}\n        today={today}\n        focused={focused}\n        selected={selection?.from ?? null}\n        events={events}\n        onEventClick={onEventClick}\n        isDisabled={isDisabled}\n        atMin={atMin}\n        atMax={atMax}\n        todayDisabled={!today || outOfBounds(today)}\n        onPrevious={() =>\n          month && changeMonth(addMonths(month, -1), \"previous\")\n        }\n        onNext={() => month && changeMonth(addMonths(month, 1), \"next\")}\n        onToday={() => {\n          if (!today || !month) return\n          setOwnFocus(today)\n          if (!isSameMonth(today, month)) {\n            changeMonth(\n              startOfMonth(today),\n              today < month ? \"previous\" : \"next\"\n            )\n          }\n        }}\n        onPick={pick}\n        onKeyDown={onGridKeyDown}\n        actions={children}\n        className={className}\n        style={style}\n        {...rest}\n      />\n    )\n  }\n\n  const grids = shownMonths.map((m) => (\n    <CalendarMonth\n      key={toDateKey(m)}\n      month={m}\n      type={type}\n      size={size}\n      range={range}\n      weekStartsOn={weekStartsOn}\n      weekdays={weekdays}\n      formats={formats}\n      today={today}\n      focused={focused}\n      shown={shown}\n      selection={selection}\n      previewEnd={previewEnd}\n      isDisabled={isDisabled}\n      onPick={pick}\n      onHover={(day) => anchor && setHovered(day)}\n      onKeyDown={onGridKeyDown}\n      onLeave={() => setHovered(null)}\n    />\n  ))\n\n  if (type === \"scroll\") {\n    return (\n      <div\n        ref={rootRef}\n        data-slot=\"calendar\"\n        data-type=\"scroll\"\n        data-size={size}\n        className={cn(\"flex min-h-0 flex-col\", className)}\n        style={style}\n        {...rest}\n      >\n        <div\n          aria-hidden=\"true\"\n          data-slot=\"calendar-weekdays\"\n          className=\"grid shrink-0 grid-cols-7 border-b border-border px-4 py-2 text-center text-xs text-muted-foreground\"\n        >\n          {weekdays.map((w) => (\n            <span key={w.long}>{w.short}</span>\n          ))}\n        </div>\n        <div\n          ref={scrollRef}\n          data-slot=\"calendar-scroll\"\n          className=\"relative min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4\"\n        >\n          {grids}\n        </div>\n        <div aria-live=\"polite\" className=\"sr-only\">\n          {status}\n        </div>\n      </div>\n    )\n  }\n\n  // The months sit in a size container, so two of them stack when there\n  // isn't room side by side rather than squeezing their days. A size\n  // container can't take its width from its content, so it's told the width\n  // the months need: a popover still sizes to it, and a narrower column gets\n  // the stack. It has no padding of its own, so a caller's padding on the\n  // root can't throw the measurement off.\n  const pageWidth = monthCount * MONTH_WIDTH[size] + (monthCount - 1) * 24\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"calendar\"\n      data-type=\"paged\"\n      data-size={size}\n      className={cn(\"overflow-hidden p-1\", className)}\n      style={style}\n      {...rest}\n    >\n      <div\n        className=\"@container/calendar\"\n        style={{\n          maxWidth: pageWidth,\n          containIntrinsicInlineSize: `${pageWidth}px`,\n        }}\n      >\n        <div className=\"relative w-fit\">\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-previous\"\n            aria-label={text.previous}\n            disabled={!month || atMin}\n            onClick={() =>\n              month && changeMonth(addMonths(month, -1), \"previous\")\n            }\n            className=\"absolute top-0 left-0\"\n          >\n            <ChevronLeftIcon aria-hidden=\"true\" />\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-next\"\n            aria-label={text.next}\n            disabled={!month || atMax}\n            onClick={() => month && changeMonth(addMonths(month, 1), \"next\")}\n            className=\"absolute top-0 right-0\"\n          >\n            <ChevronRightIcon aria-hidden=\"true\" />\n          </Button>\n          <div\n            key={monthKey}\n            data-slot=\"calendar-months\"\n            data-slide={slide ?? undefined}\n            className={cn(\n              \"flex flex-col gap-6\",\n              monthCount > 1 &&\n                (size === \"lg\"\n                  ? \"@min-[640px]/calendar:flex-row\"\n                  : \"@min-[528px]/calendar:flex-row\"),\n              slide &&\n                \"animate-in duration-200 ease-out fade-in-0 motion-reduce:animate-none\",\n              slide === \"next\" && \"slide-in-from-right-6\",\n              slide === \"previous\" && \"slide-in-from-left-6\"\n            )}\n          >\n            {grids}\n          </div>\n        </div>\n      </div>\n      <div aria-live=\"polite\" className=\"sr-only\">\n        {status}\n      </div>\n    </div>\n  )\n}\n\ntype CalendarFormats = {\n  month: Intl.DateTimeFormat\n  day: Intl.DateTimeFormat\n  full: Intl.DateTimeFormat\n}\n\nfunction CalendarMonth({\n  month,\n  type,\n  size,\n  range,\n  weekStartsOn,\n  weekdays,\n  formats,\n  today,\n  focused,\n  shown,\n  selection,\n  previewEnd,\n  isDisabled,\n  onPick,\n  onHover,\n  onKeyDown,\n  onLeave,\n}: {\n  month: Date\n  type: CalendarType\n  size: CalendarSize\n  range: boolean\n  weekStartsOn: WeekStart\n  weekdays: { short: string; long: string }[]\n  formats: CalendarFormats\n  today: Date | null\n  focused: Date | null\n  shown: DateRange | null\n  selection: DraftRange | null\n  previewEnd: Date | null\n  isDisabled: (day: Date) => boolean\n  onPick: (day: Date) => void\n  onHover: (day: Date) => void\n  onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void\n  onLeave: () => void\n}) {\n  const titleId = React.useId()\n  const weeks = getMonthWeeks(month, weekStartsOn)\n  const holdsFocus =\n    !!focused && differenceInMonths(focused, month) === 0 ? true : undefined\n  const inShown = (day: Date) => !!shown && day >= shown.from && day <= shown.to\n  const complete = !!selection?.to || !!previewEnd\n\n  return (\n    <div\n      data-slot=\"calendar-month\"\n      data-focus-month={holdsFocus}\n      className={cn(\"flex flex-col gap-1\", type === \"scroll\" && \"pt-4\")}\n    >\n      <div\n        id={titleId}\n        data-slot=\"calendar-month-title\"\n        className={cn(\n          \"flex h-8 items-center text-sm font-medium\",\n          type === \"paged\" ? \"justify-center\" : \"px-1\"\n        )}\n      >\n        {formats.month.format(month)}\n      </div>\n      <div\n        role=\"grid\"\n        aria-labelledby={titleId}\n        aria-multiselectable={range || undefined}\n        data-slot=\"calendar-grid\"\n        onKeyDown={onKeyDown}\n        onPointerLeave={onLeave}\n      >\n        <div\n          role=\"row\"\n          className={cn(\"grid grid-cols-7\", type === \"scroll\" && \"sr-only\")}\n        >\n          {weekdays.map((w) => (\n            <div\n              key={w.long}\n              role=\"columnheader\"\n              aria-label={w.long}\n              className={cn(\n                \"flex h-8 items-center justify-center text-xs font-normal text-muted-foreground\",\n                size === \"lg\" ? \"w-11\" : \"w-9\"\n              )}\n            >\n              {w.short}\n            </div>\n          ))}\n        </div>\n        {weeks.map((week, row) => {\n          // One bar per week row, from the center of the first day in range\n          // to the center of the last. Where the range runs on past the row,\n          // the bar runs to the row's edge so it reads as one band.\n          const cols = week.flatMap((day, i) => (day && inShown(day) ? i : []))\n          const first = cols[0]\n          const last = cols[cols.length - 1]\n          const bar =\n            range &&\n            shown &&\n            complete &&\n            first !== undefined &&\n            last !== undefined\n              ? {\n                  start:\n                    week[first] && isSameDay(week[first], shown.from)\n                      ? first + 0.5\n                      : first,\n                  end:\n                    week[last] && isSameDay(week[last], shown.to)\n                      ? last + 0.5\n                      : last + 1,\n                }\n              : null\n          return (\n            <div\n              key={row}\n              role=\"row\"\n              className={cn(\n                \"relative grid grid-cols-7\",\n                size === \"lg\" && \"py-0.5\"\n              )}\n            >\n              {bar && bar.end > bar.start ? (\n                <div\n                  aria-hidden=\"true\"\n                  data-slot=\"calendar-range-bar\"\n                  data-preview={!selection?.to || undefined}\n                  className={cn(\n                    \"pointer-events-none absolute bg-secondary\",\n                    size === \"lg\" ? \"inset-y-0.5\" : \"inset-y-0\"\n                  )}\n                  style={{\n                    left: `${(bar.start / 7) * 100}%`,\n                    width: `${((bar.end - bar.start) / 7) * 100}%`,\n                  }}\n                />\n              ) : null}\n              {week.map((day, col) => {\n                if (!day) return <div key={col} role=\"gridcell\" />\n                const key = toDateKey(day)\n                const start = !!shown && isSameDay(day, shown.from)\n                const end = !!shown && isSameDay(day, shown.to)\n                const thumb =\n                  !!selection &&\n                  (isSameDay(day, selection.from) ||\n                    (!!selection.to && isSameDay(day, selection.to)))\n                const selected =\n                  thumb || (inShown(day) && (!!selection?.to || !range))\n                const preview =\n                  !!previewEnd && isSameDay(day, previewEnd) && !thumb\n                const disabled = isDisabled(day)\n                const isToday = !!today && isSameDay(day, today)\n                return (\n                  <div\n                    key={key}\n                    role=\"gridcell\"\n                    aria-selected={selected}\n                    className=\"flex justify-center\"\n                  >\n                    <button\n                      type=\"button\"\n                      data-slot=\"calendar-day\"\n                      data-date={key}\n                      data-today={isToday || undefined}\n                      data-selected={thumb || undefined}\n                      data-range-start={(range && start) || undefined}\n                      data-range-end={(range && end) || undefined}\n                      data-in-range={\n                        (range && inShown(day) && !start && !end) || undefined\n                      }\n                      data-preview={preview || undefined}\n                      data-disabled={disabled || undefined}\n                      tabIndex={focused && isSameDay(day, focused) ? 0 : -1}\n                      aria-label={formats.full.format(day)}\n                      aria-current={isToday ? \"date\" : undefined}\n                      aria-disabled={disabled || undefined}\n                      onClick={() => onPick(day)}\n                      onPointerEnter={() => onHover(day)}\n                      onFocus={() => onHover(day)}\n                      className={cn(\n                        \"relative z-10 inline-flex items-center justify-center rounded-full border border-transparent text-sm tabular-nums transition-colors outline-none hover:bg-secondary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring-subtle\",\n                        size === \"lg\" ? \"size-11 text-base\" : \"size-9\",\n                        \"data-in-range:hover:bg-secondary-hover\",\n                        \"data-preview:border-primary\",\n                        \"data-today:font-semibold data-today:after:absolute data-today:after:bottom-1 data-today:after:size-1 data-today:after:rounded-full data-today:after:bg-current\",\n                        \"data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:bg-primary-hover\",\n                        \"aria-disabled:cursor-not-allowed aria-disabled:text-muted-foreground aria-disabled:line-through aria-disabled:opacity-50 aria-disabled:hover:bg-transparent\"\n                      )}\n                    >\n                      {formats.day.format(day)}\n                    </button>\n                  </div>\n                )\n              })}\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\n/*\n * The month type. The grid keeps the date grid's keyboard model: each cell\n * holds one day button, the grid is one tab stop, and arrows move between\n * days. A cell's event cards and dots sit inside that button, hidden from\n * assistive technology, and the button is described by a spoken list of the\n * day's events instead, so nothing inside a cell takes focus or breaks the\n * grid. Events become buttons in the selected day's list, outside the grid,\n * which sits below the grid in a narrow calendar and beside it in a wide one.\n * Which layout shows is decided by the calendar's own width, not the\n * viewport's, so a month in a side panel gets the compact cells.\n */\nfunction CalendarMonthView({\n  ref,\n  month,\n  slide,\n  weekStartsOn,\n  weekdays,\n  formats,\n  locale,\n  text,\n  today,\n  focused,\n  selected,\n  events,\n  onEventClick,\n  isDisabled,\n  atMin,\n  atMax,\n  todayDisabled,\n  onPrevious,\n  onNext,\n  onToday,\n  onPick,\n  onKeyDown,\n  actions,\n  className,\n  ...rest\n}: Omit<React.ComponentProps<\"div\">, \"onKeyDown\"> & {\n  month: Date | null\n  slide: \"next\" | \"previous\" | null\n  weekStartsOn: WeekStart\n  weekdays: { short: string; long: string }[]\n  formats: CalendarFormats\n  locale: string\n  text: CalendarLabels\n  today: Date | null\n  focused: Date | null\n  selected: Date | null\n  events: CalendarEvent[] | undefined\n  onEventClick: ((event: CalendarEvent) => void) | undefined\n  isDisabled: (day: Date) => boolean\n  atMin: boolean\n  atMax: boolean\n  todayDisabled: boolean\n  onPrevious: () => void\n  onNext: () => void\n  onToday: () => void\n  onPick: (day: Date) => void\n  onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void\n  actions: React.ReactNode\n}) {\n  const id = React.useId()\n  const titleId = `${id}-title`\n  const agendaId = `${id}-agenda`\n\n  const byDay = React.useMemo(() => {\n    const map = new Map<string, CalendarEvent[]>()\n    for (const event of events ?? []) {\n      const key = toDateKey(event.start)\n      const list = map.get(key)\n      if (list) list.push(event)\n      else map.set(key, [event])\n    }\n    // All-day events first, then by start time.\n    for (const list of map.values()) {\n      list.sort(\n        (a, b) =>\n          Number(!!b.allDay) - Number(!!a.allDay) ||\n          a.start.getTime() - b.start.getTime()\n      )\n    }\n    return map\n  }, [events])\n\n  const agendaFormat = React.useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        weekday: \"long\",\n        month: \"long\",\n        day: \"numeric\",\n      }),\n    [locale]\n  )\n\n  const timeOf = (event: CalendarEvent) =>\n    event.allDay\n      ? text.allDay\n      : event.end\n        ? formatTimeRange(event.start, event.end, locale)\n        : formatTime(event.start, locale)\n\n  // Every day the weeks touch, the ends of the months either side included.\n  const weeks: Date[][] = []\n  if (month) {\n    const first = startOfWeek(month, weekStartsOn)\n    const rows = getMonthWeeks(month, weekStartsOn).length\n    for (let row = 0; row < rows; row++) {\n      weeks.push(\n        Array.from({ length: 7 }, (_, col) => addDays(first, row * 7 + col))\n      )\n    }\n  }\n\n  const agendaDay =\n    selected && month && isSameMonth(selected, month) ? selected : focused\n  const agendaEvents = agendaDay ? (byDay.get(toDateKey(agendaDay)) ?? []) : []\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"calendar\"\n      data-type=\"month\"\n      className={cn(\n        \"@container/calendar flex h-full min-h-0 w-full min-w-0 flex-1 flex-col gap-4\",\n        className\n      )}\n      {...rest}\n    >\n      <div\n        data-slot=\"calendar-header\"\n        className=\"flex flex-wrap items-center gap-x-4 gap-y-3\"\n      >\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <div\n            id={titleId}\n            data-slot=\"calendar-month-title\"\n            className=\"truncate text-lg font-semibold @3xl/calendar:text-xl\"\n          >\n            {month ? formats.month.format(month) : null}\n          </div>\n          <div\n            data-slot=\"calendar-month-span\"\n            className=\"text-sm text-muted-foreground\"\n          >\n            {month\n              ? formatDateRange({ from: month, to: endOfMonth(month) }, locale)\n              : null}\n          </div>\n        </div>\n        <div data-slot=\"calendar-nav\" className=\"flex\">\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-previous\"\n            aria-label={text.previous}\n            disabled={!month || atMin}\n            onClick={onPrevious}\n            className=\"rounded-r-none focus-visible:z-10\"\n          >\n            <ChevronLeftIcon aria-hidden=\"true\" />\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            data-slot=\"calendar-today\"\n            disabled={todayDisabled}\n            onClick={onToday}\n            className=\"-mx-px rounded-none focus-visible:z-10\"\n          >\n            {text.today}\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"icon-sm\"\n            data-slot=\"calendar-next\"\n            aria-label={text.next}\n            disabled={!month || atMax}\n            onClick={onNext}\n            className=\"rounded-l-none focus-visible:z-10\"\n          >\n            <ChevronRightIcon aria-hidden=\"true\" />\n          </Button>\n        </div>\n        {actions ? (\n          <div data-slot=\"calendar-actions\" className=\"flex items-center gap-2\">\n            {actions}\n          </div>\n        ) : null}\n      </div>\n      <div\n        data-slot=\"calendar-body\"\n        className=\"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto @5xl/calendar:flex-row @5xl/calendar:overflow-visible\"\n      >\n        <div\n          key={month ? toDateKey(month) : \"\"}\n          role=\"grid\"\n          aria-labelledby={titleId}\n          data-slot=\"calendar-grid\"\n          data-slide={slide ?? undefined}\n          onKeyDown={onKeyDown}\n          className={cn(\n            \"flex shrink-0 flex-col gap-px overflow-clip rounded-xl border border-border bg-border @3xl/calendar:flex-1 @5xl/calendar:min-h-0 @5xl/calendar:overflow-y-auto\",\n            slide &&\n              \"animate-in duration-200 ease-out fade-in-0 motion-reduce:animate-none\"\n          )}\n        >\n          <div role=\"row\" className=\"grid shrink-0 grid-cols-7 gap-px\">\n            {weekdays.map((w) => (\n              <div\n                key={w.long}\n                role=\"columnheader\"\n                aria-label={w.long}\n                className=\"flex h-8 items-center justify-center bg-background text-xs text-muted-foreground @3xl/calendar:justify-start @3xl/calendar:px-2\"\n              >\n                {w.short}\n              </div>\n            ))}\n          </div>\n          {weeks.map((week, row) => (\n            <div\n              key={row}\n              role=\"row\"\n              className=\"grid grid-cols-7 gap-px @3xl/calendar:flex-1\"\n            >\n              {week.map((day) => {\n                const key = toDateKey(day)\n                const dayEvents = byDay.get(key) ?? []\n                const outside = !!month && !isSameMonth(day, month)\n                const isSelected = !!selected && isSameDay(day, selected)\n                const isToday = !!today && isSameDay(day, today)\n                const disabled = isDisabled(day)\n                const extra =\n                  dayEvents.length > MONTH_CARDS\n                    ? dayEvents.length - MONTH_CARDS\n                    : 0\n                const summaryId = `${id}-${key}`\n                return (\n                  <div\n                    key={key}\n                    role=\"gridcell\"\n                    aria-selected={isSelected}\n                    className=\"relative flex min-w-0 bg-background\"\n                  >\n                    <button\n                      type=\"button\"\n                      data-slot=\"calendar-day\"\n                      data-date={key}\n                      data-today={isToday || undefined}\n                      data-selected={isSelected || undefined}\n                      data-outside={outside || undefined}\n                      data-disabled={disabled || undefined}\n                      data-events={dayEvents.length || undefined}\n                      tabIndex={\n                        !outside && focused && isSameDay(day, focused) ? 0 : -1\n                      }\n                      aria-label={formats.full.format(day)}\n                      aria-current={isToday ? \"date\" : undefined}\n                      aria-disabled={disabled || undefined}\n                      aria-describedby={\n                        dayEvents.length ? summaryId : undefined\n                      }\n                      onClick={() => onPick(day)}\n                      className=\"group/day flex min-h-12 min-w-0 flex-1 flex-col items-center gap-1 p-1 text-left outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring-subtle focus-visible:ring-inset aria-disabled:cursor-not-allowed aria-disabled:hover:bg-transparent @3xl/calendar:min-h-28 @3xl/calendar:items-stretch @3xl/calendar:p-1.5\"\n                    >\n                      <span\n                        data-slot=\"calendar-day-number\"\n                        className=\"flex size-7 shrink-0 items-center justify-center rounded-full border border-transparent text-sm tabular-nums group-data-outside/day:text-muted-foreground group-data-today/day:border-primary group-data-today/day:font-semibold group-data-selected/day:bg-primary group-data-selected/day:text-primary-foreground group-data-disabled/day:text-muted-foreground group-data-disabled/day:line-through @3xl/calendar:self-start\"\n                      >\n                        {formats.day.format(day)}\n                      </span>\n                      {dayEvents.length ? (\n                        <>\n                          <span\n                            aria-hidden=\"true\"\n                            data-slot=\"calendar-day-events\"\n                            className=\"hidden min-w-0 flex-col gap-0.5 @3xl/calendar:flex\"\n                          >\n                            {dayEvents.slice(0, MONTH_CARDS).map((event) => (\n                              <span\n                                key={event.id}\n                                data-slot=\"calendar-event-card\"\n                                className=\"flex min-w-0 flex-col rounded-sm bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground group-data-outside/day:text-muted-foreground\"\n                              >\n                                <span className=\"truncate font-medium\">\n                                  {event.title}\n                                </span>\n                                {event.allDay ? null : (\n                                  <span className=\"truncate text-muted-foreground tabular-nums\">\n                                    {formatTime(event.start, locale)}\n                                  </span>\n                                )}\n                              </span>\n                            ))}\n                            {extra ? (\n                              <span\n                                data-slot=\"calendar-event-more\"\n                                className=\"px-1.5 text-xs text-muted-foreground\"\n                              >\n                                {text.more(extra)}\n                              </span>\n                            ) : null}\n                          </span>\n                          <span\n                            aria-hidden=\"true\"\n                            data-slot=\"calendar-day-dots\"\n                            className=\"flex gap-0.5 @3xl/calendar:hidden\"\n                          >\n                            {dayEvents.slice(0, MONTH_DOTS).map((event) => (\n                              <span\n                                key={event.id}\n                                className=\"size-1 rounded-full bg-foreground group-data-outside/day:bg-muted-foreground\"\n                              />\n                            ))}\n                          </span>\n                        </>\n                      ) : null}\n                    </button>\n                    {dayEvents.length ? (\n                      <span id={summaryId} className=\"sr-only\">\n                        {`${text.events(dayEvents.length)}: ${dayEvents\n                          .map((event) => `${event.title}, ${timeOf(event)}`)\n                          .join(\"; \")}`}\n                      </span>\n                    ) : null}\n                  </div>\n                )\n              })}\n            </div>\n          ))}\n        </div>\n        <div\n          data-slot=\"calendar-agenda\"\n          className=\"flex flex-1 flex-col gap-2 @3xl/calendar:flex-none @5xl/calendar:min-h-0 @5xl/calendar:w-64 @5xl/calendar:overflow-y-auto\"\n        >\n          <div\n            id={agendaId}\n            data-slot=\"calendar-agenda-title\"\n            className=\"text-sm font-medium\"\n          >\n            {agendaDay ? agendaFormat.format(agendaDay) : null}\n          </div>\n          {agendaEvents.length ? (\n            <ul\n              aria-labelledby={agendaId}\n              data-slot=\"calendar-agenda-list\"\n              className=\"flex flex-col gap-1\"\n            >\n              {agendaEvents.map((event) => {\n                const content = (\n                  <>\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"w-1 shrink-0 self-stretch rounded-full bg-primary\"\n                    />\n                    <span className=\"flex min-w-0 flex-col\">\n                      <span className=\"truncate text-sm font-medium\">\n                        {event.title}\n                      </span>\n                      <span className=\"text-xs text-muted-foreground tabular-nums\">\n                        {timeOf(event)}\n                      </span>\n                    </span>\n                  </>\n                )\n                const rowClass =\n                  \"flex w-full gap-3 rounded-lg px-2 py-1.5 text-left\"\n                return (\n                  <li key={event.id} data-slot=\"calendar-event\">\n                    {onEventClick ? (\n                      <button\n                        type=\"button\"\n                        onClick={() => onEventClick(event)}\n                        className={cn(\n                          rowClass,\n                          \"outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring-subtle\"\n                        )}\n                      >\n                        {content}\n                      </button>\n                    ) : (\n                      <div className={rowClass}>{content}</div>\n                    )}\n                  </li>\n                )\n              })}\n            </ul>\n          ) : (\n            <p\n              data-slot=\"calendar-agenda-empty\"\n              className=\"px-2 py-1.5 text-sm text-muted-foreground\"\n            >\n              {text.noEvents}\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { Calendar, useToday }\nexport type {\n  CalendarEvent,\n  CalendarLabels,\n  CalendarProps,\n  CalendarRangeProps,\n  CalendarSingleProps,\n  CalendarSize,\n  CalendarType,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/lib/dates.ts",
      "content": "/*\n * Calendar-day arithmetic on plain Dates in the local time zone. Every date\n * here is a local midnight, so two dates are the same day exactly when their\n * year, month and day match, whatever the time zone or daylight saving.\n */\n\n/** A finished range: both ends picked, `from` on or before `to`. */\nexport type DateRange = { from: Date; to: Date }\n\n/** A range being picked: `to` is missing until the second day is chosen. */\nexport type DraftRange = { from: Date; to?: Date }\n\n/** The first column of the week: 0 for Sunday, 1 for Monday. */\nexport type WeekStart = 0 | 1\n\nexport function startOfDay(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate())\n}\n\nexport function startOfMonth(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth(), 1)\n}\n\nexport function endOfMonth(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth() + 1, 0)\n}\n\n// Built from the parts rather than by adding milliseconds, so a day across a\n// daylight-saving change is still one day.\nexport function addDays(date: Date, days: number): Date {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days)\n}\n\n/** Moves by whole months, keeping the day but never spilling past the end. */\nexport function addMonths(date: Date, months: number): Date {\n  const target = new Date(date.getFullYear(), date.getMonth() + months, 1)\n  const last = endOfMonth(target).getDate()\n  return new Date(\n    target.getFullYear(),\n    target.getMonth(),\n    Math.min(date.getDate(), last)\n  )\n}\n\nexport function isSameDay(a: Date, b: Date): boolean {\n  return (\n    a.getFullYear() === b.getFullYear() &&\n    a.getMonth() === b.getMonth() &&\n    a.getDate() === b.getDate()\n  )\n}\n\nexport function isSameMonth(a: Date, b: Date): boolean {\n  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()\n}\n\n/** Whole calendar days from `a` to `b`, negative when `b` comes first. */\nexport function differenceInDays(b: Date, a: Date): number {\n  const utc = (d: Date) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n  return Math.round((utc(b) - utc(a)) / 86_400_000)\n}\n\n/** Whole months from the month of `a` to the month of `b`. */\nexport function differenceInMonths(b: Date, a: Date): number {\n  return (\n    (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth())\n  )\n}\n\nexport function startOfWeek(date: Date, weekStartsOn: WeekStart = 0): Date {\n  const offset = (date.getDay() - weekStartsOn + 7) % 7\n  return addDays(date, -offset)\n}\n\nexport function endOfWeek(date: Date, weekStartsOn: WeekStart = 0): Date {\n  return addDays(startOfWeek(date, weekStartsOn), 6)\n}\n\n/** Keeps a day inside the bounds given, either of which may be missing. */\nexport function clampDate(date: Date, min?: Date | null, max?: Date | null) {\n  const day = startOfDay(date)\n  if (min && day < startOfDay(min)) return startOfDay(min)\n  if (max && day > startOfDay(max)) return startOfDay(max)\n  return day\n}\n\n/** Two days as a range, earliest first, whichever order they were picked. */\nexport function orderRange(a: Date, b: Date): DateRange {\n  const [from, to] = a <= b ? [a, b] : [b, a]\n  return { from: startOfDay(from), to: startOfDay(to) }\n}\n\n/**\n * The part of a range inside the bounds, or null when none of it is. A\n * preset such as \"Last 30 days\" still works next to a minimum date by\n * starting at that minimum.\n */\nexport function clampRange(\n  range: DateRange,\n  min?: Date | null,\n  max?: Date | null\n): DateRange | null {\n  if (min && startOfDay(range.to) < startOfDay(min)) return null\n  if (max && startOfDay(range.from) > startOfDay(max)) return null\n  return {\n    from: clampDate(range.from, min, max),\n    to: clampDate(range.to, min, max),\n  }\n}\n\n/** Days in a range, counting both ends: Oct 1 to Oct 7 is 7 days. */\nexport function countDays(range: DateRange): number {\n  return Math.abs(differenceInDays(range.to, range.from)) + 1\n}\n\n/**\n * A month as rows of seven, starting on `weekStartsOn`. Days from the\n * months either side are null, so a grid can leave those cells empty.\n */\nexport function getMonthWeeks(\n  month: Date,\n  weekStartsOn: WeekStart = 0\n): (Date | null)[][] {\n  const first = startOfMonth(month)\n  const last = endOfMonth(month)\n  const weeks: (Date | null)[][] = []\n  let day = startOfWeek(first, weekStartsOn)\n  while (day <= last) {\n    const week: (Date | null)[] = []\n    for (let i = 0; i < 7; i++) {\n      week.push(isSameMonth(day, first) ? day : null)\n      day = addDays(day, 1)\n    }\n    weeks.push(week)\n  }\n  return weeks\n}\n\n/** A stable key for a day, such as `2026-10-05`, for lookups and data attributes. */\nexport function toDateKey(date: Date): string {\n  const pad = (n: number) => String(n).padStart(2, \"0\")\n  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`\n}\n\n/*\n * ICU builds differ in the spaces they put in dates: Node writes thin spaces\n * around a range's dash where a browser may write plain ones, so markup\n * rendered on the server wouldn't match the client's. Plain spaces read the\n * same and hydrate.\n */\nconst plainSpaces = (text: string) => text.replace(/[\\u2009\\u202f]/g, \" \")\n\n/** One day for a trigger or summary, such as \"Oct 5, 2026\". */\nexport function formatDate(date: Date, locale = \"en-US\"): string {\n  const text = new Intl.DateTimeFormat(locale, {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  }).format(date)\n  return plainSpaces(text)\n}\n\n/**\n * A range the way the locale writes one, sharing what both ends have in\n * common: \"Oct 1 – 7, 2026\", or \"Oct 5, 2026\" when it's a single day.\n */\nexport function formatDateRange(range: DateRange, locale = \"en-US\"): string {\n  const text = new Intl.DateTimeFormat(locale, {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  }).formatRange(range.from, range.to)\n  return plainSpaces(text)\n}\n\n/** A time of day the way the locale writes one, such as \"9:30 AM\". */\nexport function formatTime(date: Date, locale = \"en-US\"): string {\n  const text = new Intl.DateTimeFormat(locale, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  }).format(date)\n  return plainSpaces(text)\n}\n\n/** Two times on one day, sharing what they have in common: \"9:30 – 10:00 AM\". */\nexport function formatTimeRange(\n  from: Date,\n  to: Date,\n  locale = \"en-US\"\n): string {\n  const text = new Intl.DateTimeFormat(locale, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  }).formatRange(from, to)\n  return plainSpaces(text)\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/lib/merge-refs.ts",
      "content": "import * as React from \"react\"\n\n/**\n * One callback ref that sets every ref given, so a part can keep its own\n * ref on an element and still hand the element to a caller's `ref`. Undo\n * runs through React 19's ref cleanup, which calls a function ref with\n * null and empties an object ref.\n */\nexport function mergeRefs<T>(\n  ...refs: (React.Ref<T> | undefined)[]\n): React.RefCallback<T> {\n  return (node) => {\n    const cleanups = refs.map((ref) => {\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node)\n        return typeof cleanup === \"function\" ? cleanup : () => ref(null)\n      }\n      if (ref) {\n        ref.current = node\n        return () => {\n          ref.current = null\n        }\n      }\n      return undefined\n    })\n    return () => {\n      for (const cleanup of cleanups) cleanup?.()\n    }\n  }\n}\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "theme": {
      "color-primary-hover": "var(--primary-hover)",
      "color-secondary-hover": "var(--secondary-hover)",
      "color-ring-subtle": "var(--ring-subtle)"
    },
    "light": {
      "primary-hover": "color-mix(in oklch, var(--color-neutral-900) 80%, transparent)",
      "secondary-hover": "var(--color-neutral-200)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-400) 50%, transparent)"
    },
    "dark": {
      "primary-hover": "color-mix(in oklch, var(--color-neutral-50) 80%, transparent)",
      "secondary-hover": "var(--color-neutral-700)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-500) 50%, transparent)"
    }
  },
  "meta": {
    "tier": "base-components",
    "group": "Forms",
    "docs": "https://fibo.toribryan.com/?path=/docs/base-components-forms-calendar--docs"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/base-components-forms-calendar--docs",
  "categories": [
    "base-components",
    "forms"
  ],
  "type": "registry:ui"
}