{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toast","type":"registry:ui","title":"Toasts","description":"Imperative toasts that stack in the corner, fan out on hover, update in place from loading to done, and blur away on dismiss.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":[],"files":[{"path":"registry/geturui/ui/toast.tsx","type":"registry:ui","target":"components/ui/toast.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToastTone = \"success\" | \"danger\" | \"info\" | \"warning\" | \"loading\";\n\nexport interface ToastData {\n  id: string;\n  tone?: ToastTone;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  action?: { label: string; onClick: () => void };\n  /** ms before auto-dismiss; 0 keeps it. Loading toasts never auto-dismiss. */\n  duration?: number;\n}\n\ntype Listener = (toasts: ToastData[]) => void;\nlet toasts: ToastData[] = [];\nconst listeners = new Set<Listener>();\nconst emit = () => listeners.forEach((l) => l(toasts));\n/** When each toast was last shown or updated — its auto-dismiss clock starts here. */\nconst stamps = new Map<string, number>();\nlet counter = 0;\n\n/** Push a toast from anywhere. Returns its id so you can update or dismiss it. */\nexport function toast(t: Omit<ToastData, \"id\"> & { id?: string }) {\n  const id = t.id ?? `t${++counter}`;\n  const exists = toasts.some((x) => x.id === id);\n  toasts = exists ? toasts.map((x) => (x.id === id ? { ...x, ...t, id } : x)) : [{ ...t, id }, ...toasts].slice(0, 6);\n  stamps.set(id, Date.now());\n  emit();\n  return id;\n}\ntoast.success = (title: React.ReactNode, description?: React.ReactNode) => toast({ tone: \"success\", title, description });\ntoast.error = (title: React.ReactNode, description?: React.ReactNode) => toast({ tone: \"danger\", title, description });\ntoast.info = (title: React.ReactNode, description?: React.ReactNode) => toast({ tone: \"info\", title, description });\ntoast.dismiss = (id: string) => {\n  toasts = toasts.filter((x) => x.id !== id);\n  stamps.delete(id);\n  emit();\n};\n\nexport function useToasts() {\n  const [list, setList] = React.useState<ToastData[]>(toasts);\n  React.useEffect(() => {\n    listeners.add(setList);\n    return () => {\n      listeners.delete(setList);\n    };\n  }, []);\n  return list;\n}\n\nconst icons = {\n  success: <CheckCircle2 className=\"size-[18px] text-success\" aria-hidden />,\n  danger: <XCircle className=\"size-[18px] text-danger\" aria-hidden />,\n  info: <Info className=\"size-[18px] text-info\" aria-hidden />,\n  warning: <AlertTriangle className=\"size-[18px] text-warning\" aria-hidden />,\n  loading: <Loader2 className=\"size-[18px] animate-spin text-ink-muted\" aria-hidden />,\n};\n\nexport function ToastCard({ t, onClose }: { t: ToastData; onClose?: () => void }) {\n  const tone = t.tone ?? \"info\";\n  return (\n    <div\n      role={tone === \"danger\" ? \"alert\" : \"status\"}\n      className=\"grid w-[360px] max-w-[calc(100vw-32px)] grid-cols-[auto_1fr_auto] items-start gap-3 rounded-[14px] bg-surface-raised px-4 py-3.5 text-ink shadow-md\"\n    >\n      <span className=\"mt-0.5\">{icons[tone]}</span>\n      <div className=\"min-w-0\">\n        <div className=\"text-sm font-semibold leading-5\">{t.title}</div>\n        {t.description && <div className=\"mt-0.5 text-[13px] leading-[18px] text-ink-muted\">{t.description}</div>}\n        {t.action && (\n          <button type=\"button\" onClick={t.action.onClick} className=\"mt-1.5 text-[13px] font-medium text-volt-ink hover:underline\">\n            {t.action.label}\n          </button>\n        )}\n      </div>\n      {onClose && (\n        <button type=\"button\" aria-label=\"Dismiss\" onClick={onClose} className=\"-mr-1 rounded-md p-1 text-ink-subtle hover:bg-surface-hover hover:text-ink\">\n          <X className=\"size-3.5\" aria-hidden />\n        </button>\n      )}\n    </div>\n  );\n}\n\n/** Stack of up to three toasts, bottom-right; hover fans them out. Mount once in your layout. */\nexport function Toaster({ position = \"bottom-right\" }: { position?: \"bottom-right\" | \"bottom-center\" }) {\n  const list = useToasts();\n  const [hover, setHover] = React.useState(false);\n\n  // Each toast keeps its own clock (pushing another doesn't extend it); hovering pauses all of them.\n  React.useEffect(() => {\n    if (hover) return;\n    const now = Date.now();\n    const timers = list\n      .filter((t) => t.tone !== \"loading\" && t.duration !== 0)\n      .map((t) => {\n        const elapsed = now - (stamps.get(t.id) ?? now);\n        return setTimeout(() => toast.dismiss(t.id), Math.max(0, (t.duration ?? 4200) - elapsed));\n      });\n    return () => timers.forEach(clearTimeout);\n  }, [list, hover]);\n\n  const visible = list.slice(0, 3);\n  return (\n    <div\n      aria-live=\"polite\"\n      onMouseEnter={() => setHover(true)}\n      onMouseLeave={() => {\n        // Restart the clocks so nothing vanishes the instant the pointer leaves.\n        const now = Date.now();\n        list.forEach((t) => stamps.set(t.id, now));\n        setHover(false);\n      }}\n      className={cn(\"fixed bottom-4 z-[100] grid\", position === \"bottom-right\" ? \"right-4\" : \"left-1/2 -translate-x-1/2\")}\n    >\n      <AnimatePresence initial={false}>\n        {visible.map((t, i) => (\n          <motion.div\n            key={t.id}\n            layout\n            initial={{ opacity: 0, y: 24, scale: 0.9, filter: \"blur(4px)\" }}\n            animate={{ opacity: 1, y: hover ? -i * 84 : -i * 14, scale: hover ? 1 : 1 - i * 0.05, filter: \"blur(0px)\" }}\n            exit={{ opacity: 0, scale: 0.92, filter: \"blur(4px)\", transition: { duration: 0.18 } }}\n            transition={{ type: \"spring\", stiffness: 380, damping: 30 }}\n            style={{ gridArea: \"1 / 1\", zIndex: 10 - i, transformOrigin: \"50% 100%\", alignSelf: \"end\" }}\n          >\n            <ToastCard t={t} onClose={() => toast.dismiss(t.id)} />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","toast","notification","sonner","feedback","loading","stack"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/toast"}}