{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "iran-map",
  "type": "registry:ui",
  "title": "IranMap",
  "description": "نقشهٔ ۳۱ استان ایران؛ نقشهٔ رنگی برای داده و/یا انتخاب یک یا چند استان، با صفحه‌کلید و جدول هم‌ارز",
  "dependencies": [
    "@kashi-ui/persian@^1.7.0"
  ],
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/chart.json",
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/iran-map-data.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/IranMap.tsx",
      "type": "registry:ui",
      "target": "components/kashi/IranMap.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState, type CSSProperties, type KeyboardEvent } from \"react\";\nimport { PROVINCES, formatNumber } from \"@kashi-ui/persian\";\nimport { cx } from \"./cx\";\nimport { ChartTip } from \"./Chart\";\nimport { IRAN_PROVINCES, IRAN_VIEWBOX } from \"./iranMapData\";\nimport { useKashi } from \"./KashiProvider\";\n\ntype Selection = string | string[] | null;\n\ninterface IranMapBase {\n  /** داده برای نقشهٔ رنگی: نام استان ← عدد؛ استان‌های بدون داده خاکستری می‌مانند */\n  data?: Record<string, number>;\n  /** نام دسترس‌پذیر @default «نقشهٔ استان‌های ایران» */\n  label?: string;\n  /** واحد در راهنما و جدول، مثل «سفارش» */\n  unit?: string;\n  /** قالب عدد @default ارقام فارسی با جداکنندهٔ هزارگان */\n  format?: (n: number) => string;\n  /** تعداد پله‌های رنگ @default 5 */\n  steps?: number;\n  /**\n   * تقسیم داده بین پله‌ها: \"quantile\" هر پله تقریباً تعداد برابری استان می‌گیرد (وقتی یک استان مثل تهران خیلی بزرگ‌تر است،\n   * بقیه هم از هم جدا دیده می‌شوند)؛ \"linear\" بازهٔ کمینه تا بیشینه را مساوی تقسیم می‌کند @default \"quantile\"\n   */\n  scale?: \"quantile\" | \"linear\";\n  /** راهنمای رنگ زیر نقشه @default true وقتی data دارد */\n  legend?: boolean;\n  /** خط اضافه در راهنمای هر استان، مثل «۱۲٪ رشد» */\n  tooltip?: (province: string, value: number | undefined) => string;\n  className?: string;\n}\n\n/** انتخاب یک استان؛ با دادن value، defaultValue یا onChange نقشه قابل انتخاب می‌شود */\ninterface IranMapSingle extends IranMapBase {\n  multiple?: false;\n  value?: string | null;\n  defaultValue?: string | null;\n  onChange?: (value: string | null) => void;\n}\n\n/** انتخاب چند استان */\ninterface IranMapMultiple extends IranMapBase {\n  multiple: true;\n  value?: string[];\n  defaultValue?: string[];\n  onChange?: (value: string[]) => void;\n}\n\nexport type IranMapProps = IranMapSingle | IranMapMultiple;\n\nconst NAMES = PROVINCES.map((p) => p.name);\nconst CAPITAL = Object.fromEntries(PROVINCES.map((p) => [p.name, p.capital]));\nconst [, , VW, VH] = IRAN_VIEWBOX.split(\" \").map(Number);\n\n/** نقشهٔ ۳۱ استان ایران؛ نقشهٔ رنگی برای داده و/یا انتخاب یک یا چند استان، با صفحه‌کلید و جدول هم‌ارز */\nexport function IranMap(props: IranMapProps) {\n  const { messages: M, digits: D } = useKashi();\n  const {\n    data, value, defaultValue, multiple = false, label = M.iranMap.label, unit,\n    format = (n: number) => formatNumber(n, { digits: D }), steps = 5, scale = \"quantile\", legend, tooltip, className,\n  } = props;\n  const onChange = props.onChange as ((v: Selection) => void) | undefined;\n  const selectable = value !== undefined || defaultValue !== undefined || onChange !== undefined;\n  const [inner, setInner] = useState<Selection>(defaultValue ?? (multiple ? [] : null));\n  const selected = value !== undefined ? value : inner;\n  const isSelected = (n: string) => (Array.isArray(selected) ? selected.includes(n) : selected === n);\n  const [active, setActive] = useState(0);\n  const [inside, setInside] = useState(false);\n  // ورود با Tab روی استان انتخاب‌شده؛ داخل نقشه همان استانی که جهت‌نماها رویش رفته‌اند\n  const firstSelected = NAMES.findIndex((n) => isSelected(n));\n  const tabStop = inside || firstSelected < 0 ? active : firstSelected;\n  const [tip, setTip] = useState<string | null>(null);\n  const box = useRef<HTMLElement>(null);\n  const refs = useRef<(SVGPathElement | null)[]>([]);\n\n  const values = data ? NAMES.map((n) => data[n]).filter((v): v is number => typeof v === \"number\") : [];\n  const min = values.length ? Math.min(...values) : 0;\n  const max = values.length ? Math.max(...values) : 0;\n  const sorted = [...values].sort((a, b) => a - b);\n  const level = (n: string) => {\n    const v = data?.[n];\n    if (typeof v !== \"number\") return undefined;\n    if (max === min) return steps;\n    // رتبهٔ اولین مقدار برابر، تا استان‌های هم‌مقدار یک رنگ بگیرند\n    // کمترین همیشه پلهٔ اول و بیشترین همیشه پلهٔ آخر؛ بقیه بر اساس رتبه بینشان پخش می‌شوند\n    if (scale === \"quantile\") return 1 + Math.round((sorted.indexOf(v) * (steps - 1)) / (sorted.length - 1));\n    return Math.min(steps, 1 + Math.floor(((v - min) / (max - min)) * steps));\n  };\n\n  // شدت رنگ هر پله؛ کم‌رنگ‌ترین پله هم از خاکستری «بدون داده» جدا دیده شود\n  const shade = (l: number) => `${Math.round(12 + (88 * l) / steps)}%`;\n\n  const toggle = (n: string) => {\n    let next: Selection;\n    if (multiple) {\n      const list = Array.isArray(selected) ? selected : [];\n      next = list.includes(n) ? list.filter((x) => x !== n) : [...list, n];\n    } else next = selected === n ? null : n;\n    if (value === undefined) setInner(next);\n    onChange?.(next);\n  };\n\n  const describe = (n: string) => {\n    const v = data?.[n];\n    return typeof v === \"number\" ? `${n}: ${format(v)}${unit ? ` ${unit}` : \"\"}` : n;\n  };\n\n  // فوکوس گردشی: یک استان در ترتیب Tab است و جهت‌نماها بین استان‌ها جابه‌جا می‌شوند\n  const onKey = (e: KeyboardEvent, i: number) => {\n    const move = { ArrowLeft: 1, ArrowDown: 1, ArrowRight: -1, ArrowUp: -1 }[e.key];\n    let next = i;\n    if (move) next = (i + move + NAMES.length) % NAMES.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = NAMES.length - 1;\n    else if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); toggle(NAMES[i]); return; }\n    else return;\n    e.preventDefault();\n    setActive(next);\n    setTip(NAMES[next]);\n    refs.current[next]?.focus();\n  };\n\n  const tipProvince = tip ? IRAN_PROVINCES[tip] : null;\n  const tipValue = tip ? data?.[tip] : undefined;\n  const showLegend = legend ?? Boolean(data);\n  const ratio = (VH / VW) * 100;\n\n  return (\n    <figure ref={box} className={cx(\"k-iran-map\", selectable && \"k-iran-map--selectable\", className)} onPointerLeave={() => setTip(null)}>\n      <div className=\"k-iran-map__canvas\" style={{ paddingBlockEnd: `${ratio}%` }}>\n        <svg\n          viewBox={IRAN_VIEWBOX}\n          className=\"k-iran-map__svg\"\n          role={selectable ? (multiple ? \"group\" : \"radiogroup\") : \"img\"}\n          onFocus={() => setInside(true)}\n          onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setInside(false); }}\n          aria-label={selectable ? label : `${label}${data ? M.iranMap.tableNote : \"\"}`}\n        >\n          {NAMES.map((n, i) => {\n            const p = IRAN_PROVINCES[n];\n            const on = isSelected(n);\n            return (\n              <path\n                key={n}\n                ref={(el) => { refs.current[i] = el; }}\n                d={p.d}\n                className=\"k-iran-map__province\"\n                data-l={level(n)}\n                style={level(n) ? ({ \"--_p\": shade(level(n)!) } as CSSProperties) : undefined}\n                data-selected={on ? \"\" : undefined}\n                data-active={tip === n ? \"\" : undefined}\n                role={selectable ? (multiple ? \"checkbox\" : \"radio\") : undefined}\n                aria-checked={selectable ? on : undefined}\n                aria-label={selectable ? describe(n) : undefined}\n                tabIndex={selectable ? (i === tabStop ? 0 : -1) : undefined}\n                onPointerEnter={() => setTip(n)}\n                onFocus={() => { setActive(i); setTip(n); }}\n                onBlur={() => setTip(null)}\n                onClick={selectable ? () => { setActive(i); toggle(n); } : undefined}\n                onKeyDown={selectable ? (e) => onKey(e, i) : undefined}\n              />\n            );\n          })}\n        </svg>\n        {tipProvince && (\n          <ChartTip\n            x={(tipProvince.x / VW) * (box.current?.clientWidth ?? VW)}\n            // راهنمای استان‌های شمالی بالای نقشه بیرون نزند (زیر سربرگ چسبان نرود)\n            y={Math.max(64, (tipProvince.y / VH) * ((box.current?.clientWidth ?? VW) * (VH / VW)) - 6)}\n            width={box.current?.clientWidth}\n            title={tip}\n            rows={[\n              ...(typeof tipValue === \"number\" ? [{ value: `${format(tipValue)}${unit ? ` ${unit}` : \"\"}` }] : []),\n              ...(tooltip ? [{ value: tooltip(tip!, tipValue) }] : []),\n              ...(typeof tipValue !== \"number\" && !tooltip ? [{ value: M.province.capital(CAPITAL[tip!]) }] : []),\n            ]}\n          />\n        )}\n      </div>\n\n      {showLegend && values.length > 0 && (\n        <figcaption className=\"k-iran-map__legend\">\n          <span>{format(min)}</span>\n          <span className=\"k-iran-map__scale\" aria-hidden=\"true\">\n            {Array.from({ length: steps }, (_, i) => <i key={i} style={{ \"--_p\": shade(i + 1) } as CSSProperties} />)}\n          </span>\n          <span>{format(max)}{unit ? ` ${unit}` : \"\"}</span>\n        </figcaption>\n      )}\n\n      {data && (\n        <table className=\"k-sr-only\">\n          <caption>{label}</caption>\n          <tbody>\n            {NAMES.filter((n) => typeof data[n] === \"number\").sort((a, b) => data[b]! - data[a]!).map((n) => (\n              <tr key={n}><th scope=\"row\">{n}</th><td>{format(data[n]!)}{unit ? ` ${unit}` : \"\"}</td></tr>\n            ))}\n          </tbody>\n        </table>\n      )}\n    </figure>\n  );\n}\n"
    }
  ]
}