{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "title": "Calendar",
  "description": "A date-grid calendar built on react-day-picker, switchable between the Jalali (Shamsi) and Gregorian (Miladi) calendars via a calendarType prop, with an optional showHolidays prop for Iranian holiday highlighting.",
  "dependencies": [
    "react-day-picker",
    "@daypicker/persian",
    "date-fns"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.persian-labs.ir/r/button.json",
    "https://ui.persian-labs.ir/r/select.json",
    "https://ui.persian-labs.ir/r/badge.json",
    "https://ui.persian-labs.ir/r/tooltip.json",
    "https://ui.persian-labs.ir/r/persian-date.json",
    "https://ui.persian-labs.ir/r/persian-holidays.json"
  ],
  "files": [
    {
      "path": "registry/base/ui/calendar.tsx",
      "content": "\"use client\"\n\nimport { DayPicker as PersianDayPicker, faIR } from \"@daypicker/persian\"\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n} from \"lucide-react\"\nimport * as React from \"react\"\nimport {\n  DayPicker as GregorianDayPicker,\n  getDefaultClassNames,\n} from \"react-day-picker\"\nimport { enUS } from \"react-day-picker/locale\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n  endOfMonth,\n  isSameDay,\n  startOfDay,\n  startOfMonth,\n  type CalendarType,\n} from \"@/lib/persian-date\"\nimport {\n  getHolidaysInRange,\n  type ResolvedHoliday,\n} from \"@/lib/persian-holidays\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * \"shamsi\" renders the Jalali/Solar Hijri calendar via `@daypicker/persian`\n * (faIR locale, RTL, Persian numerals by default). \"miladi\" renders the\n * plain Gregorian calendar via `react-day-picker` (enUS locale, LTR, Latin\n * numerals).\n */\nexport type { CalendarType } from \"@/lib/persian-date\"\n\n// A fixed, non-time-dependent placeholder month used only for the very first\n// render (server + initial client) when the caller controls neither `month`\n// nor `defaultMonth`. react-day-picker falls back to `new Date()` internally\n// whenever both are absent, and reading the current date during render trips\n// Next 16 Cache Components' \"blocking prerender\" guard. The real current\n// month is swapped in client-side via an effect immediately after mount.\nconst DEFAULT_MONTH_FALLBACK = new Date(2024, 0, 1)\n\ntype GregorianDayPickerProps = React.ComponentProps<typeof GregorianDayPicker>\ntype PersianDayPickerProps = React.ComponentProps<typeof PersianDayPicker>\n\nexport type CalendarProps = GregorianDayPickerProps & {\n  /** @default \"shamsi\" */\n  calendarType?: CalendarType\n  buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"]\n  /**\n   * Highlights Iranian holidays (red text) for the visible month(s) and\n   * shows a hover tooltip with the holiday's title and whether it's an\n   * official day off or a commemorative occasion. Uses `persian-holidays`\n   * internally. @default false\n   */\n  showHolidays?: boolean\n}\n\nfunction Calendar({\n  calendarType = \"shamsi\",\n  className,\n  classNames,\n  showOutsideDays,\n  fixedWeeks = true,\n  captionLayout = \"label\",\n  buttonVariant = \"ghost\",\n  formatters,\n  components,\n  locale,\n  dir,\n  numerals,\n  numberOfMonths,\n  showHolidays = false,\n  month,\n  onMonthChange,\n  modifiers,\n  modifiersClassNames,\n  ...props\n}: CalendarProps) {\n  const defaultClassNames = getDefaultClassNames()\n\n  // Tracks the visible month locally whenever the caller isn't already\n  // controlling `month` (or hasn't supplied a static `defaultMonth`) so we\n  // can snap the calendar to \"today\" once mounted, and so `showHolidays` has\n  // a month to compute against. Deferred to an effect (not read at render\n  // time) since \"today\" differs between server and client, and since\n  // react-day-picker itself would otherwise read `new Date()` during render\n  // -- see DEFAULT_MONTH_FALLBACK above.\n  const [fallbackMonth, setFallbackMonth] = React.useState<Date | null>(null)\n  React.useEffect(() => {\n    if (month || props.defaultMonth) return\n    const init = () => setFallbackMonth((current) => current ?? new Date())\n    init()\n  }, [month, props.defaultMonth])\n\n  const visibleMonth = month ?? fallbackMonth ?? undefined\n  const handleMonthChange = (nextMonth: Date) => {\n    onMonthChange?.(nextMonth)\n    if (!month) setFallbackMonth(nextMonth)\n  }\n\n  const holidays = React.useMemo(() => {\n    if (!showHolidays || !visibleMonth) return []\n    return getHolidaysInRange(\n      startOfMonth(visibleMonth),\n      endOfMonth(visibleMonth),\n      { includeUnofficial: true }\n    )\n  }, [showHolidays, visibleMonth])\n\n  // A day can carry both an official and an unofficial holiday at once, so\n  // group by day first and let official win -- rather than assigning both\n  // modifiers to the same date and leaving the result to CSS class order.\n  const { officialHolidayDates, unofficialHolidayDates } = React.useMemo(() => {\n    const officialByDay = new Map<number, boolean>()\n    for (const holiday of holidays) {\n      const key = startOfDay(holiday.date).getTime()\n      officialByDay.set(\n        key,\n        (officialByDay.get(key) ?? false) || holiday.official\n      )\n    }\n    const official: Date[] = []\n    const unofficial: Date[] = []\n    for (const [time, isOfficial] of officialByDay) {\n      ;(isOfficial ? official : unofficial).push(new Date(time))\n    }\n    return {\n      officialHolidayDates: official,\n      unofficialHolidayDates: unofficial,\n    }\n  }, [holidays])\n\n  const resolvedModifiers = showHolidays\n    ? {\n        ...modifiers,\n        holidayOfficial: officialHolidayDates,\n        holidayUnofficial: unofficialHolidayDates,\n      }\n    : modifiers\n  const resolvedModifiersClassNames = showHolidays\n    ? {\n        ...modifiersClassNames,\n        holidayOfficial: \"text-destructive font-semibold\",\n        holidayUnofficial: \"text-info font-semibold\",\n      }\n    : modifiersClassNames\n\n  const resolvedLocale = locale ?? (calendarType === \"shamsi\" ? faIR : enUS)\n  const resolvedDir = dir ?? (calendarType === \"shamsi\" ? \"rtl\" : \"ltr\")\n  const resolvedNumerals =\n    numerals ?? (calendarType === \"shamsi\" ? \"arabext\" : \"latn\")\n  // With multiple months shown side by side, each month's leading/trailing\n  // \"outside days\" bleed into the adjacent month's grid, so the shared\n  // boundary date (e.g. the 31st) visually appears twice. Defaulting outside\n  // days off for multi-month layouts avoids that -- still overridable.\n  const resolvedShowOutsideDays = showOutsideDays ?? true\n\n  const dayPickerProps = {\n    showOutsideDays: resolvedShowOutsideDays,\n    fixedWeeks,\n    numberOfMonths,\n    ...(visibleMonth\n      ? { month: visibleMonth }\n      : { defaultMonth: DEFAULT_MONTH_FALLBACK }),\n    onMonthChange: handleMonthChange,\n    modifiers: resolvedModifiers,\n    modifiersClassNames: resolvedModifiersClassNames,\n    className: cn(\n      \"group/calendar bg-background p-3 [--cell-size:--spacing(10)] sm:[--cell-size:--spacing(9)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n      // A Gregorian calendar retains its LTR grid by default, but it can be\n      // placed within an RTL interface. Mirror only its arrow artwork in that\n      // inherited RTL context; an explicit dir=\"rtl\" is handled directly by\n      // the Chevron component below.\n      className\n    ),\n    captionLayout,\n    locale: resolvedLocale,\n    dir: resolvedDir,\n    numerals: resolvedNumerals,\n    formatters: {\n      formatMonthDropdown: (date: Date) =>\n        date.toLocaleString(\"default\", { month: \"short\" }),\n      ...formatters,\n    },\n    classNames: {\n      root: cn(\"m-0 w-fit\", defaultClassNames.root),\n      months: cn(\n        \"relative flex flex-col gap-4 md:flex-row\",\n        defaultClassNames.months\n      ),\n      month: cn(\"flex w-full flex-col gap-4\", defaultClassNames.month),\n      nav: cn(\n        // Absolutely positioned and full-width so the prev/next buttons can\n        // sit flush at either edge, but that leaves its empty middle\n        // stretching across the whole caption row -- including right over\n        // captionLayout=\"dropdown\"'s month/year Select triggers, silently\n        // eating their clicks before they ever reach the trigger underneath.\n        // pointer-events-none here (re-enabled per-button below) lets clicks\n        // pass through the empty middle instead of dead-ending on <nav>.\n        \"pointer-events-none absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1\",\n        defaultClassNames.nav\n      ),\n      button_previous: cn(\n        buttonVariants({ variant: buttonVariant }),\n        \"pointer-events-auto size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n        defaultClassNames.button_previous\n      ),\n      button_next: cn(\n        buttonVariants({ variant: buttonVariant }),\n        \"pointer-events-auto size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n        defaultClassNames.button_next\n      ),\n      month_caption: cn(\n        \"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)\",\n        defaultClassNames.month_caption\n      ),\n      dropdowns: cn(\n        \"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium\",\n        defaultClassNames.dropdowns\n      ),\n      caption_label: cn(\n        \"font-medium select-none\",\n        captionLayout === \"label\"\n          ? \"text-sm\"\n          : \"flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground\",\n        defaultClassNames.caption_label\n      ),\n      month_grid: cn(\"w-full border-collapse\", defaultClassNames.month_grid),\n      weekdays: cn(\"flex\", defaultClassNames.weekdays),\n      weekday: cn(\n        \"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none\",\n        defaultClassNames.weekday\n      ),\n      week: cn(\"mt-2 flex w-full\", defaultClassNames.week),\n      week_number_header: cn(\n        \"w-(--cell-size) select-none\",\n        defaultClassNames.week_number_header\n      ),\n      week_number: cn(\n        \"text-[0.8rem] text-muted-foreground select-none\",\n        defaultClassNames.week_number\n      ),\n      day: cn(\n        \"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-s-md [&:last-child[data-selected=true]_button]:rounded-e-md\",\n        defaultClassNames.day\n      ),\n      range_start: cn(\"rounded-s-md bg-accent\", defaultClassNames.range_start),\n      range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n      range_end: cn(\"rounded-e-md bg-accent\", defaultClassNames.range_end),\n      today: cn(\n        \"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none data-[selected=true]:bg-transparent\",\n        defaultClassNames.today\n      ),\n      outside: cn(\n        \"text-muted-foreground aria-selected:text-muted-foreground\",\n        defaultClassNames.outside\n      ),\n      disabled: cn(\n        \"text-muted-foreground opacity-50\",\n        defaultClassNames.disabled\n      ),\n      hidden: cn(\"invisible\", defaultClassNames.hidden),\n      ...classNames,\n    },\n    components: {\n      Root: ({\n        className: rootClassName,\n        rootRef,\n        ...rootProps\n      }: {\n        className?: string\n        rootRef?: React.Ref<HTMLDivElement>\n      } & React.HTMLAttributes<HTMLDivElement>) => {\n        return (\n          <div\n            data-slot=\"calendar\"\n            data-calendar-type={calendarType}\n            ref={rootRef}\n            className={cn(rootClassName)}\n            {...rootProps}\n          />\n        )\n      },\n      Chevron: ({\n        className: chevronClassName,\n        orientation,\n        ...chevronProps\n      }: {\n        className?: string\n        orientation?: \"up\" | \"down\" | \"left\" | \"right\"\n      } & React.SVGProps<SVGSVGElement>) => {\n        if (orientation === \"left\") {\n          const Icon =\n            resolvedDir === \"rtl\" ? ChevronRightIcon : ChevronLeftIcon\n          return (\n            <Icon\n              className={cn(\"size-4.5 sm:size-4\", chevronClassName)}\n              {...chevronProps}\n            />\n          )\n        }\n\n        if (orientation === \"right\") {\n          const Icon =\n            resolvedDir === \"rtl\" ? ChevronLeftIcon : ChevronRightIcon\n          return (\n            <Icon\n              className={cn(\"size-4.5 sm:size-4\", chevronClassName)}\n              {...chevronProps}\n            />\n          )\n        }\n\n        return (\n          <ChevronDownIcon\n            className={cn(\"size-4\", chevronClassName)}\n            {...chevronProps}\n          />\n        )\n      },\n      DayButton: showHolidays\n        ? (dayButtonProps: CalendarDayButtonProps) => (\n            <HolidayDayButton {...dayButtonProps} holidays={holidays} />\n          )\n        : CalendarDayButton,\n      Dropdown: CalendarDropdown,\n      WeekNumber: ({\n        children,\n        ...weekNumberProps\n      }: {\n        children?: React.ReactNode\n      } & React.ThHTMLAttributes<HTMLTableCellElement>) => {\n        return (\n          <td {...weekNumberProps}>\n            <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n              {children}\n            </div>\n          </td>\n        )\n      },\n      ...components,\n    },\n    ...props,\n  }\n\n  const picker =\n    calendarType === \"shamsi\" ? (\n      <PersianDayPicker\n        {...(dayPickerProps as unknown as PersianDayPickerProps)}\n      />\n    ) : (\n      <GregorianDayPicker {...(dayPickerProps as GregorianDayPickerProps)} />\n    )\n\n  const content = (\n    <>\n      {calendarType === \"miladi\" && dir !== \"rtl\" && (\n        <style>{`[dir=\"rtl\"] [data-calendar-type=\"miladi\"] .rdp-button_previous svg,\n[dir=\"rtl\"] [data-calendar-type=\"miladi\"] .rdp-button_next svg { transform: rotate(180deg); }`}</style>\n      )}\n      {picker}\n    </>\n  )\n\n  return showHolidays ? (\n    <TooltipProvider delay={150}>{content}</TooltipProvider>\n  ) : (\n    content\n  )\n}\n\ninterface CalendarDayButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  day: { date: Date }\n  modifiers: Record<string, boolean>\n}\n\nfunction CalendarDayButton({\n  className,\n  day,\n  modifiers,\n  ...props\n}: CalendarDayButtonProps) {\n  const defaultClassNames = getDefaultClassNames()\n\n  const ref = React.useRef<HTMLButtonElement>(null)\n  React.useEffect(() => {\n    if (modifiers.focused) ref.current?.focus()\n  }, [modifiers.focused])\n\n  return (\n    <Button\n      ref={ref}\n      variant=\"ghost\"\n      size=\"icon\"\n      data-day={day.date.toLocaleDateString()}\n      data-selected-single={\n        modifiers.selected &&\n        !modifiers.range_start &&\n        !modifiers.range_end &&\n        !modifiers.range_middle\n      }\n      data-today={modifiers.today}\n      data-range-start={modifiers.range_start}\n      data-range-end={modifiers.range_end}\n      data-range-middle={modifiers.range_middle}\n      className={cn(\n        \"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 text-base leading-none font-normal select-none group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 disabled:text-muted-foreground/72 disabled:line-through disabled:opacity-100 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-s-none data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-e-none data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[today=true]:after:pointer-events-none data-[today=true]:after:absolute data-[today=true]:after:inset-x-0 data-[today=true]:after:bottom-1 data-[today=true]:after:z-1 data-[today=true]:after:mx-auto data-[today=true]:after:size-[3px] data-[today=true]:after:rounded-full data-[today=true]:after:bg-primary data-[today=true]:after:content-[''] data-[today=true]:disabled:after:bg-foreground/30 data-[today=true]:data-[range-end=true]:after:bg-primary-foreground data-[today=true]:data-[range-start=true]:after:bg-primary-foreground data-[today=true]:data-[selected-single=true]:after:bg-primary-foreground sm:text-sm [&>span]:text-xs [&>span]:opacity-70\",\n        // The selected/range states above set bg-primary + text-primary-foreground on\n        // the *base* class, but Button's own ghost variant also carries a plain\n        // hover:bg-muted hover:text-foreground with the same specificity -- whichever\n        // rule Tailwind happens to emit later in the stylesheet wins on hover,\n        // regardless of selected state. That's what produced the white/black flicker\n        // in dark mode (hovering a selected, white day could randomly flip it dark).\n        // Force hover to keep showing the selected color instead of falling back to\n        // ghost's generic hover. The `dark:hover:bg-muted/50` in Button's own ghost\n        // variant is a compound (dark+hover) variant that outranks a plain\n        // data-[x]:hover: override in Tailwind's generated stylesheet order in dark\n        // mode specifically -- so the dark-mode-selected day silently fell back to\n        // ghost's muted hover. Matching it with an equally-compound\n        // dark:data-[x]:hover: override wins regardless of source order.\n        \"data-[selected-single=true]:hover:bg-primary data-[selected-single=true]:hover:text-primary-foreground dark:data-[selected-single=true]:hover:bg-primary dark:data-[selected-single=true]:hover:text-primary-foreground\",\n        \"data-[range-start=true]:hover:bg-primary data-[range-start=true]:hover:text-primary-foreground dark:data-[range-start=true]:hover:bg-primary dark:data-[range-start=true]:hover:text-primary-foreground\",\n        \"data-[range-end=true]:hover:bg-primary data-[range-end=true]:hover:text-primary-foreground dark:data-[range-end=true]:hover:bg-primary dark:data-[range-end=true]:hover:text-primary-foreground\",\n        \"data-[range-middle=true]:hover:bg-accent data-[range-middle=true]:hover:text-accent-foreground dark:data-[range-middle=true]:hover:bg-accent dark:data-[range-middle=true]:hover:text-accent-foreground\",\n        defaultClassNames.day,\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ninterface HolidayDayButtonProps extends CalendarDayButtonProps {\n  holidays: ResolvedHoliday[]\n}\n\n/** `showHolidays`'s DayButton: wraps CalendarDayButton with a hover tooltip on holiday cells. */\nfunction HolidayDayButton({\n  day,\n  modifiers,\n  holidays,\n  ...props\n}: HolidayDayButtonProps) {\n  const dayHolidays = holidays.filter((holiday) =>\n    isSameDay(holiday.date, day.date)\n  )\n\n  if (dayHolidays.length === 0) {\n    return <CalendarDayButton day={day} modifiers={modifiers} {...props} />\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={\n          <CalendarDayButton day={day} modifiers={modifiers} {...props} />\n        }\n      />\n      <TooltipContent className=\"flex flex-col items-start gap-1.5 py-2\">\n        {dayHolidays.map((holiday) => (\n          <div key={holiday.title} className=\"flex items-start gap-1.5\">\n            <Badge\n              // TooltipContent is intentionally inverted (bg-foreground/\n              // text-background) relative to the page. Badge's \"outline\" and\n              // \"secondary\" variants are tuned for a normal bg-background/\n              // bg-popover surface (secondary is even a translucent black/\n              // white overlay, same pitfall as bg-muted on a floating chip),\n              // so they'd wash out or disappear here. \"destructive\" and\n              // \"info\" are solid theme colors independent of that inversion,\n              // so they stay readable on any surface.\n              variant={holiday.official ? \"destructive\" : \"info\"}\n              className=\"shrink-0 leading-5\"\n            >\n              {holiday.official ? \"تعطیل رسمی\" : \"مناسبت\"}\n            </Badge>\n            <span className=\"leading-5\">{holiday.title}</span>\n          </div>\n        ))}\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\ninterface CalendarDropdownOption {\n  value: number\n  label: string\n  disabled: boolean\n}\n\ntype CalendarDropdownProps = Omit<\n  React.SelectHTMLAttributes<HTMLSelectElement>,\n  \"children\" | \"value\" | \"onChange\"\n> & {\n  options?: CalendarDropdownOption[]\n  value?: number\n  onChange?: React.ChangeEventHandler<HTMLSelectElement>\n}\n\n/**\n * Replaces react-day-picker's default native `<select>` (used for the month/\n * year captionLayout=\"dropdown\" navigation) with this repo's own `Select`.\n * react-day-picker only reads `event.target.value` from `onChange`, so a\n * minimal synthetic event is enough to keep it fully in sync -- no need to\n * fork react-day-picker's internal month/year change handlers.\n */\nfunction CalendarDropdown({\n  options,\n  value,\n  onChange,\n  disabled,\n  className,\n  \"aria-label\": ariaLabel,\n}: CalendarDropdownProps) {\n  return (\n    <Select\n      value={value != null ? String(value) : undefined}\n      onValueChange={(next) => {\n        onChange?.({\n          target: { value: String(next) },\n        } as React.ChangeEvent<HTMLSelectElement>)\n      }}\n      disabled={disabled}\n      // SelectValue only knows how to resolve a label for the current value\n      // via this `items` map -- without it, it falls back to rendering the\n      // raw value (\"4\") instead of the option's label (\"تیر\").\n      items={options?.map((option) => ({\n        value: String(option.value),\n        label: option.label,\n      }))}\n    >\n      <SelectTrigger\n        aria-label={ariaLabel}\n        className={cn(\n          \"h-8 w-fit min-w-0 gap-1 border-none bg-transparent px-2 text-sm font-medium shadow-none hover:bg-muted data-[popup-open]:bg-muted\",\n          className\n        )}\n      >\n        <SelectValue className=\"truncate\" />\n      </SelectTrigger>\n      <SelectContent>\n        {options?.map((option) => (\n          <SelectItem\n            key={option.value}\n            value={String(option.value)}\n            disabled={option.disabled}\n          >\n            {option.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  )\n}\n\nexport { Calendar, CalendarDayButton, CalendarDropdown, HolidayDayButton }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}