{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "description": "A complete toast / notification system in one file: call `toast()` — or `toast.success` / `.error` / `.info` / `.warning` / `.loading` / `.promise` — from anywhere in your app, and render a single `<Toaster />` at the root. No provider, no context, nothing to wire up. Use it for save and delete confirmations, form submission results, copy-to-clipboard feedback, async job status, optimistic updates that may fail, undo prompts, rate-limit and validation errors — any \"it worked\" or \"it failed\" message that should not interrupt what the user is doing. Common asks it answers: \"toast notification react\", \"toast component shadcn\", \"snackbar component\", \"notification popup\", \"flash message\", \"alert toast\", \"undo toast with action button\", \"promise toast for async requests\", \"loading toast that turns into success\", \"toast without a provider\", \"toast from outside a component\", \"sonner alternative\", \"react-hot-toast alternative\", \"react-toastify alternative\", \"notification system react\", \"show a message after form submit\". `toast.promise(request, { loading, success, error })` moves one toast through all three states in place, so an async call needs a single line instead of a chain of manual dismissals — `success` and `error` also accept a function, so the final message can quote the resolved value or the thrown error. Every toast takes a `description`, an `action` button (the undo affordance), a `duration` where `Infinity` pins it until dismissed, and an `id` you can reuse to update a toast already on screen. `<Toaster />` takes any of six positions. The queue lives outside React through useSyncExternalStore, so a toast can be fired from an event handler, a fetch or axios interceptor, a route guard, or a plain module — the places a hook-based API cannot reach, and the usual reason a toast library ends up wrapped in a context that has to be threaded everywhere. The details a rushed implementation drops: auto-dismiss pauses while the pointer is over the stack or focus is inside it, so a toast cannot vanish mid-sentence or while a keyboard user is reaching for its action button, and it stays paused across a promise's loading → success swap. Errors announce assertive and everything else polite, so a failure is not queued behind three success messages. Swipe-to-dismiss on touch, and enter / exit animations that respect prefers-reduced-motion. Official shadcn/ui offers two, and both hand you a package to carry: its toast is built on @radix-ui/react-toast and only works once you have mounted a ToastProvider and a ToastViewport and threaded its useToast hook to every caller, and its sonner is a wrapper around the sonner package plus next-themes. This is one file you own and can edit, styled with your own theme tokens, with no provider to mount and no context to thread, and lucide-react — already present in a shadcn project — as its only package import.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/toast.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  CheckCircle2,\n  Info,\n  Loader2,\n  TriangleAlert,\n  X,\n  XCircle,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * A self-contained toast/notification system. No provider or context required —\n * call `toast(...)` from anywhere (event handlers, async code, outside React) and\n * render a single `<Toaster />` at your app root (outside any `transform`ed or\n * `overflow-hidden` container, since it positions itself with `position: fixed`).\n *\n * Features: imperative API with `success`/`error`/`info`/`warning`/`loading` and a\n * promise helper; auto-dismiss with pause-on-hover/focus; swipe-to-dismiss; six\n * positions; accessible live regions (assertive for errors, polite otherwise);\n * enter/exit animation that respects `prefers-reduced-motion`.\n */\n\nexport type ToastType = \"default\" | \"success\" | \"error\" | \"info\" | \"warning\" | \"loading\"\n\nexport interface ToastAction {\n  label: string\n  onClick: () => void\n}\n\nexport interface ToastOptions {\n  /** Reuse an id to update an existing toast in place (used by `toast.promise`). */\n  id?: string\n  description?: React.ReactNode\n  /** Auto-dismiss after this many ms. `Infinity` keeps it until dismissed. */\n  duration?: number\n  action?: ToastAction\n}\n\ninterface ToastRecord {\n  id: string\n  type: ToastType\n  title: React.ReactNode\n  description?: React.ReactNode\n  duration: number\n  action?: ToastAction\n  /** false once dismissed — kept mounted briefly so the exit can animate. */\n  open: boolean\n}\n\n// --- module store (framework-agnostic, so `toast()` works without React context) ---\n\nconst DEFAULT_DURATION = 4000\nconst EXIT_MS = 220 // keep in sync with the CSS transition below\nconst MAX_TOASTS = 5\n\nlet records: ToastRecord[] = []\nlet counter = 0\nconst listeners = new Set<() => void>()\nconst removalTimers = new Map<string, ReturnType<typeof setTimeout>>()\n\nfunction emit(next: ToastRecord[]) {\n  records = next\n  for (const l of listeners) l()\n}\n\nfunction cancelRemoval(id: string) {\n  const t = removalTimers.get(id)\n  if (t) {\n    clearTimeout(t)\n    removalTimers.delete(id)\n  }\n}\n\nfunction scheduleRemoval(id: string) {\n  cancelRemoval(id)\n  removalTimers.set(\n    id,\n    setTimeout(() => {\n      removalTimers.delete(id)\n      emit(records.filter((r) => r.id !== id))\n    }, EXIT_MS)\n  )\n}\n\nfunction upsert(rec: ToastRecord) {\n  // A re-used id may be mid-exit; cancel its pending removal so it stays put.\n  cancelRemoval(rec.id)\n  const i = records.findIndex((r) => r.id === rec.id)\n  if (i >= 0) {\n    const next = records.slice()\n    next[i] = rec\n    emit(next)\n  } else {\n    // Cap the queue: once over budget, drop the oldest toast (overflow is removed\n    // immediately, without an exit animation — it's a hard cap, not a dismissal).\n    const next = [...records, rec]\n    emit(next.length > MAX_TOASTS ? next.slice(next.length - MAX_TOASTS) : next)\n  }\n}\n\nfunction create(type: ToastType, title: React.ReactNode, opts: ToastOptions = {}) {\n  const id = opts.id ?? `toast-${++counter}`\n  const duration =\n    opts.duration ?? (type === \"loading\" ? Number.POSITIVE_INFINITY : DEFAULT_DURATION)\n  upsert({\n    id,\n    type,\n    title,\n    description: opts.description,\n    duration,\n    action: opts.action,\n    open: true,\n  })\n  return id\n}\n\nfunction dismiss(id?: string) {\n  if (id == null) {\n    // Dismiss everything that's currently open.\n    for (const r of records) if (r.open) scheduleRemoval(r.id)\n    emit(records.map((r) => (r.open ? { ...r, open: false } : r)))\n    return\n  }\n  const r = records.find((x) => x.id === id)\n  if (!r || !r.open) return\n  scheduleRemoval(id)\n  emit(records.map((x) => (x.id === id ? { ...x, open: false } : x)))\n}\n\ntype PromiseMessages<T> = {\n  loading: React.ReactNode\n  success: React.ReactNode | ((value: T) => React.ReactNode)\n  error: React.ReactNode | ((err: unknown) => React.ReactNode)\n}\n\nexport const toast = Object.assign(\n  (title: React.ReactNode, opts?: ToastOptions) => create(\"default\", title, opts),\n  {\n    success: (title: React.ReactNode, opts?: ToastOptions) => create(\"success\", title, opts),\n    error: (title: React.ReactNode, opts?: ToastOptions) => create(\"error\", title, opts),\n    info: (title: React.ReactNode, opts?: ToastOptions) => create(\"info\", title, opts),\n    warning: (title: React.ReactNode, opts?: ToastOptions) => create(\"warning\", title, opts),\n    loading: (title: React.ReactNode, opts?: ToastOptions) => create(\"loading\", title, opts),\n    dismiss,\n    /** Drive a toast from a promise: loading → success/error, updated in place. */\n    promise<T>(promise: Promise<T>, messages: PromiseMessages<T>) {\n      const id = create(\"loading\", messages.loading, {\n        duration: Number.POSITIVE_INFINITY,\n      })\n      promise.then(\n        (value) =>\n          create(\"success\", resolveMessage(messages.success, value), { id }),\n        (err) => create(\"error\", resolveMessage(messages.error, err), { id })\n      )\n      return promise\n    },\n  }\n)\n\nfunction resolveMessage<T>(\n  msg: React.ReactNode | ((arg: T) => React.ReactNode),\n  arg: T\n): React.ReactNode {\n  return typeof msg === \"function\"\n    ? (msg as (arg: T) => React.ReactNode)(arg)\n    : msg\n}\n\n// --- store subscription for the renderer ---\n\nconst EMPTY: ToastRecord[] = []\nfunction subscribe(cb: () => void) {\n  listeners.add(cb)\n  return () => {\n    listeners.delete(cb)\n  }\n}\nfunction getSnapshot() {\n  return records\n}\nfunction getServerSnapshot() {\n  return EMPTY\n}\n\nfunction usePrefersReducedMotion() {\n  const [reduced, setReduced] = React.useState(false)\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const onChange = () => setReduced(mq.matches)\n    onChange()\n    mq.addEventListener(\"change\", onChange)\n    return () => mq.removeEventListener(\"change\", onChange)\n  }, [])\n  return reduced\n}\n\n// --- rendering ---\n\nexport type ToastPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\"\n\nexport interface ToasterProps {\n  position?: ToastPosition\n  /** Gap between stacked toasts, in px. */\n  gap?: number\n}\n\nconst POSITION_CLASSES: Record<ToastPosition, string> = {\n  \"top-left\": \"top-0 left-0 items-start\",\n  \"top-center\": \"top-0 left-1/2 -translate-x-1/2 items-center\",\n  \"top-right\": \"top-0 right-0 items-end\",\n  \"bottom-left\": \"bottom-0 left-0 items-start\",\n  \"bottom-center\": \"bottom-0 left-1/2 -translate-x-1/2 items-center\",\n  \"bottom-right\": \"bottom-0 right-0 items-end\",\n}\n\nexport function Toaster({ position = \"bottom-right\", gap = 12 }: ToasterProps) {\n  const list = React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n  const isTop = position.startsWith(\"top\")\n\n  return (\n    // An <ol> (not a <section>) so the <li> toasts nest in a valid list; the\n    // aria-label names the region for assistive tech. `m-0 list-none` keeps it\n    // reset even without Tailwind's preflight.\n    <ol\n      aria-label=\"Notifications\"\n      className={cn(\n        \"pointer-events-none fixed z-[100] m-0 flex w-full max-w-[400px] list-none flex-col p-4\",\n        // newest toast sits closest to the screen edge\n        isTop ? \"flex-col\" : \"flex-col-reverse\",\n        POSITION_CLASSES[position]\n      )}\n      style={{ gap }}\n    >\n      {list.map((record) => (\n        <ToastItem key={record.id} record={record} fromTop={isTop} />\n      ))}\n    </ol>\n  )\n}\n\nconst ICONS: Partial<Record<ToastType, React.ReactNode>> = {\n  success: <CheckCircle2 className=\"size-5 text-emerald-500\" />,\n  error: <XCircle className=\"size-5 text-destructive\" />,\n  warning: <TriangleAlert className=\"size-5 text-amber-500\" />,\n  info: <Info className=\"size-5 text-sky-500\" />,\n  loading: <Loader2 className=\"size-5 animate-spin text-muted-foreground\" />,\n}\n\nconst SWIPE_THRESHOLD = 80\n\nfunction ToastItem({ record, fromTop }: { record: ToastRecord; fromTop: boolean }) {\n  const { id, type, title, description, action, duration, open } = record\n  const reduced = usePrefersReducedMotion()\n\n  // Enter animation: start offset, then settle on the next frame.\n  const [entered, setEntered] = React.useState(false)\n  React.useEffect(() => {\n    const raf = requestAnimationFrame(() => setEntered(true))\n    return () => cancelAnimationFrame(raf)\n  }, [])\n\n  // Auto-dismiss timer with pause-on-hover/focus. Tracks remaining time so a\n  // resumed toast doesn't restart from the full duration.\n  const remaining = React.useRef(duration)\n  const startedAt = React.useRef(0)\n  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n  const paused = React.useRef(false)\n  // True while the pointer is over the toast or focus is inside it — the countdown\n  // stays paused the whole time, including across a promise loading → success swap.\n  const over = React.useRef(false)\n\n  const clearTimer = React.useCallback(() => {\n    if (timer.current) {\n      clearTimeout(timer.current)\n      timer.current = undefined\n    }\n  }, [])\n\n  const run = React.useCallback(() => {\n    clearTimer()\n    if (!open || !Number.isFinite(remaining.current)) return\n    startedAt.current = Date.now()\n    timer.current = setTimeout(() => dismiss(id), remaining.current)\n  }, [open, id, clearTimer])\n\n  // (Re)start whenever the toast is (re)opened or its duration changes (e.g. a\n  // promise toast going loading → success). If the user is interacting with it,\n  // keep it paused instead of auto-dismissing under their cursor.\n  React.useEffect(() => {\n    remaining.current = duration\n    if (over.current) {\n      paused.current = true\n      clearTimer()\n    } else {\n      paused.current = false\n      run()\n    }\n    return clearTimer\n  }, [duration, open, type, run, clearTimer])\n\n  const pause = React.useCallback(() => {\n    if (paused.current || !Number.isFinite(remaining.current)) return\n    paused.current = true\n    clearTimer()\n    remaining.current -= Date.now() - startedAt.current\n  }, [clearTimer])\n\n  const resume = React.useCallback(() => {\n    // Only actually resume once the pointer/focus has truly left the toast.\n    if (!paused.current || over.current) return\n    paused.current = false\n    run()\n  }, [run])\n\n  const onEnter = React.useCallback(() => {\n    over.current = true\n    pause()\n  }, [pause])\n  const onLeave = React.useCallback(() => {\n    over.current = false\n    resume()\n  }, [resume])\n\n  // Swipe-to-dismiss.\n  const [dx, setDx] = React.useState(0)\n  const dragging = React.useRef(false)\n  const pointerStart = React.useRef(0)\n\n  function onPointerDown(e: React.PointerEvent) {\n    if (e.pointerType === \"mouse\" && e.button !== 0) return\n    dragging.current = true\n    pointerStart.current = e.clientX\n    e.currentTarget.setPointerCapture?.(e.pointerId)\n    pause()\n  }\n  function onPointerMove(e: React.PointerEvent) {\n    if (!dragging.current) return\n    setDx(e.clientX - pointerStart.current)\n  }\n  function endDrag(e: React.PointerEvent) {\n    if (!dragging.current) return\n    dragging.current = false\n    const moved = e.clientX - pointerStart.current\n    if (Math.abs(moved) > SWIPE_THRESHOLD) {\n      // Reset the swipe offset so the exit animates via the opacity/translateY\n      // branch instead of freezing at the half-swiped position.\n      setDx(0)\n      dismiss(id)\n    } else {\n      setDx(0)\n      resume()\n    }\n  }\n\n  const isDragging = dragging.current\n  const visible = entered && open\n  const enterOffset = fromTop ? -16 : 16\n\n  const transform = dx !== 0 ? `translateX(${dx}px)` : `translateY(${visible ? 0 : enterOffset}px)`\n  const opacity = dx !== 0 ? Math.max(0, 1 - Math.abs(dx) / (SWIPE_THRESHOLD * 2)) : visible ? 1 : 0\n  const transition =\n    reduced || isDragging\n      ? \"none\"\n      : \"transform .22s cubic-bezier(.21,1.02,.73,1), opacity .22s ease\"\n\n  const assertive = type === \"error\" || type === \"warning\"\n\n  return (\n    <li className=\"pointer-events-auto w-full list-none\" style={{ transform, opacity, transition }}>\n      <div\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        onMouseEnter={onEnter}\n        onMouseLeave={onLeave}\n        onFocus={onEnter}\n        onBlur={(e) => {\n          // Ignore focus moving between the toast's own buttons; resume only once\n          // focus has truly left the toast.\n          if (!e.currentTarget.contains(e.relatedTarget as Node)) onLeave()\n        }}\n        className={cn(\n          \"flex items-start gap-3 rounded-lg border bg-background p-4 pr-10 shadow-lg\",\n          \"relative touch-pan-y select-none\"\n        )}\n      >\n        {ICONS[type] ? <span className=\"mt-0.5 shrink-0\">{ICONS[type]}</span> : null}\n        <div\n          key={type}\n          role={assertive ? \"alert\" : \"status\"}\n          aria-live={assertive ? \"assertive\" : \"polite\"}\n          aria-atomic=\"true\"\n          className=\"min-w-0 flex-1\"\n        >\n          <div className=\"text-sm font-medium text-foreground\">{title}</div>\n          {description ? (\n            <div className=\"mt-1 text-sm text-muted-foreground\">{description}</div>\n          ) : null}\n          {action ? (\n            <button\n              type=\"button\"\n              onClick={() => {\n                action.onClick()\n                dismiss(id)\n              }}\n              className=\"mt-2 inline-flex h-7 items-center rounded-md border bg-transparent px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              {action.label}\n            </button>\n          ) : null}\n        </div>\n        <button\n          type=\"button\"\n          aria-label=\"Dismiss notification\"\n          onClick={() => dismiss(id)}\n          className=\"absolute right-2 top-2 inline-flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        >\n          <X className=\"size-4\" />\n        </button>\n      </div>\n    </li>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}