{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "type": "registry:ui",
  "title": "Toast",
  "description": "Toast notifications with a toast() API for success, error, warning and promise states. The Toaster stacks cards, fans them out on hover and pauses timers.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/toast.tsx",
      "type": "registry:ui",
      "target": "components/ui/toast.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { AlertTriangleIcon, CheckIcon, InfoIcon, LoaderCircleIcon, XIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\ntype Tone = \"neutral\" | \"success\" | \"danger\" | \"warning\" | \"loading\"\n\nconst ICON: Record<Tone, React.ReactNode> = {\n  neutral: <InfoIcon className=\"size-3.5\" />,\n  success: <CheckIcon className=\"size-3.5\" strokeWidth={2.5} />,\n  danger: <XIcon className=\"size-3.5\" strokeWidth={2.5} />,\n  warning: <AlertTriangleIcon className=\"size-3.5\" />,\n  loading: <LoaderCircleIcon className=\"size-3.5 animate-spin\" />,\n}\n\nconst ICON_TONE: Record<Tone, string> = {\n  neutral: \"bg-sunken text-fg-muted\",\n  success: \"bg-success text-white\",\n  danger: \"bg-danger text-white\",\n  warning: \"bg-warning text-[oklch(0.25_0.05_75)]\",\n  loading: \"bg-accent-soft text-accent-fg\",\n}\n\n/**\n * Toast — a single notification card: status glyph, title, optional\n * description and action. Use <Toaster /> + toast() for the stacked system.\n */\nfunction Toast({\n  title,\n  description,\n  tone = \"neutral\",\n  action,\n  onDismiss,\n  className,\n}: {\n  title: React.ReactNode\n  description?: React.ReactNode\n  tone?: Tone\n  action?: { label: string; onClick: () => void }\n  onDismiss?: () => void\n  className?: string\n}) {\n  return (\n    <div\n      data-slot=\"toast\"\n      data-tone={tone}\n      role={tone === \"danger\" ? \"alert\" : \"status\"}\n      className={cn(\n        \"group/toast relative flex w-[356px] max-w-full items-start gap-3 rounded-xl border border-border bg-raised p-3.5 pr-10 shadow-overlay\",\n        className\n      )}\n    >\n      <span className={cn(\"mt-px grid size-5 shrink-0 place-items-center rounded-full\", ICON_TONE[tone])}>{ICON[tone]}</span>\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"text-[0.8125rem] font-medium text-fg\">{title}</p>\n        {description ? <p className=\"mt-0.5 text-[0.8125rem] leading-[1.5] text-fg-muted\">{description}</p> : null}\n        {action ? (\n          <button\n            type=\"button\"\n            onClick={action.onClick}\n            className=\"mt-2 inline-flex h-7 items-center rounded-md bg-ink px-2.5 text-xs font-medium text-on-ink shadow-ink outline-none hover:bg-ink-hover focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-raised\"\n          >\n            {action.label}\n          </button>\n        ) : null}\n      </div>\n      {onDismiss ? (\n        <button\n          type=\"button\"\n          aria-label=\"Dismiss\"\n          onClick={onDismiss}\n          className=\"absolute top-2.5 right-2.5 grid size-6 place-items-center rounded-md text-fg-subtle opacity-0 outline-none transition-opacity group-hover/toast:opacity-100 hover:bg-sunken hover:text-fg focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-accent\"\n        >\n          <XIcon className=\"size-3.5\" />\n        </button>\n      ) : null}\n    </div>\n  )\n}\n\nfunction ToastStack({ children, className }: { children?: React.ReactNode; className?: string }) {\n  return (\n    <div data-slot=\"toast-stack\" className={cn(\"flex flex-col gap-2\", className)}>\n      {children}\n    </div>\n  )\n}\n\n/* ── Toaster: a stacked, self-dismissing system ──────────────────────────── */\n\ntype ToastItem = {\n  id: number\n  title: React.ReactNode\n  description?: React.ReactNode\n  tone: Tone\n  action?: { label: string; onClick: () => void }\n  duration: number\n}\n\ntype Listener = (items: ToastItem[]) => void\nlet items: ToastItem[] = []\nlet nextId = 1\nconst listeners = new Set<Listener>()\nconst emit = () => listeners.forEach((l) => l(items))\n\nfunction push(t: Omit<ToastItem, \"id\" | \"duration\" | \"tone\"> & { tone?: Tone; duration?: number }) {\n  const item: ToastItem = { id: nextId++, tone: \"neutral\", duration: 4000, ...t }\n  items = [item, ...items].slice(0, 6)\n  emit()\n  return item.id\n}\n\nfunction update(id: number, patch: Partial<ToastItem>) {\n  items = items.map((t) => (t.id === id ? { ...t, ...patch } : t))\n  emit()\n}\n\nfunction dismiss(id: number) {\n  items = items.filter((t) => t.id !== id)\n  emit()\n}\n\ntype Opts = { description?: React.ReactNode; action?: { label: string; onClick: () => void }; duration?: number }\n\n/** Imperative API: toast(\"Saved\"), toast.success(…), toast.error(…), toast.promise(p, {…}). */\nconst toast = Object.assign((title: React.ReactNode, o?: Opts) => push({ title, ...o }), {\n  success: (title: React.ReactNode, o?: Opts) => push({ title, tone: \"success\", ...o }),\n  error: (title: React.ReactNode, o?: Opts) => push({ title, tone: \"danger\", ...o }),\n  warning: (title: React.ReactNode, o?: Opts) => push({ title, tone: \"warning\", ...o }),\n  dismiss,\n  promise<T>(p: Promise<T>, m: { loading: React.ReactNode; success: React.ReactNode | ((v: T) => React.ReactNode); error: React.ReactNode }) {\n    const id = push({ title: m.loading, tone: \"loading\", duration: Infinity })\n    p.then(\n      (v) => update(id, { title: typeof m.success === \"function\" ? (m.success as (v: T) => React.ReactNode)(v) : m.success, tone: \"success\", duration: 4000 }),\n      () => update(id, { title: m.error, tone: \"danger\", duration: 5000 })\n    )\n    return p\n  },\n})\n\nfunction ToasterItem({ t, index, expanded, onHeight }: { t: ToastItem; index: number; expanded: boolean; onHeight: (id: number, h: number) => void; }) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  React.useLayoutEffect(() => {\n    if (ref.current) onHeight(t.id, ref.current.offsetHeight)\n  })\n  React.useEffect(() => {\n    if (!Number.isFinite(t.duration) || expanded) return\n    const h = window.setTimeout(() => dismiss(t.id), t.duration)\n    return () => window.clearTimeout(h)\n  }, [t.id, t.duration, t.tone, expanded])\n  return (\n    <div ref={ref} data-index={index} className=\"absolute right-0 bottom-0 w-full animate-[rise-in_260ms_var(--ease-hairline)_both]\">\n      <Toast className=\"w-full\" title={t.title} description={t.description} tone={t.tone} action={t.action} onDismiss={() => dismiss(t.id)} />\n    </div>\n  )\n}\n\n/**\n * Toaster — mount once. Toasts stack like cards; hover to fan them out.\n * Timers pause while expanded.\n */\nfunction Toaster({ className }: { className?: string }) {\n  const [list, setList] = React.useState<ToastItem[]>(items)\n  const [expanded, setExpanded] = React.useState(false)\n  const [heights, setHeights] = React.useState<Record<number, number>>({})\n  React.useEffect(() => {\n    const l: Listener = (x) => setList([...x])\n    listeners.add(l)\n    return () => { listeners.delete(l) }\n  }, [])\n  const onHeight = React.useCallback((id: number, h: number) => setHeights((s) => (s[id] === h ? s : { ...s, [id]: h })), [])\n  const visible = list.slice(0, expanded ? 6 : 3)\n  let offset = 0\n  return (\n    <ol\n      data-slot=\"toaster\"\n      aria-live=\"polite\"\n      onMouseEnter={() => setExpanded(true)}\n      onMouseLeave={() => setExpanded(false)}\n      onFocus={() => setExpanded(true)}\n      onBlur={() => setExpanded(false)}\n      className={cn(\"fixed right-4 bottom-4 z-[60] w-[356px] max-w-[calc(100vw-2rem)]\", className)}\n      style={{ height: expanded ? visible.reduce((a, t) => a + (heights[t.id] ?? 72) + 8, 0) : (heights[visible[0]?.id] ?? 0) + Math.min(visible.length - 1, 2) * 10 }}\n    >\n      {visible.map((t, i) => {\n        const y = expanded ? -offset : -i * 10\n        const scale = expanded ? 1 : 1 - i * 0.05\n        offset += (heights[t.id] ?? 72) + 8\n        return (\n          <li\n            key={t.id}\n            className={cn(\n              \"absolute right-0 bottom-0 w-full list-none transition-[transform,opacity] duration-300 ease-hairline\",\n              \"[&_[data-slot=toast]>*]:transition-opacity [&_[data-slot=toast]>*]:duration-200\",\n              !expanded && i > 0 && \"[&_[data-slot=toast]]:max-h-[var(--front-h)] [&_[data-slot=toast]]:overflow-hidden [&_[data-slot=toast]>*]:opacity-0\"\n            )}\n            style={{ transform: `translateY(${y}px) scale(${scale})`, transformOrigin: \"bottom center\", opacity: !expanded && i > 2 ? 0 : 1, zIndex: 10 - i, [\"--front-h\" as string]: `${heights[visible[0]?.id] ?? 72}px` }}\n          >\n            <ToasterItem t={t} index={i} expanded={expanded} onHeight={onHeight} />\n          </li>\n        )\n      })}\n    </ol>\n  )\n}\n\nexport { Toast, ToastStack, Toaster, toast }\n"
    }
  ]
}
