{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "type": "registry:ui",
  "title": "Toaster",
  "description": "محل نمایش اعلان‌ها؛ یک بار در ریشهٔ برنامه قرار دهید",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/icons.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json",
    "https://kashi.sepantapouya.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/kashi/Toast.tsx",
      "type": "registry:ui",
      "target": "components/kashi/Toast.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, useSyncExternalStore, type ReactNode } from \"react\";\nimport { isDev } from \"./utils\";\nimport { cx } from \"./cx\";\nimport { CloseIcon, ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from \"./icons\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport type ToastTone = \"neutral\" | \"success\" | \"error\" | \"warning\" | \"info\";\n\nexport interface ToastOptions {\n  /** شناسهٔ دلخواه؛ toast() با همین شناسه اعلان قبلی را به‌روز می‌کند */\n  id?: number | string;\n  title: ReactNode;\n  description?: ReactNode;\n  tone?: ToastTone;\n  /** میلی‌ثانیه؛ Infinity برای ماندن تا بستن دستی. پیش‌فرض ۵ ثانیه، و بی‌نهایت اگر action داشته باشد */\n  duration?: number;\n  action?: { label: string; onClick: () => void };\n}\n\ninterface ToastItem extends Omit<ToastOptions, \"id\"> { id: number | string; closing?: boolean; loading?: boolean; version: number }\n\n// مخزن سادهٔ سراسری؛ هر جای برنامه می‌تواند toast() را صدا بزند\nlet items: ToastItem[] = [];\nlet nextId = 1;\nconst listeners = new Set<() => void>();\nconst emit = () => listeners.forEach((l) => l());\nconst subscribe = (l: () => void) => {\n  listeners.add(l);\n  return () => listeners.delete(l);\n};\n\nfunction dismiss(id: number | string) {\n  items = items.map((t) => (t.id === id ? { ...t, closing: true } : t));\n  emit();\n  setTimeout(() => {\n    items = items.filter((t) => t.id !== id);\n    emit();\n  }, 200);\n}\n\nfunction upsert(o: ToastOptions & { loading?: boolean }): number | string {\n  const id = o.id ?? nextId++;\n  const existing = items.find((t) => t.id === id);\n  const item: ToastItem = { ...o, id, closing: false, version: (existing?.version ?? 0) + 1 };\n  items = existing ? items.map((t) => (t.id === id ? item : t)) : [...items, item];\n  emit();\n  return id;\n}\n\n/** نمایش اعلان کوتاه؛ شناسهٔ آن را برمی‌گرداند */\nexport function toast(options: ToastOptions | string): number | string {\n  return upsert(typeof options === \"string\" ? { title: options } : options);\n}\ntoast.success = (title: ReactNode, o: Omit<ToastOptions, \"title\" | \"tone\"> = {}) => toast({ ...o, title, tone: \"success\" });\ntoast.error = (title: ReactNode, o: Omit<ToastOptions, \"title\" | \"tone\"> = {}) => toast({ ...o, title, tone: \"error\" });\ntoast.warning = (title: ReactNode, o: Omit<ToastOptions, \"title\" | \"tone\"> = {}) => toast({ ...o, title, tone: \"warning\" });\ntoast.info = (title: ReactNode, o: Omit<ToastOptions, \"title\" | \"tone\"> = {}) => toast({ ...o, title, tone: \"info\" });\ntoast.dismiss = dismiss;\n\ntype Msg<T> = ReactNode | ((value: T) => ReactNode);\n/**\n * اعلان برای کار زمان‌بر: «در حال ذخیره…» و سپس «ذخیره شد» یا پیام خطا، در همان اعلان.\n *   toast.promise(save(), { loading: \"در حال ذخیره…\", success: \"ذخیره شد\", error: (e) => e.message })\n */\ntoast.promise = <T,>(promise: Promise<T>, msgs: { loading: ReactNode; success: Msg<T>; error: Msg<unknown> }): Promise<T> => {\n  const id = upsert({ title: msgs.loading, tone: \"info\", duration: Infinity, loading: true });\n  const show = <V,>(m: Msg<V>, v: V) => (typeof m === \"function\" ? (m as (x: V) => ReactNode)(v) : m);\n  promise.then(\n    (v) => upsert({ id, title: show(msgs.success, v), tone: \"success\" }),\n    (e) => upsert({ id, title: show(msgs.error, e), tone: \"error\" }),\n  );\n  return promise;\n};\n\nconst ICONS: Record<ToastTone, () => ReactNode> = {\n  neutral: InfoIcon, info: InfoIcon, success: SuccessIcon, warning: WarningIcon, error: ErrorIcon,\n};\n\nfunction ToastView({ t, closeLabel }: { t: ToastItem; closeLabel: string }) {\n  const duration = t.duration ?? (t.action ? Infinity : 5000);\n  const remaining = useRef(duration);\n  // اعلان به‌روزشده (مثلاً پس از toast.promise) زمانش از نو شروع می‌شود\n  useEffect(() => {\n    remaining.current = duration;\n  }, [t.version]);\n  const started = useRef(0);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const [paused, setPaused] = useState(false);\n\n  // زمان‌سنج با توقف هنگام hover یا فوکوس (WCAG 2.2.1)\n  useEffect(() => {\n    if (!Number.isFinite(duration) || paused || t.closing) return;\n    started.current = Date.now();\n    timer.current = setTimeout(() => dismiss(t.id), remaining.current);\n    return () => {\n      clearTimeout(timer.current);\n      remaining.current -= Date.now() - started.current;\n    };\n  }, [paused, t.closing, t.version]);\n\n  const Icon = ICONS[t.tone ?? \"neutral\"];\n  const icon = t.loading ? <span className=\"k-spinner k-spinner--sm\" style={{ color: \"inherit\" }} /> : <Icon />;\n  return (\n    <li\n      className={cx(\"k-toast\", t.tone && t.tone !== \"neutral\" && `k-toast--${t.tone}`)}\n      data-state={t.closing ? \"closing\" : \"open\"}\n      role={t.tone === \"error\" ? \"alert\" : \"status\"}\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocus={() => setPaused(true)}\n      onBlur={() => setPaused(false)}\n      onKeyDown={(e) => e.key === \"Escape\" && dismiss(t.id)}\n    >\n      <span className=\"k-toast__icon\" aria-hidden=\"true\">{icon}</span>\n      <div className=\"k-toast__content\">\n        <p className=\"k-toast__title\">{t.title}</p>\n        {t.description && <p className=\"k-toast__description\">{t.description}</p>}\n        {t.action && (\n          <button\n            type=\"button\"\n            className=\"k-btn k-btn--link k-toast__action\"\n            onClick={() => {\n              t.action!.onClick();\n              dismiss(t.id);\n            }}\n          >\n            {t.action.label}\n          </button>\n        )}\n      </div>\n      <button type=\"button\" className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm k-toast__close\" aria-label={closeLabel} onClick={() => dismiss(t.id)}>\n        <CloseIcon />\n      </button>\n      {Number.isFinite(duration) && <span key={t.version} className=\"k-toast__progress\" aria-hidden=\"true\" style={{ animationDuration: `${duration}ms` }} />}\n    </li>\n  );\n}\n\nexport type ToastPlacement = \"top-start\" | \"top-center\" | \"top-end\" | \"bottom-start\" | \"bottom-center\" | \"bottom-end\";\n\nexport interface ToasterProps {\n  /** جای اعلان‌ها؛ در RTL «end» سمت چپ است @default \"bottom-end\" */\n  placement?: ToastPlacement;\n  /** @deprecated از placement استفاده کنید */\n  position?: \"top\" | \"bottom\";\n  /** بیشترین تعداد اعلان هم‌زمان؛ قدیمی‌ترها پنهان می‌شوند @default 5 */\n  max?: number;\n  /** @default \"اعلان‌ها\" */\n  label?: string;\n  /** @default \"بستن اعلان\" */\n  closeLabel?: string;\n}\n\n// اگر چند Toaster هم‌زمان در صفحه باشد، فقط آخرینِ نصب‌شده اعلان‌ها را نشان می‌دهد تا تکراری نشوند\nlet toasters: string[] = [];\nconst toasterListeners = new Set<() => void>();\nconst emitToasters = () => toasterListeners.forEach((l) => l());\nconst subscribeToasters = (l: () => void) => {\n  toasterListeners.add(l);\n  return () => toasterListeners.delete(l);\n};\n\n/** محل نمایش اعلان‌ها؛ یک بار در ریشهٔ برنامه قرار دهید */\nexport function Toaster({ placement, position, max = 5, label, closeLabel }: ToasterProps) {\n  const { messages: M } = useKashi();\n  label ??= M.toast.region;\n  closeLabel ??= M.toast.close;\n  const id = useId();\n  const all = useSyncExternalStore(subscribe, () => items, () => items);\n  const active = useSyncExternalStore(subscribeToasters, () => toasters[toasters.length - 1], () => undefined);\n  useEffect(() => {\n    toasters = [...toasters, id];\n    if (isDev() && toasters.length > 1) console.warn(\"[kashi] بیش از یک <Toaster> در صفحه است؛ فقط آخرین آن‌ها اعلان‌ها را نشان می‌دهد.\");\n    emitToasters();\n    return () => {\n      toasters = toasters.filter((t) => t !== id);\n      emitToasters();\n    };\n  }, [id]);\n  // تا وقتی Toasterی ثبت نشده (مثلاً رندر سرور) همین Toaster نشان می‌دهد\n  const list = active === undefined || active === id ? all.slice(-max) : [];\n  const place: ToastPlacement = placement ?? (position === \"top\" ? \"top-end\" : \"bottom-end\");\n  return (\n    <section aria-label={label}>\n      {/* ناحیهٔ زنده باید از قبل در صفحه باشد تا اعلان‌های جدید خوانده شوند */}\n      <ol className={cx(\"k-toaster\", `k-toaster--${place}`)} data-placement={place} aria-live=\"polite\">\n        {list.map((t) => <ToastView key={t.id} t={t} closeLabel={closeLabel} />)}\n      </ol>\n    </section>\n  );\n}\n"
    }
  ]
}