{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "description": "Toast notifications with a pausable countdown and promise states.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/toast.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AlertTriangleIcon, CheckCircle2Icon, InfoIcon, XCircleIcon, XIcon } from \"lucide-react\";\n\nimport { enterOnce, gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tone = \"default\" | \"success\" | \"error\" | \"warning\" | \"info\";\ntype ToastAction = { label: string; onClick: () => void };\ntype ToastInput = { description?: React.ReactNode; tone?: Tone; duration?: number; action?: ToastAction; id?: string };\ntype ToastData = Required<Pick<ToastInput, \"tone\" | \"duration\">> & Omit<ToastInput, \"tone\" | \"duration\"> & { id: string; title: React.ReactNode };\n\n/* A tiny external store: `toast()` works anywhere (event handlers, mutations), the Toaster subscribes. */\nlet toasts: ToastData[] = [];\nconst listeners = new Set<() => void>();\nconst emit = () => listeners.forEach((l) => l());\nlet counter = 0;\n\nfunction show(title: React.ReactNode, input: ToastInput = {}) {\n  const id = input.id ?? `t${++counter}`;\n  const data: ToastData = { tone: \"default\", duration: 4500, ...input, id, title };\n  toasts = toasts.some((t) => t.id === id) ? toasts.map((t) => (t.id === id ? data : t)) : [...toasts, data];\n  emit();\n  return id;\n}\n\nfunction dismiss(id?: string) {\n  toasts = id ? toasts.filter((t) => t.id !== id) : [];\n  emit();\n}\n\n/**\n * `toast(\"Saved\")`, `toast.success(…)`, `toast.error(…)`, `toast.promise(p, { loading, success, error })`.\n * Each call returns the toast id, which `toast.dismiss(id)` accepts.\n */\nconst toast = Object.assign(show, {\n  success: (title: React.ReactNode, input?: ToastInput) => show(title, { ...input, tone: \"success\" }),\n  error: (title: React.ReactNode, input?: ToastInput) => show(title, { ...input, tone: \"error\" }),\n  warning: (title: React.ReactNode, input?: ToastInput) => show(title, { ...input, tone: \"warning\" }),\n  info: (title: React.ReactNode, input?: ToastInput) => show(title, { ...input, tone: \"info\" }),\n  dismiss,\n  promise: <T,>(promise: Promise<T>, messages: { loading: React.ReactNode; success: React.ReactNode | ((v: T) => React.ReactNode); error: React.ReactNode | ((e: unknown) => React.ReactNode) }) => {\n    const id = show(messages.loading, { duration: Infinity, tone: \"info\" });\n    promise.then(\n      (v) => show(typeof messages.success === \"function\" ? messages.success(v) : messages.success, { id, tone: \"success\" }),\n      (e) => show(typeof messages.error === \"function\" ? messages.error(e) : messages.error, { id, tone: \"error\" }),\n    );\n    return promise;\n  },\n});\n\nconst icons: Record<Tone, React.ComponentType<{ className?: string }> | null> = {\n  default: null,\n  success: CheckCircle2Icon,\n  error: XCircleIcon,\n  warning: AlertTriangleIcon,\n  info: InfoIcon,\n};\nconst toneColor: Record<Tone, string> = { default: \"text-foreground\", success: \"text-success\", error: \"text-destructive\", warning: \"text-warning\", info: \"text-primary\" };\n\ntype ToasterProps = { position?: \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\" | \"bottom-center\" | \"top-center\"; className?: string };\n\n/** Mount once near the root. Toasts rise in, show a hairline countdown that pauses on hover, and slide away. */\nfunction Toaster({ position = \"bottom-right\", className }: ToasterProps) {\n  const items = React.useSyncExternalStore(\n    (l) => {\n      listeners.add(l);\n      return () => listeners.delete(l);\n    },\n    () => toasts,\n    () => toasts,\n  );\n  const top = position.startsWith(\"top\");\n\n  return (\n    <section\n      aria-label=\"Notifications\"\n      data-slot=\"toaster\"\n      className={cn(\n        \"pointer-events-none fixed z-[100] flex w-full max-w-sm flex-col gap-2 p-4\",\n        top ? \"top-0 flex-col-reverse\" : \"bottom-0\",\n        position.endsWith(\"right\") && \"right-0\",\n        position.endsWith(\"left\") && \"left-0\",\n        position.endsWith(\"center\") && \"left-1/2 -translate-x-1/2\",\n        className,\n      )}\n    >\n      {items.map((t) => (\n        <ToastItem key={t.id} toast={t} fromTop={top} />\n      ))}\n    </section>\n  );\n}\n\nfunction ToastItem({ toast: t, fromTop }: { toast: ToastData; fromTop: boolean }) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const bar = React.useRef<HTMLSpanElement>(null);\n  const timer = React.useRef<gsap.core.Tween | null>(null);\n  const Icon = icons[t.tone];\n\n  const close = React.useCallback(() => {\n    const el = ref.current;\n    timer.current?.kill();\n    if (!el || !motionScale(el)) return dismiss(t.id);\n    gsap.to(el, { x: 32, opacity: 0, height: 0, marginTop: -8, paddingTop: 0, paddingBottom: 0, duration: 0.35, ease: \"cue\", onComplete: () => dismiss(t.id) });\n  }, [t.id]);\n\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const scale = motionScale(el);\n    if (scale) enterOnce(el, (s) => ({ y: (fromTop ? -16 : 16) * s, opacity: 0, scale: 1 - 0.04 * s }), { duration: 0.45, ease: \"cue-snap\" });\n  }, [fromTop]);\n\n  React.useEffect(() => {\n    timer.current?.kill();\n    if (!Number.isFinite(t.duration) || !bar.current) return;\n    timer.current = gsap.fromTo(bar.current, { scaleX: 1 }, { scaleX: 0, duration: t.duration / 1000, ease: \"none\", onComplete: close });\n    return () => {\n      timer.current?.kill();\n    };\n  }, [t.duration, t.title, close]);\n\n  return (\n    <div\n      ref={ref}\n      role={t.tone === \"error\" ? \"alert\" : \"status\"}\n      aria-live={t.tone === \"error\" ? \"assertive\" : \"polite\"}\n      data-slot=\"toast\"\n      data-tone={t.tone}\n      onPointerEnter={() => timer.current?.pause()}\n      onPointerLeave={() => timer.current?.resume()}\n      onFocus={() => timer.current?.pause()}\n      onBlur={() => timer.current?.resume()}\n      className=\"pointer-events-auto relative flex items-start gap-3 overflow-hidden rounded-xl bg-popover ring-1 ring-foreground/10 p-3.5 pe-9 text-sm text-popover-foreground shadow-overlay\"\n    >\n      {Icon && <Icon className={cn(\"mt-0.5 size-4 shrink-0\", toneColor[t.tone])} />}\n      <div className=\"grid flex-1 gap-0.5\">\n        <div className=\"font-medium\">{t.title}</div>\n        {t.description && <div className=\"text-muted-foreground\">{t.description}</div>}\n        {t.action && (\n          <button\n            type=\"button\"\n            onClick={() => {\n              t.action?.onClick();\n              close();\n            }}\n            className=\"mt-1.5 w-fit rounded-sm text-sm font-medium text-primary underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50\"\n          >\n            {t.action.label}\n          </button>\n        )}\n      </div>\n      <button\n        type=\"button\"\n        aria-label=\"Dismiss notification\"\n        onClick={close}\n        className=\"absolute top-3 right-3 rounded-sm text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50\"\n      >\n        <XIcon className=\"size-4\" />\n      </button>\n      {Number.isFinite(t.duration) && <span ref={bar} aria-hidden=\"true\" className={cn(\"absolute inset-x-0 bottom-0 h-px origin-left bg-current opacity-40\", toneColor[t.tone])} />}\n    </div>\n  );\n}\n\nexport { Toaster, toast };\nexport type { ToasterProps, ToastInput };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/toast",
  "categories": [
    "overlays"
  ],
  "type": "registry:ui"
}