{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner",
  "title": "Sonner",
  "description": "Toast component built with Sonner.",
  "dependencies": [
    "next-themes",
    "sonner",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://optics.agusmayol.com.ar/r/button.json",
    "https://optics.agusmayol.com.ar/r/spinner.json",
    "https://optics.agusmayol.com.ar/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/optics/sonner.jsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { toast as sonnerToast, Toaster as Sonner } from \"sonner\";\nimport { Button } from \"@/registry/optics/button\";\nimport {\n\tX,\n\tInfo,\n\tCheck,\n\tAlertCircle,\n\tAlertTriangle,\n\tLoader2,\n} from \"lucide-react\";\nimport { Spinner } from \"@/registry/optics/spinner\";\nimport { cn } from \"@/registry/optics/lib/utils\";\n\n// Toast type definitions\nconst TOAST_TYPES = {\n\tsuccess: {\n\t\ticon: Check,\n\t\tbgColor: \"bg-emerald-500/20\",\n\t\ticonColor: \"text-emerald-600\",\n\t},\n\tinfo: {\n\t\ticon: Info,\n\t\tbgColor: \"bg-blue-500/20\",\n\t\ticonColor: \"text-blue-600\",\n\t},\n\twarning: {\n\t\ticon: AlertTriangle,\n\t\tbgColor: \"bg-yellow-500/20\",\n\t\ticonColor: \"text-yellow-600\",\n\t},\n\terror: {\n\t\ticon: AlertCircle,\n\t\tbgColor: \"bg-red-500/20\",\n\t\ticonColor: \"text-red-600\",\n\t},\n\tpromise: {\n\t\ticon: Loader2,\n\t\tbgColor: \"\",\n\t\ticonColor: \"text-zinc-600\",\n\t\tuseSpinner: true,\n\t},\n};\n\nexport function Toaster({ className = \"\", ...props }) {\n\tconst { theme = \"system\" } = useTheme();\n\n\treturn (\n\t\t<Sonner\n\t\t\ttheme={theme}\n\t\t\tclassName=\"toaster group\"\n\t\t\tstyle={{\n\t\t\t\t\"--normal-bg\": \"var(--popover)\",\n\t\t\t\t\"--normal-text\": \"var(--popover-foreground)\",\n\t\t\t\t\"--normal-border\": \"var(--border)\",\n\t\t\t}}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\n// Store for managing toast instances and bounce effects\nconst toastInstances = new Map();\n// Store for managing promise instances to avoid multiple rejects\nconst promiseInstances = new Map();\n// Store for managing promise states globally\nconst promiseStates = new Map();\n\n/** I recommend abstracting the toast function\n *  so that you can call it without having to use toast.custom everytime. */\nexport function toast(toastConfig = {}) {\n\tconst {\n\t\ttype = \"info\",\n\t\ttitle,\n\t\tdescription,\n\t\tbutton,\n\t\tpromise,\n\t\tloading,\n\t\tsuccess,\n\t\terror,\n\t\ttoastId,\n\t\tduration = 5500, // 5.5 seconds default\n\t} = toastConfig;\n\n\t// Generate or use provided ID\n\tconst id = toastId || `${type}-${Date.now()}-${Math.random()}`;\n\n\t// Check if toast with same ID already exists\n\tconst existingToast = toastInstances.get(id);\n\tif (existingToast) {\n\t\t// Trigger bounce effect\n\t\texistingToast.bounce();\n\t\treturn existingToast.id;\n\t}\n\n\t// Handle promise toast with custom component\n\tif (type === \"promise\" && promise) {\n\t\t// Initialize promise state if it doesn't exist\n\t\tif (!promiseStates.has(id)) {\n\t\t\tpromiseStates.set(id, {\n\t\t\t\tstate: \"loading\",\n\t\t\t\tresult: null,\n\t\t\t\tisTransitioning: false,\n\t\t\t});\n\n\t\t\t// Execute promise only once\n\t\t\tpromise\n\t\t\t\t.then((data) => {\n\t\t\t\t\tconst currentState = promiseStates.get(id);\n\t\t\t\t\tif (currentState) {\n\t\t\t\t\t\tcurrentState.isTransitioning = true;\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tcurrentState.state = \"success\";\n\t\t\t\t\t\t\tcurrentState.result = data;\n\t\t\t\t\t\t\tcurrentState.isTransitioning = false;\n\t\t\t\t\t\t\t// Notify all components with this ID\n\t\t\t\t\t\t\twindow.dispatchEvent(\n\t\t\t\t\t\t\t\tnew CustomEvent(\"promise-state-update\", {\n\t\t\t\t\t\t\t\t\tdetail: { toastId: id, state: \"success\", result: data },\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}, 150);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconst currentState = promiseStates.get(id);\n\t\t\t\t\tif (currentState) {\n\t\t\t\t\t\tcurrentState.isTransitioning = true;\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tcurrentState.state = \"error\";\n\t\t\t\t\t\t\tcurrentState.result = err;\n\t\t\t\t\t\t\tcurrentState.isTransitioning = false;\n\t\t\t\t\t\t\t// Notify all components with this ID\n\t\t\t\t\t\t\twindow.dispatchEvent(\n\t\t\t\t\t\t\t\tnew CustomEvent(\"promise-state-update\", {\n\t\t\t\t\t\t\t\t\tdetail: { toastId: id, state: \"error\", result: err },\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}, 150);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t}\n\n\t\tconst promiseToastId = sonnerToast.custom(\n\t\t\t(sonnerId) => (\n\t\t\t\t<PromiseToast\n\t\t\t\t\tid={sonnerId}\n\t\t\t\t\ttoastId={id}\n\t\t\t\t\tloading={loading || \"Loading...\"}\n\t\t\t\t\tsuccess={success || \"Success!\"}\n\t\t\t\t\terror={error || \"Something went wrong!\"}\n\t\t\t\t\tbutton={button}\n\t\t\t\t\tduration={duration}\n\t\t\t\t/>\n\t\t\t),\n\t\t\t{\n\t\t\t\tduration: duration,\n\t\t\t},\n\t\t);\n\n\t\t// Store toast instance\n\t\ttoastInstances.set(id, {\n\t\t\tid: promiseToastId,\n\t\t\tbounce: () => {\n\t\t\t\t// Trigger bounce effect by updating the bounce state\n\t\t\t\t// This will be handled by the component's bounce state\n\t\t\t\twindow.dispatchEvent(\n\t\t\t\t\tnew CustomEvent(\"toast-bounce\", {\n\t\t\t\t\t\tdetail: { toastId: id },\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t},\n\t\t});\n\n\t\t// Auto-remove from store after duration\n\t\tsetTimeout(() => {\n\t\t\ttoastInstances.delete(id);\n\t\t\tpromiseInstances.delete(id);\n\t\t\tpromiseStates.delete(id);\n\t\t}, duration);\n\n\t\treturn promiseToastId;\n\t}\n\n\t// Handle regular toasts\n\tconst regularToastId = sonnerToast.custom(\n\t\t(sonnerId) => (\n\t\t\t<Toast\n\t\t\t\tid={sonnerId}\n\t\t\t\ttoastId={id}\n\t\t\t\ttype={type}\n\t\t\t\ttitle={title}\n\t\t\t\tdescription={description}\n\t\t\t\tbutton={\n\t\t\t\t\tbutton\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tlabel: button.label,\n\t\t\t\t\t\t\t\tonClick: () => button.onClick(),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: undefined\n\t\t\t\t}\n\t\t\t\tduration={duration}\n\t\t\t/>\n\t\t),\n\t\t{\n\t\t\tduration: duration,\n\t\t},\n\t);\n\n\t// Store toast instance\n\ttoastInstances.set(id, {\n\t\tid: regularToastId,\n\t\tbounce: () => {\n\t\t\t// Trigger bounce effect by updating the bounce state\n\t\t\t// This will be handled by the component's bounce state\n\t\t\twindow.dispatchEvent(\n\t\t\t\tnew CustomEvent(\"toast-bounce\", {\n\t\t\t\t\tdetail: { toastId: id },\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t});\n\n\t// Auto-remove from store after duration\n\tsetTimeout(() => {\n\t\ttoastInstances.delete(id);\n\t\tpromiseInstances.delete(id);\n\t\tpromiseStates.delete(id);\n\t}, duration);\n\n\treturn regularToastId;\n}\n\n/** Promise toast component that handles promise state */\nfunction PromiseToast(props) {\n\tconst { id, toastId, loading, success, error, button, duration } = props;\n\tconst [state, setState] = React.useState(\"loading\");\n\tconst [result, setResult] = React.useState(null);\n\tconst [isTransitioning, setIsTransitioning] = React.useState(false);\n\tconst [isBouncing, setIsBouncing] = React.useState(false);\n\n\t// Initialize state from global store\n\tReact.useEffect(() => {\n\t\tconst globalState = promiseStates.get(toastId);\n\t\tif (globalState) {\n\t\t\tsetState(globalState.state);\n\t\t\tsetResult(globalState.result);\n\t\t\tsetIsTransitioning(globalState.isTransitioning);\n\t\t}\n\t}, [toastId]);\n\n\t// Listen for bounce events\n\tReact.useEffect(() => {\n\t\tconst handleBounce = (event) => {\n\t\t\tif (event.detail.toastId === toastId) {\n\t\t\t\tsetIsBouncing(true);\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tsetIsBouncing(false);\n\t\t\t\t}, 400); // Zoom animation duration\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"toast-bounce\", handleBounce);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"toast-bounce\", handleBounce);\n\t\t};\n\t}, [toastId]);\n\n\t// Listen for promise state updates\n\tReact.useEffect(() => {\n\t\tconst handleStateUpdate = (event) => {\n\t\t\tif (event.detail.toastId === toastId) {\n\t\t\t\tsetState(event.detail.state);\n\t\t\t\tsetResult(event.detail.result);\n\t\t\t\tsetIsTransitioning(false);\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"promise-state-update\", handleStateUpdate);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"promise-state-update\", handleStateUpdate);\n\t\t};\n\t}, [toastId]);\n\n\tconst getToastConfig = () => {\n\t\tswitch (state) {\n\t\t\tcase \"success\":\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"success\",\n\t\t\t\t\ttitle: success,\n\t\t\t\t\tdescription:\n\t\t\t\t\t\ttypeof result === \"string\"\n\t\t\t\t\t\t\t? result\n\t\t\t\t\t\t\t: \"Operation completed successfully!\",\n\t\t\t\t};\n\t\t\tcase \"error\":\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\ttitle: error,\n\t\t\t\t\tdescription: result?.message || \"Something went wrong!\",\n\t\t\t\t};\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"promise\",\n\t\t\t\t\ttitle: loading,\n\t\t\t\t\tdescription: \"Please wait while we process your request.\",\n\t\t\t\t};\n\t\t}\n\t};\n\n\tconst toastConfig = getToastConfig();\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"transition-all duration-300 ease-in-out\",\n\t\t\t\tisTransitioning && \"opacity-75 scale-95\",\n\t\t\t\tisBouncing && \"animate-zoom-subtle\",\n\t\t\t)}\n\t\t>\n\t\t\t<Toast {...toastConfig} id={id} button={button} />\n\t\t</div>\n\t);\n}\n\n/** A fully custom toast that still maintains the animations and interactions. */\nfunction Toast(props) {\n\tconst {\n\t\ttitle,\n\t\tdescription,\n\t\tbutton,\n\t\tid,\n\t\ttoastId,\n\t\ttype = \"info\",\n\t\tduration,\n\t} = props;\n\tconst [isBouncing, setIsBouncing] = React.useState(false);\n\tconst toastType = TOAST_TYPES[type] || TOAST_TYPES.info;\n\tconst IconComponent = toastType.icon;\n\n\t// Listen for bounce events\n\tReact.useEffect(() => {\n\t\tconst handleBounce = (event) => {\n\t\t\tif (event.detail.toastId === toastId) {\n\t\t\t\tsetIsBouncing(true);\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tsetIsBouncing(false);\n\t\t\t\t}, 400); // Zoom animation duration\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"toast-bounce\", handleBounce);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"toast-bounce\", handleBounce);\n\t\t};\n\t}, [toastId]);\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"bg-muted z-50 grid w-full gap-4 rounded-2xl border p-1 shadow-lg sm:min-w-md sm:max-w-md select-none transition-all duration-300\",\n\t\t\t\tisBouncing && \"animate-zoom-subtle\",\n\t\t\t\t!description && \"sm:min-w-sm sm:max-w-sm\",\n\t\t\t)}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"bg-background grid w-full gap-4 rounded-xl p-6 px-6 shadow-lg bg-[repeating-linear-gradient(45deg,var(--background),var(--card)_3px,var(--muted)_3px,var(--muted)_6px)] relative\",\n\t\t\t\t\t!description && \"row-span-2\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<div className=\"absolute inset-0 bg-[linear-gradient(135deg,var(--background)_45%,transparent_100%)] rounded-lg\" />\n\t\t\t\t{/* Header */}\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex flex-col gap-2 text-center sm:text-left relative\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{/* Icon */}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"w-full flex items-start justify-between -mt-2\",\n\t\t\t\t\t\t\t!description && \"-mb-2 -mt-3\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex items-center justify-center gap-2.5 -ml-2\",\n\t\t\t\t\t\t\t\t!description && \"-mb-1\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"p-1 rounded-full squircle-none flex items-center justify-center shadow-md transition-all duration-300 ease-in-out\",\n\t\t\t\t\t\t\t\t\ttoastType.bgColor,\n\t\t\t\t\t\t\t\t\ttoastType.useSpinner && \"shadow-none\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{toastType.useSpinner ? (\n\t\t\t\t\t\t\t\t\t<Spinner\n\t\t\t\t\t\t\t\t\t\tsize=\"size-4\"\n\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\"transition-colors duration-300\",\n\t\t\t\t\t\t\t\t\t\t\ttoastType.iconColor,\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<IconComponent\n\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\"size-4 transition-colors duration-300\",\n\t\t\t\t\t\t\t\t\t\t\ttoastType.iconColor,\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<h2\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"text-base font-medium transition-all duration-300\",\n\t\t\t\t\t\t\t\t\ttoastType.iconColor,\n\t\t\t\t\t\t\t\t\t!description && \"text-sm !text-foreground\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t{/* Description */}\n\t\t\t\t\t{description && (\n\t\t\t\t\t\t<p className=\"text-muted-foreground text-xs text-pretty transition-all duration-300\">\n\t\t\t\t\t\t\t{description}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\n\t\t\t\t{/* Footer */}\n\t\t\t\t{button && !toastType.useSpinner && (\n\t\t\t\t\t<div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end relative\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"raised\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tclassName=\"-mr-4 -mb-4\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tbutton.onClick();\n\t\t\t\t\t\t\t\tsonnerToast.dismiss(id);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{button.label}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nToaster.displayName = \"Toaster\";\nPromiseToast.displayName = \"PromiseToast\";\nToast.displayName = \"Toast\";\n",
      "type": "registry:component",
      "target": "components/optics/sonner.jsx"
    }
  ],
  "type": "registry:component"
}