{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "A month to choose a day from: the chosen day lands into the switcher's thumb look on the part spring, months turn on the drum and slide in from the side you head to; DatePicker opens it from a form field.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/calendar/calendar.tsx",
      "type": "registry:ui",
      "target": "components/metalui/calendar/calendar.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { SwapText } from '../../motion/swap';\nimport { buttonClasses } from '../button/button';\nimport { Popover } from '../popover/popover';\n\n/* ─────────────────────────────────────────────────────────\n * CALENDAR and DATE PICKER, a month to choose a day from\n *\n *   grid      weekday initials, then six rows of days (always six: the height never jumps)\n *   hover     a day sinks a touch into the switcher's track look (only the chosen day stands raised)\n *   today     a small green lamp under its number\n *   choose    the chosen day takes the switcher's raised thumb look and lands into it on the part\n *             spring (a day is not a track: the choice lands where it is, never glides across weeks)\n *   month     the title turns on the drum (up for later, down for earlier); the grid comes in two\n *             grid steps from the side you are heading to as it fades in, on the settle spring\n *   keys      arrows by day and week, Page Up / Down by month, Home / End to the week's ends,\n *             Enter or Space chooses; moving past the month turns it\n *   limits    days outside the month in ink3; days out of range disabled at 40 %\n *   picker    a form field that opens the calendar in a popover; choosing closes it and the\n *             field's text turns on the drum\n * Reduce Motion: the grid arrives and the choice lands at once; the fades stay.\n * ───────────────────────────────────────────────────────── */\n\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst addMonths = (d: Date, n: number) => {\n  const target = new Date(d.getFullYear(), d.getMonth() + n, 1);\n  const last = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n  return new Date(target.getFullYear(), target.getMonth(), Math.min(d.getDate(), last));\n};\nconst sameDay = (a: Date | null | undefined, b: Date | null | undefined) => !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\nconst monthKey = (d: Date) => d.getFullYear() * 12 + d.getMonth();\n\n/** The first day of the week for a locale, as a JS day (0 Sunday … 6 Saturday). */\nfunction weekStartOf(locale?: string) {\n  try {\n    const loc = new Intl.Locale(locale ?? navigator.language) as Intl.Locale & { getWeekInfo?: () => { firstDay: number }; weekInfo?: { firstDay: number } };\n    const first = loc.getWeekInfo?.().firstDay ?? loc.weekInfo?.firstDay ?? 1;\n    return first % 7;\n  } catch {\n    return 1;\n  }\n}\n\nconst ROOT = 'mu-calendar inline-grid gap-calendar-head-gap p-calendar-pad select-none';\nconst HEAD = 'mu-calendar-head flex items-center justify-between gap-calendar-head-gap h-calendar-head-height';\nconst TITLE = 'mu-calendar-title type-title text-ink';\nconst STEP = `${buttonClasses('standard', 'compact')} mu-calendar-step px-0! w-calendar-head-height justify-center`;\nconst GRID_WRAP = 'mu-calendar-body relative';\nconst TABLE = 'mu-calendar-grid calendar-grid';\nconst WEEKDAY = 'mu-calendar-weekday size-calendar-day-size p-0 type-meta text-ink3 text-center';\nconst CELL = 'p-0';\nconst DAY = 'mu-calendar-day relative z-1 grid place-items-center size-calendar-day-size rounded-calendar-day-radius border-0 bg-transparent type-ui tabular-nums text-ink cursor-pointer outline-none transition-row hover:not-data-selected:recipe-switcher data-selected:recipe-switcher-thumb data-selected:text-ink data-selected:calendar-land focus-visible:focus-ring data-outside:text-ink3 disabled:opacity-calendar-disabled disabled:cursor-default data-today:calendar-today';\n\nexport interface CalendarProps {\n  value?: Date | null;\n  defaultValue?: Date | null;\n  onValueChange?: (value: Date) => void;\n  /** The month shown first (the value's, or today's). */\n  defaultMonth?: Date;\n  /** The earliest and latest days that can be chosen. */\n  min?: Date;\n  max?: Date;\n  /** A locale for names and the first day of the week (the browser's by default). */\n  locale?: string;\n  /** Put focus on the chosen day (or today) when it mounts: for a calendar opened in a picker. */\n  autoFocus?: boolean;\n  'aria-label'?: string;\n  className?: string;\n}\n\n/** A month to choose a day from. */\nexport function Calendar({ value, defaultValue, onValueChange, defaultMonth, min, max, locale, autoFocus, className, ...aria }: CalendarProps) {\n  const [own, setOwn] = React.useState<Date | null>(defaultValue ?? null);\n  const chosen = value !== undefined ? value : own;\n  const today = startOfDay(new Date());\n  const [focused, setFocused] = React.useState<Date>(() => startOfDay(chosen ?? defaultMonth ?? today));\n  const [dir, setDir] = React.useState<'later' | 'earlier' | null>(null);\n  const moved = React.useRef(autoFocus ?? false);\n  const grid = React.useRef<HTMLTableElement>(null);\n  const titleId = React.useId();\n\n  const first = weekStartOf(locale);\n  const month = startOfMonth(focused);\n  const lead = (month.getDay() - first + 7) % 7;\n  const days = Array.from({ length: 42 }, (_, i) => addDays(month, i - lead));\n  const weekdays = days.slice(0, 7).map((d) => ({ short: new Intl.DateTimeFormat(locale, { weekday: 'narrow' }).format(d), long: new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(d) }));\n  const title = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(month);\n  const full = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\n  const out = (d: Date) => (min != null && d < startOfDay(min)) || (max != null && d > startOfDay(max));\n\n  const go = (next: Date, byKey: boolean) => {\n    const clamped = min && next < startOfDay(min) ? startOfDay(min) : max && next > startOfDay(max) ? startOfDay(max) : next;\n    if (monthKey(clamped) !== monthKey(focused)) setDir(monthKey(clamped) > monthKey(focused) ? 'later' : 'earlier');\n    moved.current = byKey;\n    setFocused(clamped);\n  };\n  const choose = (d: Date) => {\n    if (out(d)) return;\n    if (value === undefined) setOwn(d);\n    if (monthKey(d) !== monthKey(focused)) go(d, false); else setFocused(d);\n    onValueChange?.(d);\n  };\n\n  // Keyboard moves carry focus with them.\n  React.useEffect(() => {\n    if (!moved.current) return;\n    grid.current?.querySelector<HTMLButtonElement>('button[tabindex=\"0\"]')?.focus();\n    moved.current = false;\n  }, [focused]);\n\n  const onKey = (e: React.KeyboardEvent) => {\n    const map: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(focused, -1),\n      ArrowRight: () => addDays(focused, 1),\n      ArrowUp: () => addDays(focused, -7),\n      ArrowDown: () => addDays(focused, 7),\n      PageUp: () => addMonths(focused, e.shiftKey ? -12 : -1),\n      PageDown: () => addMonths(focused, e.shiftKey ? 12 : 1),\n      Home: () => addDays(focused, -((focused.getDay() - first + 7) % 7)),\n      End: () => addDays(focused, 6 - ((focused.getDay() - first + 7) % 7)),\n    };\n    if (map[e.key]) {\n      e.preventDefault();\n      go(map[e.key](), true);\n    } else if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault();\n      choose(focused);\n    }\n  };\n\n  const arrive = dir === 'later' ? 'calendar-arrive-later' : dir === 'earlier' ? 'calendar-arrive-earlier' : '';\n  return (\n    <div className={className ? `${ROOT} ${className}` : ROOT} aria-label={aria['aria-label']} role=\"group\">\n      <div className={HEAD}>\n        <button type=\"button\" className={STEP} aria-label=\"Previous month\" onClick={() => go(addMonths(focused, -1), false)} disabled={min != null && startOfMonth(focused) <= startOfMonth(min)}>\n          <svg aria-hidden viewBox=\"0 0 10 10\" className=\"size-calendar-step-glyph\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M6.25 2 3.25 5l3 3\" /></svg>\n        </button>\n        <span id={titleId} aria-live=\"polite\" className={dir === 'earlier' ? `${TITLE} swap-down` : TITLE}><SwapText value={title} /></span>\n        <button type=\"button\" className={STEP} aria-label=\"Next month\" onClick={() => go(addMonths(focused, 1), false)} disabled={max != null && startOfMonth(focused) >= startOfMonth(max)}>\n          <svg aria-hidden viewBox=\"0 0 10 10\" className=\"size-calendar-step-glyph\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M3.75 2 6.75 5l-3 3\" /></svg>\n        </button>\n      </div>\n      <div className={GRID_WRAP}>\n        <table ref={grid} key={monthKey(month)} role=\"grid\" aria-labelledby={titleId} className={`${TABLE} ${arrive}`} onKeyDown={onKey}>\n          <thead>\n            <tr>{weekdays.map((w, i) => <th key={i} scope=\"col\" abbr={w.long} className={WEEKDAY}>{w.short}</th>)}</tr>\n          </thead>\n          <tbody>\n            {Array.from({ length: 6 }, (_, r) => (\n              <tr key={r}>\n                {days.slice(r * 7, r * 7 + 7).map((d) => {\n                  const selected = sameDay(d, chosen);\n                  return (\n                    <td key={d.getTime()} role=\"gridcell\" aria-selected={selected} className={CELL}>\n                      <button\n                        type=\"button\"\n                        tabIndex={sameDay(d, focused) ? 0 : -1}\n                        className={DAY}\n                        aria-label={full.format(d)}\n                        aria-current={sameDay(d, today) ? 'date' : undefined}\n                        data-today={sameDay(d, today) ? '' : undefined}\n                        data-selected={selected ? '' : undefined}\n                        data-outside={d.getMonth() !== month.getMonth() ? '' : undefined}\n                        disabled={out(d)}\n                        onClick={() => choose(d)}\n                        onFocus={() => { if (!sameDay(d, focused)) setFocused(d); }}\n                      >\n                        {d.getDate()}\n                      </button>\n                    </td>\n                  );\n                })}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n}\n\nconst PICKER = 'mu-date-picker relative inline-flex items-center gap-field-regular-gap h-field-regular-height min-w-calendar-picker-min-width pl-field-regular-pad-left pr-field-regular-pad-right rounded-field-regular-radius box-border border-0 recipe-well-field type-ui text-field-field-ink text-left cursor-pointer outline-none focus-visible:focus-ring-flush data-popup-open:focus-ring-flush disabled:opacity-field-state-disabled disabled:cursor-default data-invalid:invalid-ring';\n\nexport interface DatePickerProps extends Omit<CalendarProps, 'autoFocus' | 'aria-label'> {\n  /** Shown when no day is chosen. */\n  placeholder?: string;\n  /** Names the field for assistive tech. */\n  'aria-label': string;\n  invalid?: boolean;\n  disabled?: boolean;\n  /** How the chosen day is written in the field. */\n  format?: Intl.DateTimeFormatOptions;\n}\n\n/** A form field that opens a calendar. */\nexport function DatePicker({ value, defaultValue, onValueChange, placeholder = 'Choose a day', invalid, disabled, format = { day: 'numeric', month: 'short', year: 'numeric' }, locale, className, ...rest }: DatePickerProps) {\n  const [own, setOwn] = React.useState<Date | null>(defaultValue ?? null);\n  const chosen = value !== undefined ? value : own;\n  const [open, setOpen] = React.useState(false);\n  const text = chosen ? new Intl.DateTimeFormat(locale, format).format(chosen) : placeholder;\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <Popover.Trigger>\n        <button type=\"button\" disabled={disabled} aria-label={`${rest['aria-label']}: ${chosen ? text : 'none chosen'}`} aria-invalid={invalid || undefined} data-invalid={invalid ? '' : undefined} className={className ? `${PICKER} ${className}` : PICKER}>\n          <svg aria-hidden viewBox=\"0 0 14 14\" className=\"size-field-regular-glyph flex-none text-ink2\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.3} strokeLinecap=\"round\"><rect x=\"2\" y=\"3\" width=\"10\" height=\"9\" rx=\"2\" /><path d=\"M2 6h10M5 1.5v3M9 1.5v3\" /></svg>\n          <span className={chosen ? 'flex-1' : 'flex-1 text-field-field-hint'}><SwapText value={text} /></span>\n        </button>\n      </Popover.Trigger>\n      <Popover.Content align=\"start\">\n        <Calendar\n          {...rest}\n          locale={locale}\n          value={chosen}\n          autoFocus\n          aria-label={rest['aria-label']}\n          onValueChange={(d) => {\n            if (value === undefined) setOwn(d);\n            onValueChange?.(d);\n            setOpen(false);\n          }}\n        />\n      </Popover.Content>\n    </Popover>\n  );\n}\n\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/calendar.md. SwiftUI: MetalCalendar in the MetalUI Swift package."
}
