{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-time-core",
  "title": "Date Time Core",
  "description": "Pure date and time functions on ISO strings with Intl formatting, Intl calendars, presets, relative ranges and recurrence rules.",
  "files": [
    {
      "path": "registry/tealmac/date-time-core.ts",
      "content": "/**\n * Date and time core for the tealmac date-time components.\n *\n * Pure functions, no dependencies. Dates travel as ISO strings with no time\n * zone (\"2026-10-25\", \"2026-10-25T14:30\", \"14:30\", \"2026-10\", \"2026-Q4\",\n * \"2026-W43\"), so a value never shifts with the viewer's time zone. All\n * arithmetic runs in UTC at noon and all labels come from `Intl`, which also\n * gives the non-Gregorian calendars (Persian, Hijri, Hebrew, Buddhist, ...).\n */\n\nexport type ISODate = string\nexport type ISOTime = string\n\nexport type CalendarSystem =\n  | \"gregory\"\n  | \"buddhist\"\n  | \"chinese\"\n  | \"coptic\"\n  | \"ethiopic\"\n  | \"hebrew\"\n  | \"indian\"\n  | \"islamic-civil\"\n  | \"islamic-umalqura\"\n  | \"japanese\"\n  | \"persian\"\n  | \"roc\"\n\nexport const CALENDAR_SYSTEMS: { id: CalendarSystem; label: string }[] = [\n  { id: \"gregory\", label: \"Gregorian\" },\n  { id: \"buddhist\", label: \"Buddhist\" },\n  { id: \"chinese\", label: \"Chinese\" },\n  { id: \"coptic\", label: \"Coptic\" },\n  { id: \"ethiopic\", label: \"Ethiopic\" },\n  { id: \"hebrew\", label: \"Hebrew\" },\n  { id: \"indian\", label: \"Indian national\" },\n  { id: \"islamic-civil\", label: \"Hijri (civil)\" },\n  { id: \"islamic-umalqura\", label: \"Hijri (Umm al-Qura)\" },\n  { id: \"japanese\", label: \"Japanese\" },\n  { id: \"persian\", label: \"Persian\" },\n  { id: \"roc\", label: \"Minguo\" },\n]\n\n/* ------------------------------------------------------------------ */\n/* ISO dates                                                            */\n/* ------------------------------------------------------------------ */\n\nconst DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\nexport function daysInMonth(year: number, month: number) {\n  return new Date(Date.UTC(year, month, 0)).getUTCDate()\n}\n\nexport function isValidDateParts(y: number, m: number, d: number) {\n  return (\n    Number.isInteger(y) &&\n    y >= 1 &&\n    y <= 9999 &&\n    m >= 1 &&\n    m <= 12 &&\n    d >= 1 &&\n    d <= daysInMonth(y, m)\n  )\n}\n\nexport function parseDate(\n  iso: string | null | undefined\n): { y: number; m: number; d: number } | null {\n  const hit = iso ? DATE_RE.exec(iso) : null\n  if (!hit) return null\n  const [y, m, d] = [Number(hit[1]), Number(hit[2]), Number(hit[3])]\n  return isValidDateParts(y, m, d) ? { y, m, d } : null\n}\n\nconst pad = (n: number, len = 2) => String(n).padStart(len, \"0\")\n\nexport function toDate(y: number, m: number, d: number): ISODate {\n  return `${pad(y, 4)}-${pad(m)}-${pad(d)}`\n}\n\nexport function isValidDate(iso: string | null | undefined): iso is ISODate {\n  return parseDate(iso) !== null\n}\n\n/** The instant of an ISO date, at 12:00 UTC (safe from DST and time zones). */\nexport function toUTC(iso: ISODate) {\n  const p = parseDate(iso)\n  if (!p) throw new RangeError(`Invalid ISO date: ${iso}`)\n  return new Date(Date.UTC(p.y, p.m - 1, p.d, 12))\n}\n\nexport function fromUTC(date: Date): ISODate {\n  return toDate(\n    date.getUTCFullYear(),\n    date.getUTCMonth() + 1,\n    date.getUTCDate()\n  )\n}\n\n/** Today in the viewer's local calendar day. */\nexport function today(now: Date = new Date()): ISODate {\n  return toDate(now.getFullYear(), now.getMonth() + 1, now.getDate())\n}\n\nexport function addDays(iso: ISODate, n: number): ISODate {\n  const d = toUTC(iso)\n  d.setUTCDate(d.getUTCDate() + n)\n  return fromUTC(d)\n}\n\n/** Adds months, clamping the day (\"2026-01-31\" + 1 month = \"2026-02-28\"). */\nexport function addMonths(iso: ISODate, n: number): ISODate {\n  const p = parseDate(iso)!\n  const index = p.y * 12 + (p.m - 1) + n\n  const y = Math.floor(index / 12)\n  const m = (index % 12) + 1\n  return toDate(y, m, Math.min(p.d, daysInMonth(y, m)))\n}\n\nexport function addYears(iso: ISODate, n: number): ISODate {\n  return addMonths(iso, n * 12)\n}\n\nexport function diffDays(a: ISODate, b: ISODate) {\n  return Math.round((toUTC(b).getTime() - toUTC(a).getTime()) / 86_400_000)\n}\n\nexport function compareDates(a: ISODate, b: ISODate) {\n  return a < b ? -1 : a > b ? 1 : 0\n}\n\nexport function minDate(a: ISODate, b: ISODate) {\n  return a <= b ? a : b\n}\n\nexport function maxDate(a: ISODate, b: ISODate) {\n  return a >= b ? a : b\n}\n\nexport function clampDate(iso: ISODate, min?: ISODate, max?: ISODate) {\n  if (min && iso < min) return min\n  if (max && iso > max) return max\n  return iso\n}\n\n/** 0 = Sunday ... 6 = Saturday. */\nexport function weekday(iso: ISODate) {\n  return toUTC(iso).getUTCDay()\n}\n\nexport function startOfMonth(iso: ISODate) {\n  return `${iso.slice(0, 7)}-01`\n}\n\nexport function endOfMonth(iso: ISODate) {\n  const p = parseDate(iso)!\n  return toDate(p.y, p.m, daysInMonth(p.y, p.m))\n}\n\nexport function startOfWeek(iso: ISODate, firstDay = 0) {\n  return addDays(iso, -((weekday(iso) - firstDay + 7) % 7))\n}\n\nexport function endOfWeek(iso: ISODate, firstDay = 0) {\n  return addDays(startOfWeek(iso, firstDay), 6)\n}\n\nexport function quarterOf(iso: ISODate) {\n  return Math.floor((parseDate(iso)!.m - 1) / 3) + 1\n}\n\nexport function startOfQuarter(iso: ISODate) {\n  const p = parseDate(iso)!\n  return toDate(p.y, (quarterOf(iso) - 1) * 3 + 1, 1)\n}\n\nexport function endOfQuarter(iso: ISODate) {\n  return endOfMonth(addMonths(startOfQuarter(iso), 2))\n}\n\nexport function startOfYear(iso: ISODate) {\n  return `${iso.slice(0, 4)}-01-01`\n}\n\nexport function endOfYear(iso: ISODate) {\n  return `${iso.slice(0, 4)}-12-31`\n}\n\nexport function isWeekend(iso: ISODate, weekend: number[] = [0, 6]) {\n  return weekend.includes(weekday(iso))\n}\n\n/** Every date from `from` to `to`, both included. Empty when to < from. */\nexport function eachDay(from: ISODate, to: ISODate): ISODate[] {\n  const out: ISODate[] = []\n  for (let d = from; d <= to && out.length < 4000; d = addDays(d, 1))\n    out.push(d)\n  return out\n}\n\n/** Days in the range that are not in `weekend`, optionally minus holidays. */\nexport function countWorkingDays(\n  from: ISODate,\n  to: ISODate,\n  options: { weekend?: number[]; holidays?: ISODate[] } = {}\n) {\n  const { weekend = [0, 6], holidays = [] } = options\n  return eachDay(from, to).filter(\n    (d) => !isWeekend(d, weekend) && !holidays.includes(d)\n  ).length\n}\n\n/** Nights between check-in and check-out. */\nexport function nightsBetween(checkIn: ISODate, checkOut: ISODate) {\n  return Math.max(0, diffDays(checkIn, checkOut))\n}\n\n/** Whole years between a birth date and a reference date. */\nexport function ageOn(birth: ISODate, on: ISODate) {\n  const b = parseDate(birth)!\n  const o = parseDate(on)!\n  let age = o.y - b.y\n  if (o.m < b.m || (o.m === b.m && o.d < b.d)) age -= 1\n  return age\n}\n\n/** ISO 8601 week: Monday first, week 1 holds the first Thursday. */\nexport function isoWeek(iso: ISODate): { year: number; week: number } {\n  const d = toUTC(iso)\n  const day = d.getUTCDay() || 7\n  d.setUTCDate(d.getUTCDate() + 4 - day)\n  const year = d.getUTCFullYear()\n  const jan1 = Date.UTC(year, 0, 1, 12)\n  const week = Math.ceil(((d.getTime() - jan1) / 86_400_000 + 1) / 7)\n  return { year, week }\n}\n\nexport function weeksInIsoYear(year: number) {\n  return isoWeek(toDate(year, 12, 28)).week\n}\n\n/** The Monday that starts an ISO week. */\nexport function isoWeekStart(year: number, week: number): ISODate {\n  const jan4 = toDate(year, 1, 4)\n  const monday = addDays(jan4, -((weekday(jan4) + 6) % 7))\n  return addDays(monday, (week - 1) * 7)\n}\n\nexport function formatIsoWeek(year: number, week: number) {\n  return `${pad(year, 4)}-W${pad(week)}`\n}\n\nexport function parseIsoWeek(value: string | null | undefined) {\n  const hit = value ? /^(\\d{4})-W(\\d{2})$/.exec(value) : null\n  if (!hit) return null\n  const year = Number(hit[1])\n  const week = Number(hit[2])\n  return week >= 1 && week <= weeksInIsoYear(year) ? { year, week } : null\n}\n\nexport function parseMonth(value: string | null | undefined) {\n  const hit = value ? /^(\\d{4})-(\\d{2})$/.exec(value) : null\n  if (!hit) return null\n  const y = Number(hit[1])\n  const m = Number(hit[2])\n  return m >= 1 && m <= 12 && y >= 1 ? { y, m } : null\n}\n\nexport function toMonth(y: number, m: number) {\n  return `${pad(y, 4)}-${pad(m)}`\n}\n\nexport function parseQuarter(value: string | null | undefined) {\n  const hit = value ? /^(\\d{4})-Q([1-4])$/.exec(value) : null\n  return hit ? { y: Number(hit[1]), q: Number(hit[2]) } : null\n}\n\nexport function toQuarter(y: number, q: number) {\n  return `${pad(y, 4)}-Q${q}`\n}\n\n/* ------------------------------------------------------------------ */\n/* ISO times                                                            */\n/* ------------------------------------------------------------------ */\n\nconst TIME_RE = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/\n\nexport function parseTime(\n  value: string | null | undefined\n): { h: number; m: number; s: number } | null {\n  const hit = value ? TIME_RE.exec(value) : null\n  if (!hit) return null\n  const [h, m, s] = [Number(hit[1]), Number(hit[2]), Number(hit[3] ?? 0)]\n  return h < 24 && m < 60 && s < 60 ? { h, m, s } : null\n}\n\nexport function toTime(h: number, m: number, s?: number): ISOTime {\n  return s === undefined\n    ? `${pad(h)}:${pad(m)}`\n    : `${pad(h)}:${pad(m)}:${pad(s)}`\n}\n\nexport function isValidTime(value: string | null | undefined) {\n  return parseTime(value) !== null\n}\n\nexport function timeToMinutes(value: ISOTime) {\n  const p = parseTime(value)\n  return p ? p.h * 60 + p.m : NaN\n}\n\n/** Minutes since midnight to \"HH:mm\"; wraps past midnight. */\nexport function minutesToTime(total: number): ISOTime {\n  const t = ((Math.round(total) % 1440) + 1440) % 1440\n  return toTime(Math.floor(t / 60), t % 60)\n}\n\nexport function addMinutesToTime(value: ISOTime, minutes: number) {\n  return minutesToTime(timeToMinutes(value) + minutes)\n}\n\n/** Every slot start in [from, to) every `step` minutes. */\nexport function timeSlots(from: ISOTime, to: ISOTime, step: number): ISOTime[] {\n  const out: ISOTime[] = []\n  const end = timeToMinutes(to)\n  for (let t = timeToMinutes(from); t < end && step > 0; t += step)\n    out.push(minutesToTime(t))\n  return out\n}\n\nexport function parseDateTime(value: string | null | undefined) {\n  const hit = value\n    ? /^(\\d{4}-\\d{2}-\\d{2})T(\\d{2}:\\d{2}(?::\\d{2})?)$/.exec(value)\n    : null\n  if (!hit || !isValidDate(hit[1]) || !isValidTime(hit[2])) return null\n  return { date: hit[1], time: hit[2] }\n}\n\nexport function toDateTime(date: ISODate, time: ISOTime) {\n  return `${date}T${time}`\n}\n\n/** Minutes between two local date-times. */\nexport function diffMinutes(a: string, b: string) {\n  const pa = parseDateTime(a)!\n  const pb = parseDateTime(b)!\n  return (\n    diffDays(pa.date, pb.date) * 1440 +\n    timeToMinutes(pb.time) -\n    timeToMinutes(pa.time)\n  )\n}\n\nexport function addMinutesToDateTime(value: string, minutes: number) {\n  const p = parseDateTime(value)!\n  const total = timeToMinutes(p.time) + minutes\n  return toDateTime(\n    addDays(p.date, Math.floor(total / 1440)),\n    minutesToTime(total)\n  )\n}\n\n/** Rounds up to the next multiple of `step` minutes. */\nexport function ceilTime(value: ISOTime, step: number) {\n  const m = timeToMinutes(value)\n  return minutesToTime(Math.ceil(m / step) * step)\n}\n\n/** The local date-time of a Date, \"YYYY-MM-DDTHH:mm\". */\nexport function nowLocal(now: Date = new Date()) {\n  return toDateTime(today(now), toTime(now.getHours(), now.getMinutes()))\n}\n\n/* ------------------------------------------------------------------ */\n/* Durations                                                            */\n/* ------------------------------------------------------------------ */\n\nexport interface Duration {\n  days: number\n  hours: number\n  minutes: number\n  seconds: number\n}\n\nexport function durationToSeconds(d: Partial<Duration>) {\n  return (\n    (d.days ?? 0) * 86400 +\n    (d.hours ?? 0) * 3600 +\n    (d.minutes ?? 0) * 60 +\n    (d.seconds ?? 0)\n  )\n}\n\nexport function secondsToDuration(total: number): Duration {\n  const t = Math.max(0, Math.round(total))\n  return {\n    days: Math.floor(t / 86400),\n    hours: Math.floor((t % 86400) / 3600),\n    minutes: Math.floor((t % 3600) / 60),\n    seconds: t % 60,\n  }\n}\n\n/** ISO 8601 duration: \"P1DT2H30M\". Zero is \"PT0S\". */\nexport function formatISODuration(d: Partial<Duration>) {\n  const x = secondsToDuration(durationToSeconds(d))\n  const date = x.days ? `${x.days}D` : \"\"\n  const time = `${x.hours ? `${x.hours}H` : \"\"}${x.minutes ? `${x.minutes}M` : \"\"}${x.seconds ? `${x.seconds}S` : \"\"}`\n  return date || time ? `P${date}${time ? `T${time}` : \"\"}` : \"PT0S\"\n}\n\nexport function parseISODuration(\n  value: string | null | undefined\n): Duration | null {\n  const hit = value\n    ? /^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?$/.exec(value)\n    : null\n  if (!hit || value === \"P\" || value?.endsWith(\"T\")) return null\n  return {\n    days: Number(hit[1] ?? 0),\n    hours: Number(hit[2] ?? 0),\n    minutes: Number(hit[3] ?? 0),\n    seconds: Number(hit[4] ?? 0),\n  }\n}\n\n/** \"1 h 30 min\", localised unit names come from Intl.NumberFormat. */\nexport function formatDuration(\n  d: Partial<Duration>,\n  locale = \"en\",\n  style: \"narrow\" | \"short\" | \"long\" = \"short\",\n  /** \"hour\" keeps long totals in hours (\"45 hr\") instead of days. */\n  largest: \"day\" | \"hour\" = \"day\"\n) {\n  const x = secondsToDuration(durationToSeconds(d))\n  if (largest === \"hour\") {\n    x.hours += x.days * 24\n    x.days = 0\n  }\n  const parts: string[] = []\n  const unit = (value: number, unit: string) =>\n    new Intl.NumberFormat(locale, {\n      style: \"unit\",\n      unit,\n      unitDisplay: style,\n    }).format(value)\n  if (x.days) parts.push(unit(x.days, \"day\"))\n  if (x.hours) parts.push(unit(x.hours, \"hour\"))\n  if (x.minutes) parts.push(unit(x.minutes, \"minute\"))\n  if (x.seconds || parts.length === 0) parts.push(unit(x.seconds, \"second\"))\n  return parts.join(\" \")\n}\n\n/* ------------------------------------------------------------------ */\n/* Locale                                                               */\n/* ------------------------------------------------------------------ */\n\nexport function resolveLocale(locale?: string) {\n  if (locale) return locale\n  if (typeof navigator !== \"undefined\" && navigator.language)\n    return navigator.language\n  return \"en-US\"\n}\n\n/** First day of the week for a locale, 0 = Sunday ... 6 = Saturday. */\nexport function firstDayOfWeek(locale?: string): number {\n  try {\n    const l = new Intl.Locale(resolveLocale(locale)) as Intl.Locale & {\n      getWeekInfo?: () => { firstDay: number }\n      weekInfo?: { firstDay: number }\n    }\n    const info = l.getWeekInfo?.() ?? l.weekInfo\n    if (info) return info.firstDay % 7\n  } catch {\n    // fall through to the region table\n  }\n  const region = resolveLocale(locale).split(\"-\")[1]?.toUpperCase()\n  if (\n    region &&\n    [\"US\", \"CA\", \"JP\", \"IN\", \"BR\", \"MX\", \"IL\", \"KR\", \"TW\", \"PH\", \"ZA\"].includes(\n      region\n    )\n  )\n    return 0\n  if (\n    region &&\n    [\n      \"AE\",\n      \"AF\",\n      \"BH\",\n      \"DZ\",\n      \"EG\",\n      \"IQ\",\n      \"IR\",\n      \"JO\",\n      \"KW\",\n      \"LY\",\n      \"OM\",\n      \"QA\",\n      \"SA\",\n      \"SD\",\n      \"SY\",\n    ].includes(region)\n  )\n    return 6\n  return 1\n}\n\n/** Weekday names starting at `firstDay` (0 = Sunday). */\nexport function weekdayNames(\n  locale?: string,\n  firstDay = 0,\n  width: \"narrow\" | \"short\" | \"long\" = \"short\",\n  calendar: CalendarSystem = \"gregory\"\n) {\n  const f = new Intl.DateTimeFormat(\n    `${resolveLocale(locale)}-u-ca-${calendar}`,\n    {\n      weekday: width,\n      timeZone: \"UTC\",\n    }\n  )\n  // 2023-01-01 was a Sunday.\n  return Array.from({ length: 7 }, (_, i) =>\n    f.format(new Date(Date.UTC(2023, 0, 1 + ((firstDay + i) % 7), 12)))\n  )\n}\n\nexport function monthNames(\n  locale?: string,\n  width: \"narrow\" | \"short\" | \"long\" = \"long\"\n) {\n  const f = new Intl.DateTimeFormat(resolveLocale(locale), {\n    month: width,\n    timeZone: \"UTC\",\n  })\n  return Array.from({ length: 12 }, (_, i) =>\n    f.format(new Date(Date.UTC(2023, i, 1, 12)))\n  )\n}\n\nexport function uses12Hour(locale?: string) {\n  const { hourCycle } = new Intl.DateTimeFormat(resolveLocale(locale), {\n    hour: \"numeric\",\n  }).resolvedOptions()\n  return hourCycle === \"h11\" || hourCycle === \"h12\"\n}\n\nexport function dayPeriodLabels(locale?: string): [string, string] {\n  const f = new Intl.DateTimeFormat(resolveLocale(locale), {\n    hour: \"numeric\",\n    hour12: true,\n    timeZone: \"UTC\",\n  })\n  const label = (h: number) =>\n    f\n      .formatToParts(new Date(Date.UTC(2023, 0, 1, h)))\n      .find((p) => p.type === \"dayPeriod\")?.value ?? (h < 12 ? \"AM\" : \"PM\")\n  return [label(9), label(15)]\n}\n\nexport type DatePartKind = \"day\" | \"month\" | \"year\"\n\n/** Order and separators of the date fields for a locale: [\"month\",\"/\",\"day\",\"/\",\"year\"]. */\nexport function dateFieldOrder(\n  locale?: string\n): ({ kind: DatePartKind } | { literal: string })[] {\n  const parts = new Intl.DateTimeFormat(resolveLocale(locale), {\n    year: \"numeric\",\n    month: \"2-digit\",\n    day: \"2-digit\",\n    timeZone: \"UTC\",\n    numberingSystem: \"latn\",\n  } as Intl.DateTimeFormatOptions).formatToParts(\n    new Date(Date.UTC(2023, 10, 22, 12))\n  )\n  const out: ({ kind: DatePartKind } | { literal: string })[] = []\n  for (const p of parts) {\n    if (p.type === \"day\" || p.type === \"month\" || p.type === \"year\")\n      out.push({ kind: p.type })\n    else if (p.type === \"literal\") out.push({ literal: p.value.trim() || \" \" })\n  }\n  return out\n}\n\nexport function formatDate(\n  iso: ISODate,\n  locale?: string,\n  options: Intl.DateTimeFormatOptions = { dateStyle: \"medium\" },\n  calendar: CalendarSystem = \"gregory\"\n) {\n  return new Intl.DateTimeFormat(`${resolveLocale(locale)}-u-ca-${calendar}`, {\n    ...options,\n    timeZone: \"UTC\",\n  }).format(toUTC(iso))\n}\n\nexport function formatTime(\n  value: ISOTime,\n  locale?: string,\n  options: Intl.DateTimeFormatOptions = { timeStyle: \"short\" }\n) {\n  const p = parseTime(value)!\n  return new Intl.DateTimeFormat(resolveLocale(locale), {\n    ...options,\n    timeZone: \"UTC\",\n  }).format(new Date(Date.UTC(2023, 0, 1, p.h, p.m, p.s)))\n}\n\nexport function formatDateTime(\n  value: string,\n  locale?: string,\n  options: Intl.DateTimeFormatOptions = {\n    dateStyle: \"medium\",\n    timeStyle: \"short\",\n  }\n) {\n  const p = parseDateTime(value)!\n  const t = parseTime(p.time)!\n  const d = parseDate(p.date)!\n  return new Intl.DateTimeFormat(resolveLocale(locale), {\n    ...options,\n    timeZone: \"UTC\",\n  }).format(new Date(Date.UTC(d.y, d.m - 1, d.d, t.h, t.m, t.s)))\n}\n\nexport function formatDateRange(\n  from: ISODate,\n  to: ISODate,\n  locale?: string,\n  options: Intl.DateTimeFormatOptions = { dateStyle: \"medium\" }\n) {\n  return new Intl.DateTimeFormat(resolveLocale(locale), {\n    ...options,\n    timeZone: \"UTC\",\n  }).formatRange(toUTC(from), toUTC(to))\n}\n\n/* ------------------------------------------------------------------ */\n/* Time zones                                                           */\n/* ------------------------------------------------------------------ */\n\n/** The wall-clock \"YYYY-MM-DDTHH:mm\" of an instant in a time zone. */\nexport function wallClock(instant: Date, timeZone: string) {\n  const p = new Intl.DateTimeFormat(\"en-US\", {\n    timeZone,\n    hourCycle: \"h23\",\n    year: \"numeric\",\n    month: \"2-digit\",\n    day: \"2-digit\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n  }).formatToParts(instant)\n  const get = (t: string) => p.find((x) => x.type === t)!.value\n  return `${get(\"year\")}-${get(\"month\")}-${get(\"day\")}T${get(\"hour\") === \"24\" ? \"00\" : get(\"hour\")}:${get(\"minute\")}`\n}\n\n/** Offset of a zone at an instant, in minutes east of UTC. */\nexport function zoneOffsetMinutes(instant: Date, timeZone: string) {\n  const wall = wallClock(instant, timeZone)\n  const asUTC = Date.parse(`${wall}:00Z`)\n  return Math.round(\n    (asUTC - Math.floor(instant.getTime() / 60000) * 60000) / 60000\n  )\n}\n\n/** The instant of a wall-clock time in a time zone. */\nexport function zonedToInstant(local: string, timeZone: string): Date {\n  const guess = Date.parse(`${local}:00Z`)\n  let t = guess - zoneOffsetMinutes(new Date(guess), timeZone) * 60000\n  t = guess - zoneOffsetMinutes(new Date(t), timeZone) * 60000\n  return new Date(t)\n}\n\n/** Converts a local date-time in one zone to the wall clock in another. */\nexport function convertZone(local: string, from: string, to: string) {\n  return wallClock(zonedToInstant(local, from), to)\n}\n\nexport function formatOffset(minutes: number) {\n  const sign = minutes < 0 ? \"-\" : \"+\"\n  const abs = Math.abs(minutes)\n  return `UTC${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n}\n\n/* ------------------------------------------------------------------ */\n/* Non-Gregorian calendars (Intl)                                       */\n/* ------------------------------------------------------------------ */\n\nexport interface CalendarParts {\n  year: number\n  /** Month number as Intl reports it (Hebrew leap years have 13). */\n  month: number\n  day: number\n  /** \"Aban\", \"Rajab\": the month name in the locale. */\n  monthName: string\n  /** Stable key for grouping days of one month. */\n  monthKey: string\n}\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>()\nfunction cached(\n  locale: string,\n  calendar: CalendarSystem,\n  opts: Intl.DateTimeFormatOptions\n) {\n  const key = `${locale}|${calendar}|${JSON.stringify(opts)}`\n  let f = formatterCache.get(key)\n  if (!f) {\n    f = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}-nu-latn`, {\n      ...opts,\n      timeZone: \"UTC\",\n    })\n    formatterCache.set(key, f)\n  }\n  return f\n}\n\nexport function calendarParts(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  locale?: string\n): CalendarParts {\n  const loc = resolveLocale(locale)\n  const date = toUTC(iso)\n  const numeric = cached(\"en\", calendar, {\n    year: \"numeric\",\n    month: \"numeric\",\n    day: \"numeric\",\n  }).formatToParts(date)\n  const get = (t: string) =>\n    numeric.find(\n      (p) =>\n        p.type === t || ((p.type as string) === \"relatedYear\" && t === \"year\")\n    )?.value ?? \"0\"\n  const year = Number(get(\"year\").replace(/\\D/g, \"\"))\n  const month = Number(get(\"month\").replace(/\\D/g, \"\")) || 1\n  const monthName =\n    cached(loc, calendar, { month: \"long\" })\n      .formatToParts(date)\n      .find((p) => p.type === \"month\")?.value ?? String(month)\n  return {\n    year,\n    month,\n    day: Number(get(\"day\")),\n    monthName,\n    monthKey: `${year}-${month}-${monthName}`,\n  }\n}\n\n/** Year label with era where the calendar needs one (\"2569 BE\", \"R6\"). */\nexport function calendarYearLabel(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  locale?: string\n) {\n  return cached(resolveLocale(locale), calendar, { year: \"numeric\" }).format(\n    toUTC(iso)\n  )\n}\n\nexport function calendarMonthLabel(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  locale?: string\n) {\n  return cached(resolveLocale(locale), calendar, {\n    month: \"long\",\n    year: \"numeric\",\n  }).format(toUTC(iso))\n}\n\nexport function calendarDayLabel(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  locale?: string\n) {\n  return cached(resolveLocale(locale), calendar, { day: \"numeric\" }).format(\n    toUTC(iso)\n  )\n}\n\nexport function calendarFullLabel(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  locale?: string\n) {\n  return cached(resolveLocale(locale), calendar, { dateStyle: \"full\" }).format(\n    toUTC(iso)\n  )\n}\n\n/** First day of the calendar month that holds `iso`. */\nexport function calendarMonthStart(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\"\n) {\n  if (\n    calendar === \"gregory\" ||\n    calendar === \"buddhist\" ||\n    calendar === \"roc\" ||\n    calendar === \"japanese\"\n  )\n    return startOfMonth(iso)\n  let d = iso\n  for (let i = 0; i < 32 && calendarParts(d, calendar).day !== 1; i++)\n    d = addDays(d, -1)\n  return d\n}\n\n/** Last day of the calendar month that holds `iso`. */\nexport function calendarMonthEnd(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\"\n) {\n  if (\n    calendar === \"gregory\" ||\n    calendar === \"buddhist\" ||\n    calendar === \"roc\" ||\n    calendar === \"japanese\"\n  )\n    return endOfMonth(iso)\n  const start = calendarMonthStart(iso, calendar)\n  let d = addDays(start, 27)\n  for (\n    let i = 0;\n    i < 6 && calendarParts(addDays(d, 1), calendar).day !== 1;\n    i++\n  )\n    d = addDays(d, 1)\n  return d\n}\n\n/** The first day of the month `n` months away, in the given calendar. */\nexport function calendarShiftMonth(\n  iso: ISODate,\n  n: number,\n  calendar: CalendarSystem = \"gregory\"\n) {\n  let start = calendarMonthStart(iso, calendar)\n  for (let i = 0; i < Math.abs(n); i++) {\n    start =\n      n > 0\n        ? addDays(calendarMonthEnd(start, calendar), 1)\n        : calendarMonthStart(addDays(start, -1), calendar)\n  }\n  return start\n}\n\nexport interface CalendarWeek {\n  /** 1-based week of the displayed month grid. */\n  index: number\n  days: ISODate[]\n}\n\n/** Weeks of the month holding `iso`, padded with neighbouring days. */\nexport function calendarMonthGrid(\n  iso: ISODate,\n  calendar: CalendarSystem = \"gregory\",\n  firstDay = 0,\n  fixedWeeks = false\n): { start: ISODate; end: ISODate; weeks: CalendarWeek[] } {\n  const start = calendarMonthStart(iso, calendar)\n  const end = calendarMonthEnd(iso, calendar)\n  const first = startOfWeek(start, firstDay)\n  const last = endOfWeek(end, firstDay)\n  const all = eachDay(first, last)\n  const total = fixedWeeks ? 42 : all.length\n  const days = fixedWeeks ? eachDay(first, addDays(first, 41)) : all\n  const weeks: CalendarWeek[] = []\n  for (let i = 0; i < total; i += 7)\n    weeks.push({ index: i / 7 + 1, days: days.slice(i, i + 7) })\n  return { start, end, weeks }\n}\n\n/* ------------------------------------------------------------------ */\n/* Ranges and presets                                                   */\n/* ------------------------------------------------------------------ */\n\nexport interface DateRange {\n  from: ISODate\n  to: ISODate\n}\n\nexport type PresetId =\n  | \"today\"\n  | \"yesterday\"\n  | \"last-7-days\"\n  | \"last-14-days\"\n  | \"last-30-days\"\n  | \"last-90-days\"\n  | \"this-week\"\n  | \"last-week\"\n  | \"this-month\"\n  | \"last-month\"\n  | \"this-quarter\"\n  | \"last-quarter\"\n  | \"this-year\"\n  | \"last-year\"\n  | \"year-to-date\"\n  | \"last-12-months\"\n\nexport const PRESET_LABELS: Record<PresetId, string> = {\n  today: \"Today\",\n  yesterday: \"Yesterday\",\n  \"last-7-days\": \"Last 7 days\",\n  \"last-14-days\": \"Last 14 days\",\n  \"last-30-days\": \"Last 30 days\",\n  \"last-90-days\": \"Last 90 days\",\n  \"this-week\": \"This week\",\n  \"last-week\": \"Last week\",\n  \"this-month\": \"This month\",\n  \"last-month\": \"Last month\",\n  \"this-quarter\": \"This quarter\",\n  \"last-quarter\": \"Last quarter\",\n  \"this-year\": \"This year\",\n  \"last-year\": \"Last year\",\n  \"year-to-date\": \"Year to date\",\n  \"last-12-months\": \"Last 12 months\",\n}\n\n/** The range a preset stands for, counted from `now` (a Date or an ISO date). */\nexport function presetRange(\n  id: PresetId,\n  now: ISODate = today(),\n  firstDay = 1\n): DateRange {\n  const t = now\n  switch (id) {\n    case \"today\":\n      return { from: t, to: t }\n    case \"yesterday\":\n      return { from: addDays(t, -1), to: addDays(t, -1) }\n    case \"last-7-days\":\n      return { from: addDays(t, -6), to: t }\n    case \"last-14-days\":\n      return { from: addDays(t, -13), to: t }\n    case \"last-30-days\":\n      return { from: addDays(t, -29), to: t }\n    case \"last-90-days\":\n      return { from: addDays(t, -89), to: t }\n    case \"this-week\":\n      return { from: startOfWeek(t, firstDay), to: endOfWeek(t, firstDay) }\n    case \"last-week\": {\n      const s = addDays(startOfWeek(t, firstDay), -7)\n      return { from: s, to: addDays(s, 6) }\n    }\n    case \"this-month\":\n      return { from: startOfMonth(t), to: endOfMonth(t) }\n    case \"last-month\": {\n      const m = addMonths(startOfMonth(t), -1)\n      return { from: m, to: endOfMonth(m) }\n    }\n    case \"this-quarter\":\n      return { from: startOfQuarter(t), to: endOfQuarter(t) }\n    case \"last-quarter\": {\n      const q = addMonths(startOfQuarter(t), -3)\n      return { from: q, to: endOfQuarter(q) }\n    }\n    case \"this-year\":\n      return { from: startOfYear(t), to: endOfYear(t) }\n    case \"last-year\": {\n      const y = addYears(startOfYear(t), -1)\n      return { from: y, to: endOfYear(y) }\n    }\n    case \"year-to-date\":\n      return { from: startOfYear(t), to: t }\n    case \"last-12-months\":\n      return { from: addDays(addMonths(t, -12), 1), to: t }\n  }\n}\n\nexport function rangesEqual(\n  a: DateRange | null | undefined,\n  b: DateRange | null | undefined\n) {\n  return !!a && !!b && a.from === b.from && a.to === b.to\n}\n\n/** Sorts a pair so from <= to. */\nexport function normaliseRange(a: ISODate, b: ISODate): DateRange {\n  return a <= b ? { from: a, to: b } : { from: b, to: a }\n}\n\nexport function rangeLength(r: DateRange) {\n  return diffDays(r.from, r.to) + 1\n}\n\n/** The range of the same length that ends the day before `r` starts. */\nexport function previousPeriod(r: DateRange): DateRange {\n  const n = rangeLength(r)\n  return { from: addDays(r.from, -n), to: addDays(r.from, -1) }\n}\n\n/** The same range one year earlier. */\nexport function samePeriodLastYear(r: DateRange): DateRange {\n  return { from: addYears(r.from, -1), to: addYears(r.to, -1) }\n}\n\nexport function isInRange(iso: ISODate, r: DateRange | null | undefined) {\n  return !!r && iso >= r.from && iso <= r.to\n}\n\nexport function rangesOverlap(a: DateRange, b: DateRange) {\n  return a.from <= b.to && b.from <= a.to\n}\n\n/* Relative (\"super date picker\") expressions. */\n\nexport type RelativeUnit =\n  \"minute\" | \"hour\" | \"day\" | \"week\" | \"month\" | \"quarter\" | \"year\"\nexport type RelativeDirection = \"last\" | \"next\"\n\nexport interface RelativeRange {\n  direction: RelativeDirection\n  amount: number\n  unit: RelativeUnit\n  /** Round the edge of the window to the start/end of the unit. */\n  round?: boolean\n}\n\nconst UNIT_CODE: Record<RelativeUnit, string> = {\n  minute: \"m\",\n  hour: \"h\",\n  day: \"d\",\n  week: \"w\",\n  month: \"M\",\n  quarter: \"Q\",\n  year: \"y\",\n}\n\n/** \"now-15m\", \"now+2w\": the compact string form of a relative window edge. */\nexport function relativeExpression(r: RelativeRange) {\n  return `now${r.direction === \"last\" ? \"-\" : \"+\"}${r.amount}${UNIT_CODE[r.unit]}${r.round ? \"/\" + UNIT_CODE[r.unit] : \"\"}`\n}\n\nexport function parseRelativeExpression(value: string): RelativeRange | null {\n  const hit = /^now([-+])(\\d+)([mhdwMQy])(\\/[mhdwMQy])?$/.exec(value)\n  if (!hit) return null\n  const unit = (Object.keys(UNIT_CODE) as RelativeUnit[]).find(\n    (u) => UNIT_CODE[u] === hit[3]\n  )\n  if (!unit) return null\n  return {\n    direction: hit[1] === \"-\" ? \"last\" : \"next\",\n    amount: Number(hit[2]),\n    unit,\n    round: !!hit[4],\n  }\n}\n\nfunction shiftLocal(local: string, amount: number, unit: RelativeUnit) {\n  const p = parseDateTime(local)!\n  switch (unit) {\n    case \"minute\":\n      return addMinutesToDateTime(local, amount)\n    case \"hour\":\n      return addMinutesToDateTime(local, amount * 60)\n    case \"day\":\n      return toDateTime(addDays(p.date, amount), p.time)\n    case \"week\":\n      return toDateTime(addDays(p.date, amount * 7), p.time)\n    case \"month\":\n      return toDateTime(addMonths(p.date, amount), p.time)\n    case \"quarter\":\n      return toDateTime(addMonths(p.date, amount * 3), p.time)\n    case \"year\":\n      return toDateTime(addYears(p.date, amount), p.time)\n  }\n}\n\n/** The absolute window \"last 15 minutes\" / \"next 2 weeks\" stands for at `now`. */\nexport function resolveRelativeRange(\n  r: RelativeRange,\n  now: string = nowLocal()\n): { from: string; to: string } {\n  const edge = shiftLocal(\n    now,\n    r.direction === \"last\" ? -r.amount : r.amount,\n    r.unit\n  )\n  let from = r.direction === \"last\" ? edge : now\n  let to = r.direction === \"last\" ? now : edge\n  if (r.round) {\n    const day = (s: string) => parseDateTime(s)!.date\n    if ([\"day\", \"week\", \"month\", \"quarter\", \"year\"].includes(r.unit)) {\n      const rs = (iso: ISODate) =>\n        r.unit === \"day\"\n          ? iso\n          : r.unit === \"week\"\n            ? startOfWeek(iso, 1)\n            : r.unit === \"month\"\n              ? startOfMonth(iso)\n              : r.unit === \"quarter\"\n                ? startOfQuarter(iso)\n                : startOfYear(iso)\n      const re = (iso: ISODate) =>\n        r.unit === \"day\"\n          ? iso\n          : r.unit === \"week\"\n            ? endOfWeek(iso, 1)\n            : r.unit === \"month\"\n              ? endOfMonth(iso)\n              : r.unit === \"quarter\"\n                ? endOfQuarter(iso)\n                : endOfYear(iso)\n      from = toDateTime(rs(day(from)), \"00:00\")\n      to = toDateTime(re(day(to)), \"23:59\")\n    } else if (r.unit === \"hour\") {\n      from = from.slice(0, 14) + \"00\"\n      to = to.slice(0, 14) + \"59\"\n    }\n  }\n  return { from, to }\n}\n\nexport function describeRelativeRange(r: RelativeRange) {\n  const unit = `${r.unit}${r.amount === 1 ? \"\" : \"s\"}`\n  return `${r.direction === \"last\" ? \"Last\" : \"Next\"} ${r.amount} ${unit}`\n}\n\n/* ------------------------------------------------------------------ */\n/* Recurrence (RRULE subset)                                            */\n/* ------------------------------------------------------------------ */\n\nexport type Frequency = \"DAILY\" | \"WEEKLY\" | \"MONTHLY\" | \"YEARLY\"\nconst RRULE_DAYS = [\"SU\", \"MO\", \"TU\", \"WE\", \"TH\", \"FR\", \"SA\"]\n\nexport interface Recurrence {\n  freq: Frequency\n  interval: number\n  /** Weekly: 0 = Sunday ... 6 = Saturday. */\n  byDay?: number[]\n  /** Monthly: day of month, or -1 for the last day. */\n  byMonthDay?: number\n  count?: number\n  until?: ISODate\n}\n\nexport function formatRRule(r: Recurrence) {\n  const parts = [`FREQ=${r.freq}`]\n  if (r.interval > 1) parts.push(`INTERVAL=${r.interval}`)\n  if (r.freq === \"WEEKLY\" && r.byDay?.length)\n    parts.push(\n      `BYDAY=${[...r.byDay]\n        .sort((a, b) => a - b)\n        .map((d) => RRULE_DAYS[d])\n        .join(\",\")}`\n    )\n  if (r.freq === \"MONTHLY\" && r.byMonthDay)\n    parts.push(`BYMONTHDAY=${r.byMonthDay}`)\n  if (r.count) parts.push(`COUNT=${r.count}`)\n  else if (r.until) parts.push(`UNTIL=${r.until.replaceAll(\"-\", \"\")}`)\n  return parts.join(\";\")\n}\n\nexport function parseRRule(value: string): Recurrence | null {\n  const map = new Map(\n    value\n      .replace(/^RRULE:/, \"\")\n      .split(\";\")\n      .map((p) => p.split(\"=\") as [string, string])\n  )\n  const freq = map.get(\"FREQ\") as Frequency | undefined\n  if (!freq || ![\"DAILY\", \"WEEKLY\", \"MONTHLY\", \"YEARLY\"].includes(freq))\n    return null\n  const r: Recurrence = { freq, interval: Number(map.get(\"INTERVAL\") ?? 1) }\n  const by = map.get(\"BYDAY\")\n  if (by)\n    r.byDay = by\n      .split(\",\")\n      .map((d) => RRULE_DAYS.indexOf(d))\n      .filter((i) => i >= 0)\n  if (map.get(\"BYMONTHDAY\")) r.byMonthDay = Number(map.get(\"BYMONTHDAY\"))\n  if (map.get(\"COUNT\")) r.count = Number(map.get(\"COUNT\"))\n  const until = map.get(\"UNTIL\")\n  if (until && /^\\d{8}$/.test(until))\n    r.until = `${until.slice(0, 4)}-${until.slice(4, 6)}-${until.slice(6, 8)}`\n  return r\n}\n\n/** The next `limit` occurrence dates of a rule, from `start` (included when it matches). */\nexport function occurrences(\n  r: Recurrence,\n  start: ISODate,\n  limit = 5\n): ISODate[] {\n  const out: ISODate[] = []\n  const max = r.count ?? limit\n  const interval = Math.max(1, r.interval)\n  let guard = 0\n  const push = (d: ISODate) => {\n    if (r.until && d > r.until) return false\n    out.push(d)\n    return out.length < Math.min(max, limit)\n  }\n  if (r.freq === \"DAILY\") {\n    for (let d = start; guard++ < 4000; d = addDays(d, interval))\n      if (!push(d)) break\n  } else if (r.freq === \"WEEKLY\") {\n    const days = r.byDay?.length\n      ? [...r.byDay].sort((a, b) => a - b)\n      : [weekday(start)]\n    const weekStart = startOfWeek(start, 1)\n    outer: for (let w = 0; guard++ < 1000; w += interval) {\n      for (const wd of days) {\n        const d = addDays(weekStart, w * 7 + ((wd + 6) % 7))\n        if (d < start) continue\n        if (!push(d)) break outer\n      }\n    }\n  } else if (r.freq === \"MONTHLY\") {\n    const p = parseDate(start)!\n    for (let i = 0; guard++ < 1000; i += interval) {\n      const base = addMonths(toDate(p.y, p.m, 1), i)\n      const bp = parseDate(base)!\n      const dim = daysInMonth(bp.y, bp.m)\n      const want = r.byMonthDay ?? p.d\n      const day = want === -1 ? dim : want\n      if (day > dim) continue\n      const d = toDate(bp.y, bp.m, day)\n      if (d < start) continue\n      if (!push(d)) break\n    }\n  } else {\n    const p = parseDate(start)!\n    for (let i = 0; guard++ < 1000; i += interval) {\n      const y = p.y + i\n      if (!isValidDateParts(y, p.m, p.d)) continue\n      if (!push(toDate(y, p.m, p.d))) break\n    }\n  }\n  return out\n}\n\nexport function describeRecurrence(r: Recurrence, locale?: string) {\n  const every = r.interval > 1 ? `Every ${r.interval} ` : \"Every \"\n  const unit = {\n    DAILY: \"day\",\n    WEEKLY: \"week\",\n    MONTHLY: \"month\",\n    YEARLY: \"year\",\n  }[r.freq]\n  let text = r.interval > 1 ? `${every}${unit}s` : `${every}${unit}`\n  if (r.freq === \"WEEKLY\" && r.byDay?.length)\n    text += ` on ${[...r.byDay]\n      .sort((a, b) => a - b)\n      .map((d) => weekdayNames(locale, 0, \"long\")[d])\n      .join(\", \")}`\n  if (r.freq === \"MONTHLY\" && r.byMonthDay)\n    text += r.byMonthDay === -1 ? \" on the last day\" : ` on day ${r.byMonthDay}`\n  if (r.count) text += `, ${r.count} time${r.count === 1 ? \"\" : \"s\"}`\n  else if (r.until) text += `, until ${formatDate(r.until, locale)}`\n  return text\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}