{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "persian-date",
  "title": "Persian Date",
  "description": "Jalali/Gregorian date utilities built on date-fns and date-fns-jalali: formatting, parsing, conversion, ranges, and digit conversion, switchable with a calendarType prop.",
  "dependencies": [
    "date-fns",
    "date-fns-jalali"
  ],
  "registryDependencies": [
    "https://ui.persian-labs.ir/r/normalize-persian-digits.json"
  ],
  "files": [
    {
      "path": "registry/base/lib/persian-date.ts",
      "content": "import * as gregorian from \"date-fns\"\nimport { enUS as gregorianEnUS, faIR as gregorianFaIR } from \"date-fns/locale\"\nimport * as jalali from \"date-fns-jalali\"\nimport { enUS as jalaliEnUS, faIR as jalaliFaIR } from \"date-fns-jalali/locale\"\n\nimport { normalizePersianDigits } from \"@/lib/normalize-persian-digits\"\n\n/**\n * \"shamsi\" is the Jalali/Solar Hijri calendar (default, Iran-first).\n * \"miladi\" is the Gregorian calendar.\n */\nexport type CalendarType = \"shamsi\" | \"miladi\"\n\nexport type DateLocale = \"fa\" | \"en\"\n\nexport type DigitStyle = \"fa\" | \"en\"\n\nexport interface DateParts {\n  /** 1-indexed month, unlike the underlying date-fns libraries. */\n  year: number\n  month: number\n  day: number\n}\n\n/** Matches react-day-picker's own DateRange shape exactly, for drop-in compatibility. */\nexport interface DateRange {\n  from: Date | undefined\n  to?: Date | undefined\n}\n\nconst persianDigitMap = [\"۰\", \"۱\", \"۲\", \"۳\", \"۴\", \"۵\", \"۶\", \"۷\", \"۸\", \"۹\"]\n\n/** Converts every plain 0-9 digit in a string to its Persian (۰-۹) equivalent. */\nexport function toPersianDigits(value: string): string {\n  return value.replace(/[0-9]/g, (digit) => persianDigitMap[Number(digit)]!)\n}\n\n/** Converts Persian/Arabic-Indic digits back to plain 0-9. Alias of normalizePersianDigits. */\nexport const toLatinDigits = normalizePersianDigits\n\nfunction calendarLib(calendarType: CalendarType) {\n  return calendarType === \"shamsi\" ? jalali : gregorian\n}\n\nfunction calendarLocale(calendarType: CalendarType, locale: DateLocale) {\n  if (calendarType === \"shamsi\") {\n    return locale === \"en\" ? jalaliEnUS : jalaliFaIR\n  }\n  return locale === \"en\" ? gregorianEnUS : gregorianFaIR\n}\n\nfunction defaultLocale(calendarType: CalendarType): DateLocale {\n  return calendarType === \"shamsi\" ? \"fa\" : \"en\"\n}\n\nexport interface DateOptions {\n  /** @default \"shamsi\" */\n  calendarType?: CalendarType\n  /** Month/weekday names. @default calendarType === \"shamsi\" ? \"fa\" : \"en\" */\n  locale?: DateLocale\n  /** Output digit style. @default calendarType === \"shamsi\" ? \"fa\" : \"en\" */\n  digits?: DigitStyle\n}\n\nfunction resolveOptions(options: DateOptions = {}) {\n  const calendarType = options.calendarType ?? \"shamsi\"\n  const locale = options.locale ?? defaultLocale(calendarType)\n  const digits = options.digits ?? defaultLocale(calendarType)\n  return { calendarType, locale, digits }\n}\n\n/**\n * Formats a date using date-fns tokens (`yyyy/MM/dd`, `EEEE d MMMM`, ...),\n * switching between the Jalali and Gregorian calendars via `calendarType`.\n */\nexport function formatDate(\n  date: Date | number,\n  pattern: string,\n  options: DateOptions = {}\n): string {\n  const { calendarType, locale, digits } = resolveOptions(options)\n  const lib = calendarLib(calendarType)\n  const result = lib.format(date, pattern, {\n    locale: calendarLocale(calendarType, locale),\n  })\n  return digits === \"fa\" ? toPersianDigits(result) : result\n}\n\n/**\n * Parses a date string against a date-fns pattern. Normalizes Persian/Arabic-Indic\n * digits before parsing. Returns `null` instead of an invalid Date on failure.\n */\nexport function parseDate(\n  value: string,\n  pattern: string,\n  referenceDate: Date | number = new Date(),\n  options: DateOptions = {}\n): Date | null {\n  const { calendarType } = resolveOptions(options)\n  const lib = calendarLib(calendarType)\n  const normalized = normalizePersianDigits(value)\n  const parsed = lib.parse(normalized, pattern, referenceDate)\n  return lib.isValid(parsed) ? parsed : null\n}\n\n/** True if `value` is a valid, non-NaN Date. */\nexport function isValidDate(value: unknown): value is Date {\n  return value instanceof Date && gregorian.isValid(value)\n}\n\n/** Reads a date's calendar fields (1-indexed month) in the given calendar. */\nexport function toParts(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): DateParts {\n  const lib = calendarLib(calendarType)\n  return {\n    year: lib.getYear(date),\n    month: lib.getMonth(date) + 1,\n    day: lib.getDate(date),\n  }\n}\n\n/** Builds a Date (at local midnight) from calendar fields (1-indexed month). */\nexport function fromParts(\n  parts: DateParts,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  const lib = calendarLib(calendarType)\n  // year/month/day are all overwritten below and startOfDay strips the\n  // time-of-day, so the starting point is arbitrary -- use a fixed date\n  // instead of `new Date()` so this function doesn't depend on the current\n  // time (and stays safe to call during SSR/prerender).\n  let date = lib.setYear(new Date(0), parts.year)\n  date = lib.setMonth(date, parts.month - 1)\n  date = lib.setDate(date, parts.day)\n  return lib.startOfDay(date)\n}\n\n/** Shorthand for `toParts(date, \"shamsi\")`. */\nexport function toShamsi(date: Date): DateParts {\n  return toParts(date, \"shamsi\")\n}\n\n/** Shorthand for `toParts(date, \"miladi\")`. */\nexport function toMiladi(date: Date): DateParts {\n  return toParts(date, \"miladi\")\n}\n\n/** Shorthand for `fromParts(parts, \"shamsi\")`. */\nexport function fromShamsi(parts: DateParts): Date {\n  return fromParts(parts, \"shamsi\")\n}\n\n/** Shorthand for `fromParts(parts, \"miladi\")`. */\nexport function fromMiladi(parts: DateParts): Date {\n  return fromParts(parts, \"miladi\")\n}\n\nexport function today(): Date {\n  return gregorian.startOfDay(new Date())\n}\n\nexport function now(): Date {\n  return new Date()\n}\n\nexport function addDays(date: Date, amount: number): Date {\n  return gregorian.addDays(date, amount)\n}\n\nexport function addWeeks(date: Date, amount: number): Date {\n  return gregorian.addWeeks(date, amount)\n}\n\nexport function addMonths(\n  date: Date,\n  amount: number,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).addMonths(date, amount)\n}\n\nexport function addYears(\n  date: Date,\n  amount: number,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).addYears(date, amount)\n}\n\nexport function startOfDay(date: Date): Date {\n  return gregorian.startOfDay(date)\n}\n\nexport function endOfDay(date: Date): Date {\n  return gregorian.endOfDay(date)\n}\n\n/** Week starts on Saturday for \"shamsi\", Sunday for \"miladi\". */\nexport function startOfWeek(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).startOfWeek(date)\n}\n\nexport function endOfWeek(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).endOfWeek(date)\n}\n\nexport function startOfMonth(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).startOfMonth(date)\n}\n\nexport function endOfMonth(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).endOfMonth(date)\n}\n\nexport function startOfYear(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).startOfYear(date)\n}\n\nexport function endOfYear(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): Date {\n  return calendarLib(calendarType).endOfYear(date)\n}\n\nexport function daysInMonth(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): number {\n  return calendarLib(calendarType).getDaysInMonth(date)\n}\n\nexport function isLeapYear(\n  date: Date,\n  calendarType: CalendarType = \"shamsi\"\n): boolean {\n  return calendarLib(calendarType).isLeapYear(date)\n}\n\n/** Real elapsed days between two dates, independent of calendar system. */\nexport function daysBetween(from: Date, to: Date): number {\n  return gregorian.differenceInCalendarDays(to, from)\n}\n\n/** Calendar-month distance between two dates; month length depends on calendarType. */\nexport function monthsBetween(\n  from: Date,\n  to: Date,\n  calendarType: CalendarType = \"shamsi\"\n): number {\n  return calendarLib(calendarType).differenceInCalendarMonths(to, from)\n}\n\nexport function isSameDay(a: Date, b: Date): boolean {\n  return gregorian.isSameDay(a, b)\n}\n\nexport function isBefore(a: Date, b: Date): boolean {\n  return gregorian.isBefore(a, b)\n}\n\nexport function isAfter(a: Date, b: Date): boolean {\n  return gregorian.isAfter(a, b)\n}\n\nexport function isToday(date: Date): boolean {\n  return gregorian.isToday(date)\n}\n\nexport function isPast(date: Date): boolean {\n  return gregorian.isPast(date)\n}\n\nexport function isFuture(date: Date): boolean {\n  return gregorian.isFuture(date)\n}\n\nexport function minDate(...dates: Date[]): Date {\n  return gregorian.min(dates)\n}\n\nexport function maxDate(...dates: Date[]): Date {\n  return gregorian.max(dates)\n}\n\n/** Clamps `date` between optional `min`/`max` bounds (inclusive). */\nexport function clampDate(\n  date: Date,\n  { min, max }: { min?: Date; max?: Date }\n): Date {\n  if (min && isBefore(date, min)) return min\n  if (max && isAfter(date, max)) return max\n  return date\n}\n\n/** Inclusive day list between `from` and `to`. */\nexport function eachDayOfRange(from: Date, to: Date): Date[] {\n  return gregorian.eachDayOfInterval({ start: from, end: to })\n}\n\n/** True when `date` falls within `range` (inclusive, missing bounds are open-ended). */\nexport function isWithinRange(date: Date, range: DateRange): boolean {\n  if (range.from && isBefore(date, startOfDay(range.from))) return false\n  if (range.to && isAfter(date, endOfDay(range.to))) return false\n  return true\n}\n\n/** Inclusive day count spanned by a range, or 0 if incomplete. */\nexport function rangeLengthInDays(range: DateRange): number {\n  if (!range.from || !range.to) return 0\n  return daysBetween(range.from, range.to) + 1\n}\n\n/**\n * Click-to-toggle logic for a two-endpoint range picker: clicking an\n * already-selected endpoint clears just that endpoint, leaving the other one\n * in place. The next click fills whichever slot is empty, keeping `from`\n * chronologically before `to` -- if the new day would invert that order, it\n * swaps into the other slot instead (e.g. clicking an earlier day while only\n * `to` is empty makes that day the new `from` and shifts the old `from` into\n * `to`, rather than storing an inverted `from > to` pair). An inverted pair\n * would still *look* like a normal range in the Calendar's own rendering\n * (start/end dots at whichever cells `from`/`to` point to), which made\n * clicking the visually-first endpoint clear the visually-last one instead --\n * always keeping `from <= to` avoids that mismatch, and matches the\n * \"inverted\" check `validateRange` already assumes elsewhere.\n * Clicking a day that isn't either endpoint while both are already set starts\n * a fresh range instead of extending the old one.\n *\n * A cleared endpoint stays cleared: consumers that render a lone `to` should\n * expose it through a custom `range_end` modifier because react-day-picker's\n * built-in range renderer only understands a lone `from`.\n */\nexport function toggleRangeSelection(range: DateRange, day: Date): DateRange {\n  const { from, to } = range\n\n  if (from && isSameDay(day, from)) return { from: undefined, to }\n  if (to && isSameDay(day, to)) return { from, to: undefined }\n  if (!from && !to) return { from: day, to: undefined }\n  if (!from) return { from: day, to }\n  if (!to)\n    return isBefore(day, from) ? { from: day, to: from } : { from, to: day }\n  return { from: day, to: undefined }\n}\n\nexport interface RangeValidationOptions {\n  minDays?: number\n  maxDays?: number\n  disablePast?: boolean\n  disabledDates?: Date[]\n}\n\n/**\n * Validates a `from`/`to` range against reservation-style constraints:\n * ordering, minimum/maximum stay length, past dates, and specific blocked dates.\n * Returns `null` when valid, or a machine-readable reason otherwise.\n */\nexport function validateRange(\n  range: DateRange,\n  options: RangeValidationOptions = {}\n):\n  | \"incomplete\"\n  | \"inverted\"\n  | \"too-short\"\n  | \"too-long\"\n  | \"in-past\"\n  | \"disabled-date\"\n  | null {\n  const { from, to } = range\n  if (!from || !to) return \"incomplete\"\n  if (isAfter(from, to)) return \"inverted\"\n\n  if (options.disablePast && isBefore(startOfDay(from), today())) {\n    return \"in-past\"\n  }\n\n  const length = rangeLengthInDays(range)\n  if (options.minDays && length < options.minDays) return \"too-short\"\n  if (options.maxDays && length > options.maxDays) return \"too-long\"\n\n  if (\n    options.disabledDates?.some((disabled) => isWithinRange(disabled, range))\n  ) {\n    return \"disabled-date\"\n  }\n\n  return null\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:lib"
}