{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar",
  "type": "registry:ui",
  "title": "Calendar",
  "description": "تقویم شمسی با الگوی grid استاندارد WAI-ARIA و پیمایش کامل با کیبورد",
  "dependencies": [
    "@kashi-ui/persian@^1.7.0"
  ],
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/Calendar.tsx",
      "type": "registry:ui",
      "target": "components/kashi/Calendar.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport {\n  JALALI_MONTHS, WEEKDAYS, WEEKDAYS_SHORT,\n  formatJalali, getIranHoliday, jalaliMonthLength, jalaliWeekday, toFaDigits, toGregorian, toJalali,\n} from \"@kashi-ui/persian\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport const sameDay = (a?: Date | null, b?: Date | null) =>\n  !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst dayKey = (d: Date) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;\n\n/** چند ماه شمسی جلو یا عقب؛ روز در ماه‌های کوتاه‌تر به آخر ماه می‌چسبد */\nfunction addJalaliMonths(d: Date, n: number): Date {\n  const { jy, jm, jd } = toJalali(d);\n  const total = jy * 12 + (jm - 1) + n;\n  const ny = Math.floor(total / 12);\n  const nm = total - ny * 12 + 1;\n  return toGregorian(ny, nm, Math.min(jd, jalaliMonthLength(ny, nm)));\n}\n\nfunction clamp(d: Date, min?: Date, max?: Date): Date {\n  if (min && d < startOfDay(min)) return startOfDay(min);\n  if (max && d > startOfDay(max)) return startOfDay(max);\n  return d;\n}\n\nexport interface CalendarLabels {\n  /** @default \"ماه قبل\" */\n  prevMonth?: string;\n  /** @default \"ماه بعد\" */\n  nextMonth?: string;\n  /** @default \"ماه\" */\n  month?: string;\n  /** @default \"سال\" */\n  year?: string;\n}\n\nexport interface CalendarProps {\n  /** تاریخ انتخاب‌شده (کنترل‌شده) */\n  value?: Date | null;\n  defaultValue?: Date | null;\n  onChange?: (date: Date) => void;\n  /** کمترین تاریخ قابل‌انتخاب */\n  min?: Date;\n  /** بیشترین تاریخ قابل‌انتخاب */\n  max?: Date;\n  /** غیرفعال کردن روزهای خاص، مثلاً جمعه‌ها */\n  isDateDisabled?: (date: Date) => boolean;\n  /**\n   * نمایش تعطیلات: true برای جمعه‌ها و تعطیلات ثابت ایران،\n   * یا تابعی که نام تعطیلی آن روز (یا null) را برمی‌گرداند، مثلاً با getIranHoliday و تعطیلات قمری همان سال\n   */\n  holidays?: boolean | ((date: Date) => string | null | false | undefined);\n  /** @default \"fa\" */\n  digits?: \"fa\" | \"latin\";\n  /** فوکوس روی روز انتخاب‌شده (یا امروز) هنگام نمایش */\n  autoFocus?: boolean;\n  /** محدودهٔ سال‌های فهرست سال؛ پیش‌فرض از min/max یا ۱۰۰ سال قبل تا ۲۰ سال بعد */\n  yearRange?: [number, number];\n  labels?: CalendarLabels;\n  /** محتوای پایین تقویم، مثل دکمهٔ «امروز» */\n  footer?: ReactNode;\n  /** حالت بازه: روزهای بین start و end برجسته می‌شوند؛ با start بدون end، hover پیش‌نمایش می‌دهد */\n  range?: { start: Date | null; end: Date | null };\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/** تقویم شمسی با الگوی grid استاندارد WAI-ARIA و پیمایش کامل با کیبورد */\nexport function Calendar({\n  value, defaultValue = null, onChange, min, max, isDateDisabled, holidays, digits, autoFocus = false,\n  yearRange, labels = {}, footer, range, className, \"aria-label\": ariaLabel,\n}: CalendarProps) {\n  const { messages: M, digits: D, locale, months: MONTHS, weekdays: WD, weekdaysShort: WDS, friday: FRI } = useKashi();\n  digits ??= D;\n  ariaLabel ??= M.calendar.label;\n  const id = useId();\n  const [inner, setInner] = useState<Date | null>(defaultValue);\n  const selected = value !== undefined ? value : inner;\n  const [today] = useState(() => startOfDay(new Date()));\n  const [focused, setFocused] = useState<Date>(() => clamp(startOfDay(range?.start ?? selected ?? today), min, max));\n  const [hover, setHover] = useState<Date | null>(null);\n  const moveFocus = useRef(autoFocus);\n  const grid = useRef<HTMLTableElement>(null);\n  const n = (v: number) => (digits === \"fa\" ? toFaDigits(v) : String(v));\n\n  // اگر مقدار از بیرون عوض شد، ماه نمایش‌داده‌شده هم دنبالش برود\n  useEffect(() => {\n    if (selected) setFocused(startOfDay(selected));\n  }, [selected?.getTime()]);\n\n  useEffect(() => {\n    if (!moveFocus.current) return;\n    moveFocus.current = false;\n    grid.current?.querySelector<HTMLButtonElement>(`[data-day=\"${dayKey(focused)}\"]`)?.focus();\n  });\n\n  const { jy, jm } = toJalali(focused);\n  const monthStart = toGregorian(jy, jm, 1);\n  const offset = jalaliWeekday(monthStart);\n  const length = jalaliMonthLength(jy, jm);\n  const cells: (Date | null)[] = [\n    ...Array<null>(offset).fill(null),\n    ...Array.from({ length }, (_, i) => addDays(monthStart, i)),\n  ];\n  while (cells.length % 7) cells.push(null);\n  const weeks = Array.from({ length: cells.length / 7 }, (_, i) => cells.slice(i * 7, i * 7 + 7));\n\n  const disabled = (d: Date) =>\n    (min !== undefined && d < startOfDay(min)) || (max !== undefined && d > startOfDay(max)) || !!isDateDisabled?.(d);\n\n  const [fromYear, toYear] = yearRange ?? [\n    min ? toJalali(min).jy : toJalali(today).jy - 100,\n    max ? toJalali(max).jy : toJalali(today).jy + 20,\n  ];\n  const years = Array.from({ length: Math.max(0, toYear - fromYear + 1) }, (_, i) => fromYear + i);\n  if (!years.includes(jy)) years.push(jy);\n\n  const go = (d: Date, focus = true) => {\n    moveFocus.current = focus;\n    setFocused(clamp(d, min, max));\n  };\n\n  const select = (d: Date) => {\n    if (disabled(d)) return;\n    if (value === undefined) setInner(d);\n    onChange?.(d);\n  };\n\n  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n    // در راست‌به‌چپ، فلش چپ یعنی روز بعد\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const wd = jalaliWeekday(focused);\n    const next: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(focused, rtl ? 1 : -1),\n      ArrowRight: () => addDays(focused, rtl ? -1 : 1),\n      ArrowUp: () => addDays(focused, -7),\n      ArrowDown: () => addDays(focused, 7),\n      Home: () => addDays(focused, -wd),\n      End: () => addDays(focused, 6 - wd),\n      PageUp: () => addJalaliMonths(focused, e.shiftKey ? -12 : -1),\n      PageDown: () => addJalaliMonths(focused, e.shiftKey ? 12 : 1),\n    };\n    if (!next[e.key]) return;\n    e.preventDefault();\n    go(next[e.key]());\n  };\n\n  const { prevMonth = M.calendar.prevMonth, nextMonth = M.calendar.nextMonth, month: monthLabel = M.calendar.month, year: yearLabel = M.calendar.year } = labels;\n  const prevDisabled = min !== undefined && toGregorian(jy, jm, 1) <= startOfDay(min);\n  const nextDisabled = max !== undefined && toGregorian(jy, jm, length) >= startOfDay(max);\n\n  return (\n    <div className={cx(\"k-calendar\", className)} role=\"group\" aria-label={ariaLabel}>\n      <div className=\"k-calendar__header\">\n        <button\n          type=\"button\"\n          className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm\"\n          aria-label={prevMonth}\n          disabled={prevDisabled}\n          onClick={() => go(addJalaliMonths(focused, -1), false)}\n        >\n          <svg data-k-mirror=\"\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.7} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m12 5-5 5 5 5\" /></svg>\n        </button>\n        <div className=\"k-calendar__selects\">\n          <select\n            className=\"k-calendar__select\"\n            aria-label={monthLabel}\n            value={jm}\n            onChange={(e) => go(toGregorian(jy, Number(e.target.value), Math.min(toJalali(focused).jd, jalaliMonthLength(jy, Number(e.target.value)))), false)}\n          >\n            {MONTHS.map((name, i) => <option key={name} value={i + 1}>{name}</option>)}\n          </select>\n          <select\n            className=\"k-calendar__select\"\n            aria-label={yearLabel}\n            value={jy}\n            onChange={(e) => go(toGregorian(Number(e.target.value), jm, Math.min(toJalali(focused).jd, jalaliMonthLength(Number(e.target.value), jm))), false)}\n          >\n            {years.sort((a, b) => a - b).map((y) => <option key={y} value={y}>{n(y)}</option>)}\n          </select>\n        </div>\n        <button\n          type=\"button\"\n          className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm\"\n          aria-label={nextMonth}\n          disabled={nextDisabled}\n          onClick={() => go(addJalaliMonths(focused, 1), false)}\n        >\n          <svg data-k-mirror=\"\" viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.7} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m8 5 5 5-5 5\" /></svg>\n        </button>\n      </div>\n\n      {/* عنوان ماه برای صفحه‌خوان؛ تغییر ماه را اعلام می‌کند */}\n      <h2 className=\"k-sr-only\" id={`${id}-title`} aria-live=\"polite\">{`${MONTHS[jm - 1]} ${n(jy)}`}</h2>\n\n      <table className=\"k-calendar__grid\" role=\"grid\" aria-labelledby={`${id}-title`} ref={grid}>\n        <thead>\n          <tr>\n            {WDS.map((s, i) => (\n              <th key={s} scope=\"col\" abbr={WD[i]}>{s}</th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {weeks.map((week, wi) => (\n            <tr key={wi}>\n              {week.map((d, di) => {\n                if (!d) return <td key={di} role=\"gridcell\" />;\n                const isDisabled = disabled(d);\n                // حالت بازه\n                const rs = range?.start ? startOfDay(range.start) : null;\n                const re = range?.end ? startOfDay(range.end) : null;\n                const previewEnd = rs && !re && hover ? hover : null;\n                const [lo, hi] = rs && (re || previewEnd) ? [rs, (re ?? previewEnd)!].sort((a, b) => +a - +b) : [null, null];\n                const inRange = !!lo && !!hi && d >= lo && d <= hi;\n                const isSelected = range ? inRange || sameDay(d, rs) : sameDay(d, selected);\n                const holiday = holidays === true ? getIranHoliday(d, { locale }) : holidays ? holidays(d) || null : null;\n                return (\n                  <td\n                    key={di}\n                    role=\"gridcell\"\n                    aria-selected={isSelected}\n                    data-in-range={range && inRange && re ? \"\" : undefined}\n                    data-preview={range && inRange && !re ? \"\" : undefined}\n                    data-range-start={range && (lo ? sameDay(d, lo) : sameDay(d, rs)) ? \"\" : undefined}\n                    data-range-end={range && (hi ? sameDay(d, hi) : sameDay(d, rs)) ? \"\" : undefined}\n                  >\n                    <button\n                      type=\"button\"\n                      className=\"k-calendar__day\"\n                      data-day={dayKey(d)}\n                      tabIndex={sameDay(d, focused) ? 0 : -1}\n                      aria-label={formatJalali(d, { style: \"full\", digits, locale }) + (holiday ? `${M.common.list}${M.calendar.holiday}${holiday === FRI ? \"\" : `: ${holiday}`}` : \"\")}\n                      title={holiday && holiday !== FRI ? holiday : undefined}\n                      data-holiday={holiday ? \"\" : undefined}\n                      aria-current={sameDay(d, today) ? \"date\" : undefined}\n                      aria-disabled={isDisabled || undefined}\n                      onClick={() => {\n                        setFocused(d);\n                        select(d);\n                      }}\n                      onKeyDown={onKeyDown}\n                      onPointerEnter={range ? () => setHover(d) : undefined}\n                      onFocus={range ? () => setHover(d) : undefined}\n                    >\n                      {n(toJalali(d).jd)}\n                    </button>\n                  </td>\n                );\n              })}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n      {footer && <div className=\"k-calendar__footer\">{footer}</div>}\n    </div>\n  );\n}\n"
    }
  ]
}