{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "type": "registry:ui",
  "description": "Single-month calendar grid for date selection (react-day-picker). Use inline or in a custom popover. For form fields, use Date Picker. Supports min/max, disabled dates, multi-month, and locale.",
  "categories": [
    "date-time"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/calendar/calendar-date.ts",
      "content": "/**\n * Local-date helpers for <uip-calendar> — the subset of date-fns / react-day-picker's\n * DateLib the React Calendar relies on, on native Date (local time, no deps).\n */\n\nexport interface DateRange {\n  from?: Date\n  to?: Date\n}\n\n/** react-day-picker's Matcher union. */\nexport type Matcher =\n  | boolean\n  | Date\n  | Date[]\n  | DateRange\n  | { dayOfWeek: number | number[] }\n  | { before: Date; after: Date }\n  | { before: Date }\n  | { after: Date }\n  | ((date: Date) => boolean)\n\nexport const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate())\nexport const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1)\nexport const endOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth() + 1, 0)\nexport const startOfYear = (d: Date) => new Date(d.getFullYear(), 0, 1)\nexport const endOfYear = (d: Date) => new Date(d.getFullYear(), 11, 31)\nexport const addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n)\nexport const addWeeks = (d: Date, n: number) => addDays(d, n * 7)\n/** date-fns semantics: the day is clamped to the target month's length. */\nexport function addMonths(d: Date, n: number) {\n  const target = new Date(d.getFullYear(), d.getMonth() + n, 1)\n  const last = endOfMonth(target).getDate()\n  return new Date(target.getFullYear(), target.getMonth(), Math.min(d.getDate(), last))\n}\nexport const addYears = (d: Date, n: number) => addMonths(d, n * 12)\nexport const isSameDay = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\nexport const isSameMonth = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()\nexport const differenceInCalendarDays = (a: Date, b: Date) =>\n  Math.round((startOfDay(a).getTime() - startOfDay(b).getTime()) / 86_400_000)\nexport const differenceInCalendarMonths = (a: Date, b: Date) =>\n  (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth())\nexport function startOfWeek(d: Date, weekStartsOn: number) {\n  const diff = (d.getDay() - weekStartsOn + 7) % 7\n  return addDays(d, -diff)\n}\nexport const endOfWeek = (d: Date, weekStartsOn: number) => addDays(startOfWeek(d, weekStartsOn), 6)\n\nconst pad = (n: number) => String(n).padStart(2, '0')\n/** `yyyy-MM-dd` (react-day-picker's `data-day`). */\nexport const isoDate = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n/** `yyyy-MM` (react-day-picker's `data-month`). */\nexport const isoMonth = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}`\n\n/** Date, `yyyy-MM-dd` (read as a LOCAL date, not UTC midnight) or any Date-parsable string. */\nexport function toDate(v: unknown): Date | undefined {\n  if (v == null || v === '') return undefined\n  if (v instanceof Date) return isNaN(v.getTime()) ? undefined : startOfDay(v)\n  if (typeof v === 'number') return startOfDay(new Date(v))\n  if (typeof v === 'string') {\n    const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(v.trim())\n    if (m) return new Date(+m[1], +m[2] - 1, +m[3])\n    const d = new Date(v)\n    return isNaN(d.getTime()) ? undefined : startOfDay(d)\n  }\n  return undefined\n}\n\nexport function rangeIncludesDate(range: DateRange, date: Date, excludeEnds = false) {\n  let { from, to } = range\n  if (from && to) {\n    if (differenceInCalendarDays(to, from) < 0) [from, to] = [to, from]\n    const e = excludeEnds ? 1 : 0\n    return differenceInCalendarDays(date, from) >= e && differenceInCalendarDays(to, date) >= e\n  }\n  if (!excludeEnds && to) return isSameDay(to, date)\n  if (!excludeEnds && from) return isSameDay(from, date)\n  return false\n}\n\n/** react-day-picker's `dateMatchModifiers`. */\nexport function matches(date: Date, matchers: Matcher | Matcher[] | undefined): boolean {\n  if (matchers === undefined) return false\n  const list = Array.isArray(matchers) && !matchers.every((m) => m instanceof Date) ? matchers : [matchers as Matcher]\n  return list.some((m) => {\n    if (typeof m === 'boolean') return m\n    if (m instanceof Date) return isSameDay(date, m)\n    if (Array.isArray(m)) return m.some((d) => isSameDay(date, d))\n    if (typeof m === 'function') return m(date)\n    if (!m || typeof m !== 'object') return false\n    if ('from' in m || 'to' in m) return rangeIncludesDate(m as DateRange, date)\n    if ('dayOfWeek' in m) {\n      const dow = m.dayOfWeek\n      return Array.isArray(dow) ? dow.includes(date.getDay()) : dow === date.getDay()\n    }\n    const before = 'before' in m ? m.before : undefined\n    const after = 'after' in m ? m.after : undefined\n    if (before && after) {\n      const isDayBefore = differenceInCalendarDays(before, date) > 0\n      const isDayAfter = differenceInCalendarDays(after, date) < 0\n      return differenceInCalendarDays(before, after) > 0 ? isDayAfter && isDayBefore : isDayBefore || isDayAfter\n    }\n    if (after) return differenceInCalendarDays(date, after) > 0\n    if (before) return differenceInCalendarDays(before, date) > 0\n    return false\n  })\n}\n\nconst isoRe = /^\\d{4}-\\d{2}-\\d{2}/\nfunction revive(v: unknown): unknown {\n  if (typeof v === 'string' && isoRe.test(v)) return toDate(v)\n  if (Array.isArray(v)) return v.map(revive)\n  if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, revive(x)]))\n  return v\n}\n\n/**\n * Attribute form of react-day-picker's `modifiers` prop: a JSON object mapping\n * custom names to matchers, with ISO date strings (`{\"booked\":[\"2026-09-21\"],\n * \"weekend\":{\"dayOfWeek\":[0,6]}}`). Anything else → undefined.\n */\nexport function parseModifiersMap(value: string | null): Record<string, Matcher | Matcher[]> | undefined {\n  if (value === null) return undefined\n  const v = value.trim()\n  if (v === '' || v === 'false') return undefined\n  try {\n    const obj = revive(JSON.parse(v))\n    if (obj && typeof obj === 'object' && !Array.isArray(obj)) return obj as Record<string, Matcher | Matcher[]>\n  } catch {\n    /* not JSON */\n  }\n  return undefined\n}\n\n/**\n * Attribute form of a Matcher: empty/`true` → every day; JSON with ISO date\n * strings (`{\"dayOfWeek\":[0,6]}`, `[{\"before\":\"2026-09-21\"}]`); or a\n * comma/space-separated list of `yyyy-MM-dd` dates.\n */\nexport function parseMatcher(value: string | null): Matcher | Matcher[] | undefined {\n  if (value === null) return undefined\n  const v = value.trim()\n  if (v === '' || v === 'true') return true\n  if (v === 'false') return undefined\n  if (v.startsWith('{') || v.startsWith('[')) {\n    try {\n      return revive(JSON.parse(v)) as Matcher | Matcher[]\n    } catch {\n      return undefined\n    }\n  }\n  return v\n    .split(/[\\s,]+/)\n    .map(toDate)\n    .filter((d): d is Date => !!d)\n}\n\n/**\n * date-fns `getWeek` port (what react-day-picker's week numbers use):\n * weekStartsOn-aware with firstWeekContainsDate = 1 (the date-fns default).\n * RDP's `ISOWeek` opt-in is not supported — ISO behaviour is approximated by\n * `week-starts-on=\"1\"` (the year-boundary rule still differs: ISO counts the\n * week with the first Thursday as week 1).\n */\nexport function getWeekNumber(d: Date, weekStartsOn: number): number {\n  const diff = startOfWeek(d, weekStartsOn).getTime() - startOfWeekYear(d, weekStartsOn).getTime()\n  // Round: a week is not a constant number of ms across a DST shift.\n  return Math.round(diff / 604_800_000) + 1\n}\n\n/** date-fns `getWeekYear` port: the week-numbering year a date belongs to. */\nfunction getWeekYear(d: Date, weekStartsOn: number): number {\n  const year = d.getFullYear()\n  const startOfNextYear = startOfWeek(new Date(year + 1, 0, 1), weekStartsOn)\n  const startOfThisYear = startOfWeek(new Date(year, 0, 1), weekStartsOn)\n  if (d.getTime() >= startOfNextYear.getTime()) return year + 1\n  if (d.getTime() >= startOfThisYear.getTime()) return year\n  return year - 1\n}\n\n/** date-fns `startOfWeekYear` port. */\nfunction startOfWeekYear(d: Date, weekStartsOn: number): Date {\n  return startOfWeek(new Date(getWeekYear(d, weekStartsOn), 0, 1), weekStartsOn)\n}\n\n/** First day of week for a locale: Intl week info when available, Sunday otherwise. */\nexport function localeWeekStart(locale: string): number {\n  try {\n    const loc = new Intl.Locale(locale) as Intl.Locale & {\n      getWeekInfo?: () => { firstDay: number }\n      weekInfo?: { firstDay: number }\n    }\n    const info = loc.getWeekInfo?.() ?? loc.weekInfo\n    if (info) return info.firstDay % 7\n  } catch {\n    /* unknown locale */\n  }\n  return 0\n}\n\n/** Locales react-day-picker renders year-first (\"2026年5月\"). */\nconst yearFirst = new Set(['eu', 'hu', 'ja', 'ja-Hira', 'ja-JP', 'ko', 'ko-KR', 'lt', 'lt-LT', 'lv', 'lv-LV', 'mn', 'mn-MN', 'zh', 'zh-CN', 'zh-HK', 'zh-TW'])\nexport const isYearFirst = (locale: string) => yearFirst.has(locale)\n\nconst ordinal = (n: number) => {\n  const s = ['th', 'st', 'nd', 'rd']\n  const v = n % 100\n  return n + (s[(v - 20) % 10] || s[v] || s[0])\n}\n\n/** date-fns `PPPP` (react-day-picker's day/gridcell label). */\nexport function formatFull(d: Date, locale: string) {\n  if (locale.startsWith('en')) {\n    const weekday = d.toLocaleDateString(locale, { weekday: 'long' })\n    const month = d.toLocaleDateString(locale, { month: 'long' })\n    return `${weekday}, ${month} ${ordinal(d.getDate())}, ${d.getFullYear()}`\n  }\n  return new Intl.DateTimeFormat(locale, { dateStyle: 'full' }).format(d)\n}\n\n/** `LLLL y` / year-first (react-day-picker's caption + grid label). */\nexport const formatMonthYear = (d: Date, locale: string) =>\n  new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(d)\n",
      "type": "registry:file",
      "target": "src/uip/components/calendar/calendar-date.ts"
    },
    {
      "path": "packages/elements-lit/src/components/calendar/calendar.ts",
      "content": "import { LitElement, css, html, isServer, nothing, type PropertyValues } from 'lit'\nimport { ChevronLeft, ChevronRight } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { buttonVariants } from '../button/button.variants'\nimport {\n  addDays,\n  addMonths,\n  addWeeks,\n  addYears,\n  differenceInCalendarMonths,\n  endOfMonth,\n  endOfWeek,\n  endOfYear,\n  formatFull,\n  formatMonthYear,\n  getWeekNumber,\n  isSameDay,\n  isSameMonth,\n  isYearFirst,\n  isoDate,\n  isoMonth,\n  localeWeekStart,\n  matches,\n  parseMatcher,\n  parseModifiersMap,\n  rangeIncludesDate,\n  startOfMonth,\n  startOfWeek,\n  startOfYear,\n  toDate,\n  type DateRange,\n  type Matcher,\n} from './calendar-date'\n\nexport type { DateRange, Matcher } from './calendar-date'\nexport type CalendarMode = 'single' | 'multiple' | 'range'\nexport type CalendarSelected = Date | Date[] | DateRange | string | string[] | undefined\n/** react-day-picker's `modifiers` prop: custom name → matcher(s). Falsy values are ignored. */\nexport type CalendarModifiers = Record<string, Matcher | Matcher[] | undefined>\n/** react-day-picker's `modifiersClassNames` prop: modifier name → class string. */\nexport type CalendarModifiersClassNames = Record<string, string>\n/**\n * react-day-picker's `formatters`, as closely as fits a web component: the\n * same names, but the third `dateLib` argument is replaced by\n * `{ locale }` (there is no date-fns here). Property only — functions can't\n * be attributes. `formatMonthDropdown` / `formatYearDropdown` are not\n * supported (the dropdowns render locale month/year names).\n */\nexport interface CalendarFormatters {\n  formatCaption?: (month: Date, options?: { locale?: string }) => string\n  formatDay?: (date: Date, options?: { locale?: string }) => string\n  formatWeekdayName?: (weekday: Date, options?: { locale?: string }) => string\n  formatWeekNumber?: (weekNumber: number, options?: { locale?: string }) => string\n  formatWeekNumberHeader?: () => string\n}\n\n/** react-day-picker's computed `Modifiers` (`Record<string, boolean>`): the built-in flags plus custom names. */\ninterface Modifiers {\n  focused: boolean\n  disabled: boolean\n  hidden: boolean\n  outside: boolean\n  today: boolean\n  selected: boolean\n  range_start: boolean\n  range_end: boolean\n  range_middle: boolean\n  [custom: string]: boolean\n}\n\n/** Built-in modifier names (a custom modifier of the same name replaces the flag, like RDP). */\nconst builtinModifierNames = [\n  'focused',\n  'disabled',\n  'hidden',\n  'outside',\n  'today',\n  'selected',\n  'range_start',\n  'range_end',\n  'range_middle',\n] as const\n\ninterface Day {\n  date: Date\n  displayMonth: Date\n  outside: boolean\n}\n\n// ---------------------------------------------------------------------------\n// React's `classNames` map, verbatim (packages/registry-react/components/calendar/Calendar.tsx).\n// Keys react-day-picker keeps at its defaults render as `rdp-<key>`.\nconst classNames = {\n  root: 'rdp-root',\n  months: 'relative flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0',\n  month: cn(\n    'flex flex-1 flex-col gap-4 motion-safe:animate-[calendar-month-in_220ms_cubic-bezier(0.22,1,0.36,1)_both]',\n  ),\n  month_caption: 'flex items-center justify-center',\n  caption_label: 'text-sm font-medium',\n  dropdowns: 'rdp-dropdowns',\n  dropdown_root: 'rdp-dropdown_root',\n  dropdown: 'rdp-dropdown',\n  months_dropdown: 'rdp-months_dropdown',\n  years_dropdown: 'rdp-years_dropdown',\n  chevron: 'rdp-chevron',\n  nav: 'absolute inset-x-0 top-0 flex items-center justify-between gap-1',\n  button_previous: cn(\n    buttonVariants({ variant: 'outline' }),\n    'size-9 bg-transparent p-0 opacity-70 hover:opacity-100 focus-visible:opacity-100',\n  ),\n  button_next: cn(\n    buttonVariants({ variant: 'outline' }),\n    'size-9 bg-transparent p-0 opacity-70 hover:opacity-100 focus-visible:opacity-100',\n  ),\n  month_grid: 'w-full border-collapse space-y-1',\n  weekdays: 'flex',\n  weekday: 'text-muted-foreground flex-1 rounded-md text-xs font-normal',\n  weeks: 'rdp-weeks',\n  week: 'mt-2 flex w-full',\n  day: cn(\n    'relative flex-1 p-0 text-center text-sm focus-within:relative focus-within:z-20',\n    '[&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',\n    '[&:has([aria-selected].day-outside)]:bg-accent/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md',\n  ),\n  day_button: cn(\n    buttonVariants({ variant: 'ghost' }),\n    'size-9 cursor-pointer p-0 font-normal transition-[color,background-color,transform,box-shadow] duration-150 aria-selected:opacity-100',\n  ),\n  selected:\n    'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground shadow-sm motion-safe:animate-[calendar-day-pop_220ms_cubic-bezier(0.22,1.4,0.36,1)_both] [&>button]:bg-primary [&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground',\n  today: '[&:not([aria-selected])]:bg-accent [&:not([aria-selected])]:text-accent-foreground',\n  outside: 'day-outside text-muted-foreground aria-selected:text-muted-foreground',\n  disabled: 'text-muted-foreground opacity-50',\n  focused: 'rdp-focused',\n  range_start:\n    'day-range-start rounded-l-md motion-safe:animate-[calendar-day-pop_220ms_cubic-bezier(0.22,1.4,0.36,1)_both]',\n  range_end:\n    'day-range-end rounded-r-md motion-safe:animate-[calendar-day-pop_220ms_cubic-bezier(0.22,1.4,0.36,1)_both]',\n  range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground transition-colors duration-200',\n  hidden: 'invisible',\n  // React's classNames map leaves these at RDP's `rdp-*` defaults (unstyled\n  // hooks). The footer stays exactly that; the week-number column gets minimal\n  // Lit styling so the numbers align with the day cells (h-9, muted, tabular).\n  footer: 'rdp-footer',\n  week_number: 'rdp-week_number text-muted-foreground flex h-9 w-9 shrink-0 items-center justify-center text-xs font-normal tabular-nums',\n  week_number_header: 'rdp-week_number_header w-9 shrink-0 text-xs font-normal',\n} as const\n\n// React injects these keyframes into <head> once (same id + content, so a page\n// running both the React and the Lit calendar shares one tag). Keyframes can't\n// be a utility; document-scope @keyframes resolve for shadow-tree animations.\nconst CAL_STYLE_ID = 'calendar-motion-styles'\nconst CAL_STYLE_CONTENT = `\n@keyframes calendar-day-pop {\n  0% { transform: scale(0.86); }\n  70% { transform: scale(1.06); }\n  100% { transform: scale(1); }\n}\n@keyframes calendar-month-in {\n  from { opacity: 0; transform: translateY(4px); }\n  to { opacity: 1; transform: translateY(0); }\n}\n@media (prefers-reduced-motion: reduce) {\n  [data-slot='calendar'] [class*='animate-\\\\[calendar-'] {\n    animation: none !important;\n  }\n}\n`\nfunction injectMotionStyles() {\n  if (isServer) return\n  let el = document.getElementById(CAL_STYLE_ID) as HTMLStyleElement | null\n  if (!el) {\n    el = document.createElement('style')\n    el.id = CAL_STYLE_ID\n    document.head.appendChild(el)\n  }\n  if (el.textContent !== CAL_STYLE_CONTENT) el.textContent = CAL_STYLE_CONTENT\n}\n\nconst matcherConverter = {\n  fromAttribute: parseMatcher,\n  toAttribute: () => null,\n}\nconst dateConverter = { fromAttribute: (v: string | null) => toDate(v), toAttribute: () => null }\n// Boolean props whose React default is `true`: `attr=\"false\"` turns them off.\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\ntype MoveBy = 'day' | 'week' | 'month' | 'year' | 'startOfWeek' | 'endOfWeek'\n\n/**\n * <uip-calendar> — the registry Calendar (react-day-picker v10 in React) as ONE\n * web component with its own month-grid logic.\n *\n * Markup, classes, ARIA and data attributes follow what react-day-picker renders\n * with React's `classNames` map: root div > months > nav + month > caption +\n * `table[role=grid]` > `td[role=gridcell]` > day `<button>`. The whole grid lives\n * in one shadow root, so the roving tabindex and focus management work like\n * react-day-picker's: Arrow keys move by day/week (Shift: month/year), PageUp /\n * PageDown by month (Shift: year), Home / End to the week's start / end,\n * Enter / Space select (native button).\n *\n * Properties / attributes (react-day-picker prop names):\n *  - `mode`: 'single' | 'multiple' | 'range' (unset = display only, like React).\n *  - `selected`: Date | Date[] | { from, to } — or ISO `yyyy-MM-dd` strings; the\n *    attribute is one date, or a comma-separated list (multiple; range = from,to).\n *  - `disabled`: a react-day-picker Matcher (Date, Date[], { before }, { after },\n *    { from, to }, { dayOfWeek }, a function, `true`) or an array of them. The\n *    attribute takes JSON with ISO dates (`{\"dayOfWeek\":[0,6]}`) or a date list.\n *  - `modifiers`: custom name → matcher(s), e.g. `{ booked: [dates] }` (RDP's\n *    `modifiers`; the attribute takes the same JSON shape with ISO dates).\n *    Custom flags join the built-ins in the `select` detail; a custom name\n *    matching a built-in (e.g. `disabled`) replaces that flag, like RDP.\n *  - `modifiers-class-names`: modifier name → class string (RDP's\n *    `modifiersClassNames`; JSON attribute). Applied to the day cell — and\n *    like RDP it can also override a built-in state's class (e.g. `{ today:\n *    '…' }`). Values must be utilities the shadow Tailwind sheet provides\n *    (colours, rings, font weight); fully custom day visuals (event dots)\n *    need RDP's `DayButton` override, which has no web-component equivalent\n *    (see below) — mark days with utilities instead.\n *  - `month` / `default-month`, `start-month` / `end-month` (ISO or Date).\n *  - `number-of-months`, `show-outside-days` (default true; `=\"false\"` to hide),\n *    `caption-layout` ('label' | 'dropdown' | 'dropdown-months' | 'dropdown-years'),\n *    `locale` (BCP-47 string, default 'en-US'), `week-starts-on` (0–6),\n *    `required`, `min` / `max` (multiple: count; range: nights), `disable-navigation`,\n *    `today` (Date/ISO).\n *  - `fixed-weeks`: always render 6 weeks per month (RDP's `fixedWeeks`).\n *  - `show-week-number`: a leading week-number column (RDP's `showWeekNumber`;\n *    numbers follow date-fns `getWeek` with the calendar's week start).\n *  - `formatters`: RDP's `formatters` (property only): `formatCaption`,\n *    `formatDay`, `formatWeekdayName`, `formatWeekNumber`,\n *    `formatWeekNumberHeader`.\n *  - `footer` / `slot=\"footer\"`: RDP's `footer` node, rendered below the grid\n *    (`role=\"status\"`, `part=\"footer\"`).\n *\n * Not supported (genuinely infeasible as web-component API, documented here\n * instead of silently dropped): `components` overrides (DayButton, Chevron,\n * … — injected light DOM would break the shadow grid's roving tabindex,\n * focus and ARIA contracts; style the container via `part=\"root\"` and mark\n * days via `modifiers` + `modifiersClassNames`), `styles` /\n * `modifiersStyles` (inline-style objects; use classes), the `hidden` prop,\n * `ISOWeek` / `broadcastCalendar`, and week-number localisation beyond\n * `week-starts-on`.\n *\n * Events (bubble, composed):\n *  - `select` — React's `onSelect`. detail: { selected, day, modifiers }. The\n *    element updates `selected` itself unless the event is canceled\n *    (`preventDefault()` = controlled).\n *  - `month-change` — React's `onMonthChange`. detail: { month }.\n */\nexport class UipCalendar extends LitElement {\n  // React's root is a block <div>.\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    mode: { reflect: true },\n    selected: {\n      converter: { fromAttribute: (v: string | null) => (v ? v.split(/\\s*,\\s*/) : undefined), toAttribute: () => null },\n    },\n    disabled: { converter: matcherConverter },\n    modifiers: { converter: { fromAttribute: parseModifiersMap, toAttribute: () => null } },\n    modifiersClassNames: {\n      attribute: 'modifiers-class-names',\n      converter: {\n        fromAttribute: (v: string | null) => {\n          if (v === null) return undefined\n          try {\n            const obj = JSON.parse(v)\n            if (obj && typeof obj === 'object' && !Array.isArray(obj)) return obj\n          } catch {\n            /* not JSON */\n          }\n          return undefined\n        },\n        toAttribute: () => null,\n      },\n    },\n    month: { converter: dateConverter },\n    defaultMonth: { attribute: 'default-month', converter: dateConverter },\n    startMonth: { attribute: 'start-month', converter: dateConverter },\n    endMonth: { attribute: 'end-month', converter: dateConverter },\n    today: { converter: dateConverter },\n    numberOfMonths: { attribute: 'number-of-months', type: Number },\n    showOutsideDays: { attribute: 'show-outside-days', converter: trueByDefault },\n    captionLayout: { attribute: 'caption-layout' },\n    locale: {},\n    weekStartsOn: { attribute: 'week-starts-on', type: Number },\n    required: { type: Boolean, reflect: true },\n    min: { type: Number },\n    max: { type: Number },\n    disableNavigation: { attribute: 'disable-navigation', type: Boolean },\n    fixedWeeks: { attribute: 'fixed-weeks', type: Boolean },\n    showWeekNumber: { attribute: 'show-week-number', type: Boolean },\n    formatters: { attribute: false },\n    footer: {},\n    accessibleLabel: { attribute: 'aria-label' },\n    firstMonth: { state: true },\n    focusedDay: { state: true },\n    hasFooter: { state: true },\n  }\n\n  mode?: CalendarMode\n  selected: CalendarSelected = undefined\n  disabled?: Matcher | Matcher[]\n  modifiers?: CalendarModifiers\n  modifiersClassNames?: CalendarModifiersClassNames\n  month?: Date\n  defaultMonth?: Date\n  startMonth?: Date\n  endMonth?: Date\n  today?: Date\n  numberOfMonths = 1\n  showOutsideDays = true\n  captionLayout: 'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years' = 'label'\n  locale = 'en-US'\n  weekStartsOn?: number\n  required = false\n  min?: number\n  max?: number\n  disableNavigation = false\n  fixedWeeks = false\n  showWeekNumber = false\n  formatters?: CalendarFormatters\n  footer?: string\n  accessibleLabel?: string\n  private firstMonth?: Date\n  private focusedDay?: Date\n  private hasFooter = false\n  private childObserver?: MutationObserver\n  private lastFocused?: Date\n  private pendingFocus = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'calendar')\n    injectMotionStyles()\n    // Frameworks may slot the footer after the host connects — re-render so it appears.\n    this.childObserver = new MutationObserver(() => this.requestUpdate())\n    this.childObserver.observe(this, { childList: true })\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.childObserver?.disconnect()\n  }\n\n  // --- derived --------------------------------------------------------------\n  private get todayDate() {\n    return toDate(this.today) ?? toDate(new Date())!\n  }\n  private get weekStart() {\n    return this.weekStartsOn ?? localeWeekStart(this.locale)\n  }\n  private get hasYearDropdown() {\n    return this.captionLayout === 'dropdown' || this.captionLayout === 'dropdown-years'\n  }\n  /** react-day-picker's getNavMonths. */\n  private get navRange(): [Date | undefined, Date | undefined] {\n    const s = toDate(this.startMonth)\n    const e = toDate(this.endMonth)\n    return [\n      s ? startOfMonth(s) : this.hasYearDropdown ? startOfYear(addYears(this.todayDate, -100)) : undefined,\n      e ? endOfMonth(e) : this.hasYearDropdown ? endOfYear(this.todayDate) : undefined,\n    ]\n  }\n\n  private get selectedDates(): Date[] {\n    const v = this.selected\n    if (v == null || v === '') return []\n    if (Array.isArray(v)) return v.map(toDate).filter((d): d is Date => !!d)\n    if (v instanceof Date || typeof v === 'string') {\n      const d = toDate(v)\n      return d ? [d] : []\n    }\n    return [v.from, v.to].map(toDate).filter((d): d is Date => !!d)\n  }\n  private get selectedSingle() {\n    return this.selectedDates[0]\n  }\n  private get selectedRange(): DateRange | undefined {\n    const v = this.selected\n    if (v == null || v === '') return undefined\n    if (Array.isArray(v)) return { from: toDate(v[0]), to: toDate(v[1]) }\n    if (v instanceof Date || typeof v === 'string') return { from: toDate(v), to: undefined }\n    return { from: toDate(v.from), to: toDate(v.to) }\n  }\n  private isSelected(date: Date) {\n    if (this.mode === 'range') {\n      const r = this.selectedRange\n      return !!r && rangeIncludesDate(r, date)\n    }\n    if (this.mode === 'single') return !!this.selectedSingle && isSameDay(this.selectedSingle, date)\n    if (this.mode === 'multiple') return this.selectedDates.some((d) => isSameDay(d, date))\n    return false\n  }\n\n  private initialMonth() {\n    const [navStart, navEnd] = this.navRange\n    let m = toDate(this.month) ?? toDate(this.defaultMonth) ?? this.todayDate\n    const n = this.numberOfMonths || 1\n    if (navEnd && differenceInCalendarMonths(navEnd, m) < n - 1) m = addMonths(navEnd, -(n - 1))\n    if (navStart && differenceInCalendarMonths(m, navStart) < 0) m = navStart\n    return startOfMonth(m)\n  }\n\n  protected willUpdate(changed: PropertyValues<this>) {\n    if (!this.firstMonth || changed.has('month')) this.firstMonth = this.initialMonth()\n  }\n\n  private get displayMonths() {\n    const [, navEnd] = this.navRange\n    const n = this.numberOfMonths || 1\n    const out: Date[] = []\n    for (let i = 0; i < n; i++) {\n      const m = addMonths(this.firstMonth!, i)\n      if (navEnd && m > navEnd) break\n      out.push(m)\n    }\n    return out\n  }\n\n  private weeksOf(month: Date): Day[][] {\n    const ws = this.weekStart\n    const first = startOfWeek(month, ws)\n    let last = endOfWeek(endOfMonth(month), ws)\n    const e = toDate(this.endMonth)\n    if (e) {\n      const limit = endOfWeek(endOfMonth(e), ws)\n      if (last > limit) last = limit\n    }\n    const weeks: Day[][] = []\n    const pushWeek = (start: Date) =>\n      weeks.push(\n        Array.from({ length: 7 }, (_, i) => {\n          const date = addDays(start, i)\n          return { date, displayMonth: month, outside: !isSameMonth(date, month) }\n        }),\n      )\n    for (let d = first; d <= last; d = addDays(d, 7)) pushWeek(d)\n    // RDP `fixedWeeks`: always 6 weeks, padding with outside days (even across\n    // endMonth, like RDP filling 42 days).\n    while (this.fixedWeeks && weeks.length < 6) {\n      const prev = weeks[weeks.length - 1]!\n      pushWeek(addDays(prev[6]!.date, 1))\n    }\n    return weeks\n  }\n\n  private modifiersOf(day: Day): Modifiers {\n    const [navStart, navEnd] = this.navRange\n    const { date, outside } = day\n    const hidden =\n      (!!navStart && date < startOfMonth(navStart)) ||\n      (!!navEnd && date > endOfMonth(navEnd)) ||\n      (!this.showOutsideDays && outside)\n    const selected = this.isSelected(date)\n    const mods: Modifiers = {\n      focused: !hidden && !!this.focusedDay && isSameDay(this.focusedDay, date) && !outside,\n      disabled: matches(date, this.disabled),\n      hidden,\n      outside,\n      today: isSameDay(date, this.todayDate),\n      selected,\n      range_start: false,\n      range_end: false,\n      range_middle: false,\n    }\n    if (this.mode === 'range') {\n      const r = this.selectedRange\n      if (r) {\n        mods.range_start = !!(r.from && r.to && isSameDay(date, r.from))\n        mods.range_end = !!(r.from && r.to && isSameDay(date, r.to))\n        mods.range_middle = rangeIncludesDate(r, date, true)\n      }\n    }\n    // Custom modifiers (RDP `modifiers`): a truthy matcher overrides the flag\n    // of the same name — including built-ins — falsy values are ignored.\n    if (this.modifiers) {\n      for (const [name, matcher] of Object.entries(this.modifiers)) {\n        if (!matcher) continue\n        mods[name] = matches(date, matcher)\n      }\n    }\n    return mods\n  }\n\n  /** RDP `getClassNamesForModifiers`: modifiersClassNames wins over the built-in class for the same key. */\n  private dayClasses(mods: Modifiers): string {\n    const cls = [classNames.day]\n    for (const k of builtinModifierNames) {\n      if (!mods[k]) continue\n      cls.push(this.modifiersClassNames?.[k] ?? classNames[k])\n    }\n    if (this.modifiersClassNames) {\n      for (const [name, active] of Object.entries(mods)) {\n        if (!active || (builtinModifierNames as readonly string[]).includes(name)) continue\n        const custom = this.modifiersClassNames[name]\n        if (custom) cls.push(custom)\n      }\n    }\n    return cls.join(' ')\n  }\n\n  /** react-day-picker's calculateFocusTarget: focused > last focused > selected > today > first enabled. */\n  private focusTarget(days: Day[]): Day | undefined {\n    let target: Day | undefined\n    let prio = -1\n    for (const day of days) {\n      const m = this.modifiersOf(day)\n      if (m.disabled || m.hidden || m.outside) continue\n      if (m.focused && prio < 3) (target = day), (prio = 3)\n      else if (this.lastFocused && isSameDay(this.lastFocused, day.date) && prio < 2) (target = day), (prio = 2)\n      else if (m.selected && prio < 1) (target = day), (prio = 1)\n      else if (m.today && prio < 0) (target = day), (prio = 0)\n    }\n    return target ?? days.find((d) => {\n      const m = this.modifiersOf(d)\n      return !m.disabled && !m.hidden && !m.outside\n    })\n  }\n\n  // --- navigation -----------------------------------------------------------\n  private get previousMonth() {\n    if (this.disableNavigation) return undefined\n    const [navStart] = this.navRange\n    const m = this.firstMonth!\n    if (navStart && differenceInCalendarMonths(m, navStart) <= 0) return undefined\n    return addMonths(m, -1)\n  }\n  private get nextMonth() {\n    if (this.disableNavigation) return undefined\n    const [, navEnd] = this.navRange\n    const m = this.firstMonth!\n    if (navEnd && differenceInCalendarMonths(navEnd, m) < (this.numberOfMonths || 1)) return undefined\n    return addMonths(m, 1)\n  }\n\n  private goToMonth(date: Date) {\n    if (this.disableNavigation) return\n    const [navStart, navEnd] = this.navRange\n    let m = startOfMonth(date)\n    if (navStart && m < startOfMonth(navStart)) m = startOfMonth(navStart)\n    if (navEnd && m > startOfMonth(navEnd)) m = startOfMonth(navEnd)\n    this.firstMonth = m\n    this.dispatchEvent(new CustomEvent('month-change', { detail: { month: m }, bubbles: true, composed: true }))\n  }\n\n  private moveFocus(moveBy: MoveBy, dir: 'before' | 'after') {\n    if (!this.focusedDay) return\n    const [navStart, navEnd] = this.navRange\n    const sign = dir === 'after' ? 1 : -1\n    let date = this.focusedDay\n    for (let attempt = 0; attempt <= 365; attempt++) {\n      const fns: Record<MoveBy, (d: Date) => Date> = {\n        day: (d) => addDays(d, sign),\n        week: (d) => addWeeks(d, sign),\n        month: (d) => addMonths(d, sign),\n        year: (d) => addYears(d, sign),\n        startOfWeek: (d) => startOfWeek(d, this.weekStart),\n        endOfWeek: (d) => endOfWeek(d, this.weekStart),\n      }\n      date = fns[moveBy](date)\n      if (dir === 'before' && navStart && date < navStart) date = navStart\n      if (dir === 'after' && navEnd && date > navEnd) date = navEnd\n      if (!matches(date, this.disabled)) break\n      if (attempt === 365) return\n    }\n    const inView = this.displayMonths.some((m) => isSameMonth(m, date))\n    if (!inView) {\n      if (this.disableNavigation) return\n      this.goToMonth(date)\n    }\n    this.focusedDay = date\n    this.pendingFocus = true\n  }\n\n  private onDayKeyDown(e: KeyboardEvent) {\n    const rtl = getComputedStyle(this).direction === 'rtl'\n    const map: Record<string, [MoveBy, 'before' | 'after']> = {\n      ArrowLeft: [e.shiftKey ? 'month' : 'day', rtl ? 'after' : 'before'],\n      ArrowRight: [e.shiftKey ? 'month' : 'day', rtl ? 'before' : 'after'],\n      ArrowDown: [e.shiftKey ? 'year' : 'week', 'after'],\n      ArrowUp: [e.shiftKey ? 'year' : 'week', 'before'],\n      PageUp: [e.shiftKey ? 'year' : 'month', 'before'],\n      PageDown: [e.shiftKey ? 'year' : 'month', 'after'],\n      Home: ['startOfWeek', 'before'],\n      End: ['endOfWeek', 'after'],\n    }\n    const step = map[e.key]\n    if (!step) return\n    e.preventDefault()\n    e.stopPropagation()\n    this.moveFocus(...step)\n  }\n\n  protected updated() {\n    if (!this.pendingFocus || !this.focusedDay) return\n    this.pendingFocus = false\n    this.renderRoot\n      .querySelector<HTMLButtonElement>(`td[data-day=\"${isoDate(this.focusedDay)}\"]:not([data-outside]) > button`)\n      ?.focus()\n  }\n\n  // --- selection ------------------------------------------------------------\n  private select(date: Date, mods: Modifiers) {\n    let next: Date | Date[] | DateRange | undefined\n    if (this.mode === 'single') {\n      const cur = this.selectedSingle\n      next = !this.required && cur && isSameDay(cur, date) ? undefined : date\n    } else if (this.mode === 'multiple') {\n      const cur = this.selectedDates\n      if (this.isSelected(date)) {\n        if (cur.length === this.min) return\n        if (this.required && cur.length === 1) return\n        next = cur.filter((d) => !isSameDay(d, date))\n      } else {\n        next = cur.length === this.max ? [date] : [...cur, date]\n      }\n    } else if (this.mode === 'range') {\n      next = this.addToRange(date, this.selectedRange)\n    } else {\n      return\n    }\n    const ev = new CustomEvent('select', {\n      detail: { selected: next, day: date, modifiers: mods },\n      bubbles: true,\n      composed: true,\n      cancelable: true,\n    })\n    if (this.dispatchEvent(ev)) this.selected = next\n  }\n\n  /** react-day-picker's addToRange. */\n  private addToRange(date: Date, range: DateRange | undefined): DateRange | undefined {\n    const min = this.min ?? 0\n    const max = this.max ?? 0\n    const { from, to } = range ?? {}\n    let r: DateRange | undefined\n    if (!from && !to) r = { from: date, to: min > 0 ? undefined : date }\n    else if (from && !to) {\n      if (isSameDay(from, date)) r = min === 0 ? { from, to: date } : this.required ? { from, to: undefined } : undefined\n      else if (date < from) r = { from: date, to: from }\n      else r = { from, to: date }\n    } else if (from && to) {\n      if (isSameDay(from, date) && isSameDay(to, date)) r = this.required ? { from, to } : undefined\n      else if (isSameDay(from, date)) r = { from, to: min > 0 ? undefined : date }\n      else if (isSameDay(to, date)) r = { from: date, to: min > 0 ? undefined : date }\n      else if (date < from) r = { from: date, to }\n      else r = { from, to: date }\n    }\n    if (r?.from && r.to) {\n      const diff = Math.round((r.to.getTime() - r.from.getTime()) / 86_400_000)\n      if (max > 0 && diff > max) r = { from: date, to: undefined }\n      else if (min > 1 && diff < min) r = { from: date, to: undefined }\n    }\n    return r\n  }\n\n  // --- render ---------------------------------------------------------------\n  private chevron(orientation: 'left' | 'right' | 'down') {\n    // React's custom Chevron: ChevronLeft for \"left\", ChevronRight otherwise\n    // (so the dropdown's \"down\" chevron is a right chevron in React too).\n    return orientation === 'left'\n      ? icon(ChevronLeft, 'chevron-left', cn('size-4', classNames.chevron))\n      : icon(ChevronRight, 'chevron-right', cn('size-4', classNames.chevron))\n  }\n\n  private formatCaption(month: Date) {\n    return this.formatters?.formatCaption?.(month, { locale: this.locale }) ?? formatMonthYear(month, this.locale)\n  }\n\n  private formatDay(date: Date) {\n    return this.formatters?.formatDay?.(date, { locale: this.locale }) ?? String(date.getDate())\n  }\n\n  private formatWeekdayName(weekday: Date) {\n    return (\n      this.formatters?.formatWeekdayName?.(weekday, { locale: this.locale }) ??\n      weekday.toLocaleDateString(this.locale, { weekday: 'narrow' })\n    )\n  }\n\n  private formatWeekNumber(weekNumber: number) {\n    return (\n      this.formatters?.formatWeekNumber?.(weekNumber, { locale: this.locale }) ?? String(weekNumber).padStart(2, '0')\n    )\n  }\n\n  private renderCaption(month: Date, monthOffset: number) {\n    const caption = this.formatCaption(month)\n    if (!this.captionLayout.startsWith('dropdown')) {\n      return html`<span class=${classNames.caption_label} role=\"status\" aria-live=\"polite\">${caption}</span>`\n    }\n    const [navStart, navEnd] = this.navRange\n    const showMonths = this.captionLayout === 'dropdown' || this.captionLayout === 'dropdown-months'\n    const showYears = this.hasYearDropdown\n    const monthLabel = (d: Date) => new Intl.DateTimeFormat(this.locale, { month: 'long' }).format(d)\n    const monthOptions = Array.from({ length: 12 }, (_, i) => {\n      const m = new Date(month.getFullYear(), i, 1)\n      return {\n        value: i,\n        label: monthLabel(m),\n        disabled: (!!navStart && m < startOfMonth(navStart)) || (!!navEnd && m > startOfMonth(navEnd)),\n      }\n    })\n    const yearOptions =\n      navStart && navEnd\n        ? Array.from({ length: navEnd.getFullYear() - navStart.getFullYear() + 1 }, (_, i) => {\n            const y = navStart.getFullYear() + i\n            return { value: y, label: String(y), disabled: false }\n          })\n        : []\n    const dropdown = (\n      kind: 'months_dropdown' | 'years_dropdown',\n      label: string,\n      options: { value: number; label: string; disabled: boolean }[],\n      value: number,\n      onChange: (v: number) => void,\n    ) => {\n      const current = options.find((o) => o.value === value)\n      return html`<span data-disabled=${String(this.disableNavigation)} class=${classNames.dropdown_root}>\n        <select\n          class=${[classNames.dropdown, classNames[kind]].join(' ')}\n          aria-label=${label}\n          ?disabled=${this.disableNavigation}\n          .value=${String(value)}\n          @change=${(e: Event) => onChange(Number((e.target as HTMLSelectElement).value))}\n        >\n          ${options.map(\n            (o) => html`<option value=${o.value} ?disabled=${o.disabled} ?selected=${o.value === value}>${o.label}</option>`,\n          )}\n        </select>\n        <span class=${classNames.caption_label} aria-hidden=\"true\">${current?.label}${this.chevron('down')}</span>\n      </span>`\n    }\n    const monthCtl = showMonths\n      ? dropdown('months_dropdown', 'Choose the Month', monthOptions, month.getMonth(), (v) =>\n          this.goToMonth(addMonths(new Date(month.getFullYear(), v, 1), -monthOffset)),\n        )\n      : html`<span>${monthLabel(month)}</span>`\n    const yearCtl = showYears\n      ? dropdown('years_dropdown', 'Choose the Year', yearOptions, month.getFullYear(), (v) =>\n          this.goToMonth(addMonths(new Date(v, month.getMonth(), 1), -monthOffset)),\n        )\n      : html`<span>${month.getFullYear()}</span>`\n    return html`<div class=${classNames.dropdowns}>\n      ${isYearFirst(this.locale) ? [yearCtl, monthCtl] : [monthCtl, yearCtl]}\n      <span role=\"status\" aria-live=\"polite\" class=\"sr-only\">${caption}</span>\n    </div>`\n  }\n\n  private renderDay(day: Day, target: Day | undefined) {\n    const mods = this.modifiersOf(day)\n    const interactive = this.mode !== undefined\n    const cls = this.dayClasses(mods)\n    const dayLabel = () => {\n      let l = formatFull(day.date, this.locale)\n      if (mods.today) l = `Today, ${l}`\n      if (mods.selected) l = `${l}, selected`\n      return l\n    }\n    const isTarget = !!target && target.date === day.date\n    return html`<td\n      class=${cls}\n      role=\"gridcell\"\n      aria-selected=${mods.selected ? 'true' : nothing}\n      aria-label=${!interactive && !mods.hidden\n        ? `${mods.today ? 'Today, ' : ''}${formatFull(day.date, this.locale)}`\n        : nothing}\n      data-day=${isoDate(day.date)}\n      data-month=${day.outside ? isoMonth(day.date) : nothing}\n      data-selected=${mods.selected ? 'true' : nothing}\n      data-disabled=${mods.disabled ? 'true' : nothing}\n      data-hidden=${mods.hidden ? 'true' : nothing}\n      data-outside=${day.outside ? 'true' : nothing}\n      data-focused=${mods.focused ? 'true' : nothing}\n      data-today=${mods.today ? 'true' : nothing}\n    >${mods.hidden\n      ? nothing\n      : interactive\n        ? html`<button\n            class=${classNames.day_button}\n            type=\"button\"\n            ?disabled=${!mods.focused && mods.disabled}\n            aria-disabled=${mods.focused && mods.disabled ? 'true' : nothing}\n            tabindex=${isTarget ? 0 : -1}\n            aria-label=${dayLabel()}\n            @click=${(e: Event) => {\n              e.preventDefault()\n              e.stopPropagation()\n              this.focusedDay = day.date\n              if (mods.disabled) return\n              this.select(day.date, mods)\n            }}\n            @focus=${() => (this.focusedDay = day.date)}\n            @blur=${() => {\n              this.lastFocused = this.focusedDay\n              this.focusedDay = undefined\n            }}\n            @keydown=${this.onDayKeyDown}\n          >${this.formatDay(day.date)}</button>`\n        : this.formatDay(day.date)}</td>`\n  }\n\n  render() {\n    const months = this.displayMonths\n    const monthsWeeks = months.map((m) => this.weeksOf(m))\n    const allDays = monthsWeeks.flat(2)\n    const target = this.focusTarget(allDays)\n    const prev = this.previousMonth\n    const next = this.nextMonth\n    const n = this.numberOfMonths || 1\n    const ws = this.weekStart\n    const weekdays = Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(this.todayDate, ws), i))\n\n    return html`<div\n      part=\"root\"\n      class=${cn(classNames.root, 'p-3')}\n      lang=${this.locale}\n      aria-label=${this.accessibleLabel ?? nothing}\n      data-mode=${this.mode ?? nothing}\n      data-required=${this.required ? 'true' : nothing}\n      data-multiple-months=${n > 1 ? 'true' : nothing}\n    >\n      <div class=${classNames.months}>\n        <nav class=${classNames.nav} aria-label=\"\">\n          <button\n            type=\"button\"\n            class=${classNames.button_previous}\n            tabindex=${prev ? nothing : -1}\n            aria-disabled=${prev ? nothing : 'true'}\n            aria-label=\"Go to the Previous Month\"\n            @click=${() => prev && this.goToMonth(prev)}\n          >${this.chevron('left')}</button>\n          <button\n            type=\"button\"\n            class=${classNames.button_next}\n            tabindex=${next ? nothing : -1}\n            aria-disabled=${next ? nothing : 'true'}\n            aria-label=\"Go to the Next Month\"\n            @click=${() => next && this.goToMonth(next)}\n          >${this.chevron('right')}</button>\n        </nav>\n        ${months.map(\n          (month, i) => html`<div class=${classNames.month}>\n            <div class=${classNames.month_caption}>${this.renderCaption(month, i)}</div>\n            <table\n              role=\"grid\"\n              aria-multiselectable=${this.mode === 'multiple' || this.mode === 'range' ? 'true' : 'false'}\n              aria-label=${this.formatCaption(month)}\n              class=${classNames.month_grid}\n            >\n              <thead aria-hidden=\"true\">\n                <tr class=${classNames.weekdays}>\n                  ${this.showWeekNumber\n                    ? html`<th aria-label=\"Week Number\" class=${classNames.week_number_header} scope=\"col\">\n                        ${this.formatters?.formatWeekNumberHeader?.() ?? ''}\n                      </th>`\n                    : nothing}\n                  ${weekdays.map(\n                    (w) => html`<th\n                      aria-label=${w.toLocaleDateString(this.locale, { weekday: 'long' })}\n                      class=${classNames.weekday}\n                      scope=\"col\"\n                    >${this.formatWeekdayName(w)}</th>`,\n                  )}\n                </tr>\n              </thead>\n              <tbody class=${classNames.weeks}>\n                ${monthsWeeks[i].map((week) => {\n                  // RDP groups days into rows by week number; rows here are fixed\n                  // 7-day chunks, so the number comes from the first day (identical\n                  // unless week-starts-on differs from the locale default at a\n                  // year boundary, where RDP would split the row).\n                  const weekNumber = getWeekNumber(week[0]!.date, ws)\n                  return html`<tr class=${classNames.week}>\n                    ${this.showWeekNumber\n                      ? html`<th scope=\"row\" role=\"rowheader\" aria-label=${`Week ${weekNumber}`} class=${classNames.week_number}\n                          >${this.formatWeekNumber(weekNumber)}</th\n                        >`\n                      : nothing}\n                    ${week.map((d) => this.renderDay(d, target))}\n                  </tr>`\n                })}\n              </tbody>\n            </table>\n          </div>`,\n        )}\n      </div>\n      ${this.footer != null || this.hasFooter || this.hasSlottedFooter\n        ? html`<div\n            part=\"footer\"\n            role=\"status\"\n            aria-live=\"polite\"\n            data-slot=\"calendar-footer\"\n            class=${classNames.footer}\n          >\n            <slot name=\"footer\" @slotchange=${this.onFooterSlotChange}>${this.footer}</slot>\n          </div>`\n        : nothing}\n    </div>`\n  }\n\n  private get hasSlottedFooter() {\n    return [...this.children].some((c) => c.getAttribute('slot') === 'footer')\n  }\n\n  private onFooterSlotChange(e: Event) {\n    this.hasFooter = (e.target as HTMLSlotElement)\n      .assignedNodes()\n      .some((n) => n.nodeType === 1 || n.textContent?.trim())\n  }\n}\n\ncustomElements.get('uip-calendar') || customElements.define('uip-calendar', UipCalendar)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-calendar': UipCalendar\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/calendar/calendar.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
