{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "range-calendar",
  "title": "Range Calendar",
  "type": "registry:ui",
  "description": "Inline from→to calendar grid. Click two dates; the range fills between. For a form field with a trigger, use Date Picker type=\"range\" instead. Same min/max and disabled-date support as Calendar.",
  "categories": [
    "date-time"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/range-calendar/range-calendar.ts",
      "content": "import { LitElement, css, html, 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'\n\n// ---------------------------------------------------------------------------\n// Local-date helpers (the subset of ../calendar/calendar-date <uip-calendar>\n// uses that a range-only grid needs — duplicated so the item stays standalone\n// instead of depending on the `calendar` item).\n// ---------------------------------------------------------------------------\n\nexport interface RangeCalendarRange {\n  from?: Date | string\n  to?: Date | string\n}\n\nexport type RangeCalendarMatcher =\n  | boolean\n  | Date\n  | Date[]\n  | RangeCalendarRange\n  | { dayOfWeek: number | number[] }\n  | { before: Date; after: Date }\n  | { before: Date }\n  | { after: Date }\n  | ((date: Date) => boolean)\n\nexport type RangeCalendarSelected = RangeCalendarRange | string | undefined\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 endOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth() + 1, 0)\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n)\nconst addWeeks = (d: Date, n: number) => addDays(d, n * 7)\nfunction 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}\nconst addYears = (d: Date, n: number) => addMonths(d, n * 12)\nconst isSameDay = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\nconst isSameMonth = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()\nconst differenceInCalendarDays = (a: Date, b: Date) =>\n  Math.round((startOfDay(a).getTime() - startOfDay(b).getTime()) / 86_400_000)\nconst differenceInCalendarMonths = (a: Date, b: Date) =>\n  (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth())\nfunction startOfWeek(d: Date, weekStartsOn: number) {\n  const diff = (d.getDay() - weekStartsOn + 7) % 7\n  return addDays(d, -diff)\n}\nconst endOfWeek = (d: Date, weekStartsOn: number) => addDays(startOfWeek(d, weekStartsOn), 6)\nconst pad = (n: number) => String(n).padStart(2, '0')\nconst isoDate = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\nconst isoMonth = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}`\n\ntype DayInput = Date | string | number | null | undefined\n\n/** Date, `yyyy-MM-dd` (read as a LOCAL date, not UTC midnight) or any Date-parsable string. */\nfunction 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\nfunction rangeIncludesDate(range: { from?: Date; to?: Date }, 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`. */\nfunction matches(date: Date, matchers: RangeCalendarMatcher | RangeCalendarMatcher[] | undefined): boolean {\n  if (matchers === undefined) return false\n  const list =\n    Array.isArray(matchers) && !matchers.every((m) => m instanceof Date) ? matchers : [matchers as RangeCalendarMatcher]\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 as (date: Date) => boolean)(date)\n    if (!m || typeof m !== 'object') return false\n    if ('from' in m || 'to' in m) {\n      const r = m as RangeCalendarRange\n      return rangeIncludesDate({ from: toDate(r.from), to: toDate(r.to) }, date)\n    }\n    if ('dayOfWeek' in m) {\n      const dow = (m as { dayOfWeek: number | number[] }).dayOfWeek\n      return Array.isArray(dow) ? dow.includes(date.getDay()) : dow === date.getDay()\n    }\n    const before = 'before' in m ? toDate((m as { before: unknown }).before) : undefined\n    const after = 'after' in m ? toDate((m as { after: unknown }).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 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 */\nfunction parseMatcher(value: string | null): RangeCalendarMatcher | RangeCalendarMatcher[] | 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 RangeCalendarMatcher | RangeCalendarMatcher[]\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/** date-fns `PPPP` (react-day-picker's day/gridcell label). */\nfunction formatFull(d: Date, locale: string) {\n  if (locale.startsWith('en')) {\n    const ordinal = (n: number) => {\n      const s = ['th', 'st', 'nd', 'rd']\n      const x = n % 100\n      return n + (s[(x - 20) % 10] || s[x] || s[0])\n    }\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` (react-day-picker's caption + grid label). */\nconst formatMonthYear = (d: Date, locale: string) =>\n  new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(d)\n\n// ---------------------------------------------------------------------------\n\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}\n\ninterface Day {\n  date: Date\n  displayMonth: Date\n  outside: boolean\n}\n\n// React's `classNames` map, verbatim (RangeCalendar.tsx). `root`/`weeks` keep\n// react-day-picker's `rdp-*` defaults (unstyled hooks), like <uip-calendar>.\nconst classNames = {\n  months: 'relative flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0',\n  month: 'flex flex-1 flex-col gap-4',\n  month_caption: 'flex items-center justify-center',\n  caption_label: 'text-sm font-medium',\n  nav: 'absolute inset-x-1 top-0 flex items-center justify-between',\n  button_previous: cn(buttonVariants({ variant: 'outline' }), 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100'),\n  button_next: cn(buttonVariants({ variant: 'outline' }), 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100'),\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  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(buttonVariants({ variant: 'ghost' }), 'size-9 cursor-pointer p-0 font-normal aria-selected:opacity-100'),\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  range_start:\n    'day-range-start rounded-l-md [&>button]:bg-primary [&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground [&>button:focus]:bg-primary [&>button:focus]:text-primary-foreground',\n  range_end:\n    'day-range-end rounded-r-md [&>button]:bg-primary [&>button]:text-primary-foreground [&>button:hover]:bg-primary [&>button:hover]:text-primary-foreground [&>button:focus]:bg-primary [&>button:focus]:text-primary-foreground',\n  range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground [&>button]:hover:bg-accent',\n  hidden: 'invisible',\n} as const\n\nconst matcherConverter = { fromAttribute: parseMatcher, toAttribute: () => null }\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-range-calendar> — the registry RangeCalendar (react-day-picker in\n * `mode=\"range\"` in React) as a web component with its own month-grid logic.\n *\n * Markup, classes, ARIA and data attributes follow what react-day-picker\n * renders with React's `classNames` map: root div > months > nav + month >\n * caption + `table[role=grid]` > `td[role=gridcell]` > day `<button>`. Range\n * start/end cells get the primary fill, the middle the accent fill. Keyboard\n * matches <uip-calendar>: arrows move by day/week (Shift: month/year), PageUp/\n * PageDown by month (Shift: year), Home/End to the week's start/end, Enter/\n * Space select.\n *\n * Properties / attributes (react-day-picker prop names, range-forced):\n *  - `selected`: { from, to } (Date or ISO `yyyy-MM-dd`); the attribute is a\n *    `from,to` ISO pair.\n *  - `disabled`: a Matcher (Date, Date[], { before }, { after }, { from, to },\n *    { dayOfWeek }, a function, `true`) or an array of them. The attribute\n *    takes JSON with ISO dates or a date list.\n *  - `month` / `default-month`, `start-month` / `end-month` (ISO or Date).\n *  - `number-of-months`, `show-outside-days` (default true), `locale`\n *    (BCP-47, default 'en-US'), `week-starts-on` (0–6, default 0 like\n *    react-day-picker), `required`, `min` / `max` (nights),\n *    `disable-navigation`, `fixed-weeks`, `today` (Date/ISO).\n *\n * Out of scope (react-day-picker chrome, documented instead of silently\n * dropped): `captionLayout` dropdowns (label captions always), `modifiers` /\n * `modifiersClassNames`, `formatters` (narrow weekday labels always),\n * `footer`, and week numbers — <uip-calendar> covers those for single dates.\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 UipRangeCalendar extends LitElement {\n  // React's root is a block <div>.\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    selected: {\n      converter: {\n        fromAttribute: (v: string | null): RangeCalendarSelected => {\n          if (v === null || v.trim() === '') return undefined\n          const [from, to] = v.split(/\\s*,\\s*/)\n          return { from, to }\n        },\n        toAttribute: () => null,\n      },\n    },\n    disabled: { converter: matcherConverter },\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    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    accessibleLabel: { attribute: 'aria-label' },\n    firstMonth: { state: true },\n    focusedDay: { state: true },\n  }\n\n  selected: RangeCalendarSelected = undefined\n  disabled?: RangeCalendarMatcher | RangeCalendarMatcher[]\n  month?: Date\n  defaultMonth?: Date\n  startMonth?: Date\n  endMonth?: Date\n  today?: Date\n  numberOfMonths = 1\n  showOutsideDays = true\n  locale = 'en-US'\n  weekStartsOn = 0\n  required = false\n  min?: number\n  max?: number\n  disableNavigation = false\n  fixedWeeks = false\n  accessibleLabel?: string\n  private firstMonth?: Date\n  private focusedDay?: Date\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', 'range-calendar')\n  }\n\n  // --- derived --------------------------------------------------------------\n  private get todayDate() {\n    return toDate(this.today) ?? toDate(new Date())!\n  }\n\n  /** react-day-picker's getNavMonths (no dropdowns: start/endMonth only). */\n  private get navRange(): [Date | undefined, Date | undefined] {\n    const s = toDate(this.startMonth)\n    const e = toDate(this.endMonth)\n    return [s ? startOfMonth(s) : undefined, e ? endOfMonth(e) : undefined]\n  }\n\n  private get selectedRange(): { from?: Date; to?: Date } {\n    const v = this.selected\n    if (v == null || v === '') return {}\n    if (typeof v === 'string') {\n      const [from, to] = v.split(/\\s*,\\s*/)\n      return { from: toDate(from), to: toDate(to) }\n    }\n    return { from: toDate(v.from as DayInput), to: toDate(v.to as DayInput) }\n  }\n\n  private isSelected(date: Date) {\n    const r = this.selectedRange\n    return rangeIncludesDate(r, date)\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.weekStartsOn\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    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 r = this.selectedRange\n    const selected = rangeIncludesDate(r, date)\n    return {\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: !!(r.from && r.to && isSameDay(date, r.from)),\n      range_end: !!(r.from && r.to && isSameDay(date, r.to)),\n      range_middle: rangeIncludesDate(r, date, true),\n    }\n  }\n\n  private dayClasses(mods: Modifiers): string {\n    const cls = [classNames.day]\n    if (mods.selected) cls.push('rdp-selected')\n    if (mods.range_start) cls.push(classNames.range_start)\n    if (mods.range_end) cls.push(classNames.range_end)\n    if (mods.range_middle) cls.push(classNames.range_middle)\n    if (mods.today) cls.push(classNames.today)\n    if (mods.outside) cls.push(classNames.outside)\n    if (mods.disabled) cls.push(classNames.disabled)\n    if (mods.hidden) cls.push(classNames.hidden)\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 (\n      target ??\n      days.find((d) => {\n        const m = this.modifiersOf(d)\n        return !m.disabled && !m.hidden && !m.outside\n      })\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\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.weekStartsOn),\n        endOfWeek: (d) => endOfWeek(d, this.weekStartsOn),\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    const next = this.addToRange(date, this.selectedRange)\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: { from?: Date; to?: Date }): { from?: Date; to?: Date } | undefined {\n    const min = this.min ?? 0\n    const max = this.max ?? 0\n    const { from, to } = range\n    let r: { from?: Date; to?: Date } | 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 renderDay(day: Day, target: Day | undefined) {\n    const mods = this.modifiersOf(day)\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      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    >\n      ${mods.hidden\n        ? nothing\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          >\n            ${day.date.getDate()}\n          </button>`}\n    </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.weekStartsOn\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('rdp-root', 'p-3')}\n      lang=${this.locale}\n      aria-label=${this.accessibleLabel ?? nothing}\n      data-mode=\"range\"\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          >\n            ${icon(ChevronLeft, 'chevron-left', cn('size-4', 'rdp-chevron'))}\n          </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          >\n            ${icon(ChevronRight, 'chevron-right', cn('size-4', 'rdp-chevron'))}\n          </button>\n        </nav>\n        ${months.map(\n          (month, mi) => html`<div class=${classNames.month}>\n            <div class=${classNames.month_caption}>\n              <span class=${classNames.caption_label} role=\"status\" aria-live=\"polite\">\n                ${formatMonthYear(month, this.locale)}\n              </span>\n            </div>\n            <table\n              role=\"grid\"\n              aria-multiselectable=\"true\"\n              aria-label=${formatMonthYear(month, this.locale)}\n              class=${classNames.month_grid}\n            >\n              <thead aria-hidden=\"true\">\n                <tr class=${classNames.weekdays}>\n                  ${weekdays.map(\n                    (w) => html`<th\n                      aria-label=${w.toLocaleDateString(this.locale, { weekday: 'long' })}\n                      class=${classNames.weekday}\n                      scope=\"col\"\n                    >\n                      ${w.toLocaleDateString(this.locale, { weekday: 'narrow' })}\n                    </th>`,\n                  )}\n                </tr>\n              </thead>\n              <tbody class=\"rdp-weeks\">\n                ${monthsWeeks[mi]!.map((week) => html`<tr class=${classNames.week}>\n                  ${week.map((d) => this.renderDay(d, target))}\n                </tr>`)}\n              </tbody>\n            </table>\n          </div>`,\n        )}\n      </div>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-range-calendar') || customElements.define('uip-range-calendar', UipRangeCalendar)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-range-calendar': UipRangeCalendar\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/range-calendar/range-calendar.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
