{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "author": "Zubeyr <https://zubeyr.dev>",
  "description": "Notifications you can raise from anywhere with toast(): success, error, warning, info and loading tones, a promise helper for saves, action buttons like Undo, pause on hover, stacking with “clear all”, and screen-reader announcements.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/dashboard/components/toast/toast.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nexport type ToastTone = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading'\n\nexport type ToastOptions = {\n  id?: string\n  description?: React.ReactNode\n  tone?: ToastTone\n  duration?: number\n  action?: { label: string; onClick: () => void }\n  onDismiss?: () => void\n}\n\ntype ToastItem = ToastOptions & {\n  id: string\n  title: React.ReactNode\n  tone: ToastTone\n  duration: number\n  createdAt: number\n}\n\nlet toasts: ToastItem[] = []\nlet mounted = 0\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}\nconst snapshot = () => toasts\nconst EMPTY: ToastItem[] = []\nconst serverSnapshot = () => EMPTY\n\nlet counter = 0\nconst defaults: Record<ToastTone, number> = {\n  default: 5000,\n  success: 4000,\n  info: 5000,\n  warning: 6000,\n  error: 8000,\n  loading: Infinity,\n}\n\nfunction show(title: React.ReactNode, options: ToastOptions = {}) {\n  const id = options.id ?? `t${++counter}`\n  const tone = options.tone ?? 'default'\n  const existing = toasts.find((t) => t.id === id)\n  const item: ToastItem = {\n    ...existing,\n    ...options,\n    id,\n    title,\n    tone,\n    duration: options.duration ?? defaults[tone],\n    createdAt: Date.now(),\n  }\n  toasts = existing ? toasts.map((t) => (t.id === id ? item : t)) : [...toasts, item]\n  emit()\n  return id\n}\n\nfunction dismiss(id?: string) {\n  const gone = id ? toasts.filter((t) => t.id === id) : toasts\n  toasts = id ? toasts.filter((t) => t.id !== id) : []\n  gone.forEach((t) => t.onDismiss?.())\n  emit()\n}\n\ntype Messages<T> = {\n  loading: React.ReactNode\n  success: React.ReactNode | ((value: T) => React.ReactNode)\n  error: React.ReactNode | ((error: unknown) => React.ReactNode)\n}\n\nexport const toast = Object.assign(show, {\n  success: (title: React.ReactNode, o?: ToastOptions) => show(title, { ...o, tone: 'success' }),\n  error: (title: React.ReactNode, o?: ToastOptions) => show(title, { ...o, tone: 'error' }),\n  warning: (title: React.ReactNode, o?: ToastOptions) => show(title, { ...o, tone: 'warning' }),\n  info: (title: React.ReactNode, o?: ToastOptions) => show(title, { ...o, tone: 'info' }),\n  loading: (title: React.ReactNode, o?: ToastOptions) => show(title, { ...o, tone: 'loading' }),\n  dismiss,\n  promise<T>(promise: Promise<T>, messages: Messages<T>, o?: ToastOptions) {\n    const id = show(messages.loading, { ...o, tone: 'loading' })\n    promise.then(\n      (value) =>\n        show(typeof messages.success === 'function' ? messages.success(value) : messages.success, {\n          ...o,\n          id,\n          tone: 'success',\n          duration: o?.duration,\n        }),\n      (error) =>\n        show(typeof messages.error === 'function' ? messages.error(error) : messages.error, {\n          ...o,\n          id,\n          tone: 'error',\n          duration: o?.duration,\n        }),\n    )\n    return promise\n  },\n  isMounted: () => mounted > 0,\n})\n\nconst icons: Record<ToastTone, React.ReactNode> = {\n  default: null,\n  success: <CheckCircle2 className=\"text-emerald-600 dark:text-emerald-400\" />,\n  error: <XCircle className=\"text-destructive\" />,\n  warning: <AlertTriangle className=\"text-amber-600 dark:text-amber-400\" />,\n  info: <Info className=\"text-sky-600 dark:text-sky-400\" />,\n  loading: <Loader2 className=\"animate-spin text-muted-foreground motion-reduce:animate-none\" />,\n}\n\nfunction ToastCard({ item, paused }: { item: ToastItem; paused: boolean }) {\n  const [entered, setEntered] = useState(false)\n  const remaining = useRef(item.duration)\n  const started = useRef(0)\n\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => setEntered(true))\n    return () => cancelAnimationFrame(frame)\n  }, [])\n\n  useEffect(() => {\n    remaining.current = item.duration\n  }, [item.createdAt, item.duration])\n\n  useEffect(() => {\n    if (paused || !Number.isFinite(remaining.current)) return\n    started.current = Date.now()\n    const timer = setTimeout(() => dismiss(item.id), remaining.current)\n    return () => {\n      clearTimeout(timer)\n      remaining.current -= Date.now() - started.current\n    }\n  }, [paused, item.id, item.createdAt])\n\n  return (\n    <li\n      role={item.tone === 'error' ? 'alert' : 'status'}\n      aria-live={item.tone === 'error' ? 'assertive' : 'polite'}\n      aria-atomic\n      className={cn(\n        'pointer-events-auto flex w-full items-start gap-3 rounded-xl bg-popover p-3.5 text-sm text-popover-foreground shadow-(--shadow-overlay) transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none [&_svg]:size-4 [&_svg]:shrink-0',\n        entered ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0',\n      )}\n    >\n      {icons[item.tone] && <span className=\"mt-0.5 flex\">{icons[item.tone]}</span>}\n      <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n        <p className=\"font-medium\">{item.title}</p>\n        {item.description && <p className=\"text-muted-foreground\">{item.description}</p>}\n      </div>\n      {item.action && (\n        <button\n          type=\"button\"\n          onClick={() => {\n            item.action!.onClick()\n            dismiss(item.id)\n          }}\n          className=\"h-7 shrink-0 rounded-lg bg-primary px-2.5 text-xs font-medium text-primary-foreground hover:bg-primary/90\"\n        >\n          {item.action.label}\n        </button>\n      )}\n      {item.tone !== 'loading' && (\n        <button\n          type=\"button\"\n          aria-label=\"Dismiss notification\"\n          onClick={() => dismiss(item.id)}\n          className=\"-mt-0.5 -mr-1 flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-3.5\"\n        >\n          <X />\n        </button>\n      )}\n    </li>\n  )\n}\n\nconst positions = {\n  'bottom-right': 'bottom-4 right-4 items-end',\n  'bottom-left': 'bottom-4 left-4 items-start',\n  'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 items-center',\n  'top-right': 'top-4 right-4 items-end',\n  'top-left': 'top-4 left-4 items-start',\n  'top-center': 'top-4 left-1/2 -translate-x-1/2 items-center',\n}\n\nexport function Toaster({\n  position = 'bottom-right',\n  max = 3,\n  className,\n}: {\n  position?: keyof typeof positions\n  max?: number\n  className?: string\n}) {\n  const items = useSyncExternalStore(subscribe, snapshot, serverSnapshot)\n  const [paused, setPaused] = useState(false)\n\n  useEffect(() => {\n    mounted++\n    return () => {\n      mounted--\n    }\n  }, [])\n\n  const visible = items.slice(-max)\n  const top = position.startsWith('top')\n\n  return (\n    <section\n      aria-label=\"Notifications\"\n      tabIndex={-1}\n      onPointerEnter={() => setPaused(true)}\n      onPointerLeave={() => setPaused(false)}\n      onFocus={() => setPaused(true)}\n      onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node) && setPaused(false)}\n      className={cn(\n        'pointer-events-none fixed z-[200] flex w-[calc(100%-2rem)] max-w-sm flex-col outline-none',\n        positions[position],\n        className,\n      )}\n    >\n      <ol className={cn('flex w-full gap-2', top ? 'flex-col' : 'flex-col-reverse')}>\n        {[...visible].reverse().map((item) => (\n          <ToastCard key={item.id} item={item} paused={paused} />\n        ))}\n      </ol>\n      {items.length > max && (\n        <p className=\"pointer-events-auto mt-2 self-end text-xs text-muted-foreground\">\n          +{items.length - max} more ·{' '}\n          <button type=\"button\" onClick={() => dismiss()} className=\"font-medium hover:underline\">\n            Clear all\n          </button>\n        </p>\n      )}\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/dashboard/toast.tsx"
    }
  ],
  "cssVars": {
    "light": {
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.12), 0 0 0 1px rgb(0 0 0 / 0.08)"
    },
    "dark": {
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.5), 0 0 0 1px #0e0e0e"
    }
  },
  "meta": {
    "scenario": "dashboard",
    "kind": "components"
  },
  "docs": "Mount <Toaster /> once near the root of your app, then call toast(\"Saved\") from anywhere.",
  "categories": [
    "dashboard",
    "feedback",
    "overlays"
  ],
  "type": "registry:component"
}