{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toast","type":"registry:ui","title":"Toast","description":"A notification system: call toast() from anywhere to queue one, mount <Toaster /> once to render the stack — success, error, info and loading variants, an optional action, and accessible live-region announcements","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["overlays"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/toast/Toast.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef, useState } from \"react\";\nimport type { CSSProperties } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { dismissToast, pauseToast, resumeToast, settleToastAction } from \"./store\";\nimport type { ToastItem } from \"./store\";\nimport \"./toast.css\";\n\nexport interface ToastProps {\n\t/** The toast to render. */\n\titem: ToastItem;\n\t/** Additional classes for the toast panel. */\n\tclassName?: string;\n}\n\nconst VARIANT_ICON_CLASSES: Record<ToastItem[\"variant\"], string> = {\n\tsuccess: \"ft-toast-icon--success\",\n\terror: \"ft-toast-icon--error\",\n\tinfo: \"ft-toast-icon--info\",\n\tloading: \"\",\n};\n\n/**\n * One toast panel. Renders what the store hands it and wires the pause /\n * resume protocol; the entrance and exit animation belong to `<Toaster>`,\n * whose per-item presence clock attaches to this component's forwarded ref —\n * the React spelling of the source's two separate `in:`/`out:` directives on\n * this same root element (a toast has no `open` boolean: its existence IS its\n * open state, so the mount clock has to live with whatever renders the list).\n */\nexport const Toast = forwardRef<HTMLDivElement, ToastProps>(function Toast(\n\t{ item, className },\n\tforwardedRef\n) {\n\t// Pause while *either* the pointer or focus is on the toast, and only\n\t// resume once *both* have left. Two independent booleans instead of one\n\t// shared flag: hovering with the mouse while also tabbing through the\n\t// toast's buttons (or the reverse) must not resume the countdown just\n\t// because one of the two let go first — `pauseToast`/`resumeToast` are\n\t// idempotent, so re-pausing while still engaged is a harmless no-op.\n\t// Plain refs, not state: nothing in the markup reads them, exactly as the\n\t// source keeps them non-reactive.\n\tconst hovering = useRef(false);\n\tconst focusedWithin = useRef(false);\n\t// Mirrors the pause state for the progress bar's `data-paused` — the only\n\t// thing in the markup that reads it, hence the one piece of state.\n\tconst [paused, setPaused] = useState(false);\n\n\tfunction syncTimer() {\n\t\tconst isPaused = hovering.current || focusedWithin.current;\n\t\tsetPaused(isPaused);\n\t\tif (isPaused) {\n\t\t\tpauseToast(item.id);\n\t\t} else {\n\t\t\tresumeToast(item.id);\n\t\t}\n\t}\n\n\tfunction handlePointerEnter() {\n\t\thovering.current = true;\n\t\tsyncTimer();\n\t}\n\n\tfunction handlePointerLeave() {\n\t\thovering.current = false;\n\t\tsyncTimer();\n\t}\n\n\tfunction handleFocusIn() {\n\t\tfocusedWithin.current = true;\n\t\tsyncTimer();\n\t}\n\n\tfunction handleFocusOut() {\n\t\tfocusedWithin.current = false;\n\t\tsyncTimer();\n\t}\n\n\tfunction handleAction() {\n\t\t// Settle first: an `onClick` that dismisses the toast must not turn\n\t\t// the close reason into \"dismiss\".\n\t\tsettleToastAction(item.id);\n\t\titem.action?.onClick();\n\t}\n\n\tfunction handleDismiss() {\n\t\tdismissToast(item.id);\n\t}\n\n\tconst classes = cn(\n\t\t\"ft-toast bg-popover text-popover-foreground border-border flex w-[300px] items-center gap-3 rounded-xl border p-3 shadow-[0_12px_32px_rgba(0,0,0,.5)]\",\n\t\titem.variant === \"error\" && \"border-destructive/30\",\n\t\titem.progress && \"relative overflow-hidden\",\n\t\tclassName\n\t);\n\n\treturn (\n\t\t<div\n\t\t\tref={forwardedRef}\n\t\t\tclassName={classes}\n\t\t\tdata-state=\"open\"\n\t\t\tdata-variant={item.variant}\n\t\t\tonPointerEnter={handlePointerEnter}\n\t\t\tonPointerLeave={handlePointerLeave}\n\t\t\t// React delegates `onFocus`/`onBlur` on the bubbling\n\t\t\t// `focusin`/`focusout` pair — the same two events the source\n\t\t\t// listens for on this element.\n\t\t\tonFocus={handleFocusIn}\n\t\t\tonBlur={handleFocusOut}\n\t\t>\n\t\t\t<span\n\t\t\t\tclassName={cn(\"ft-toast-icon flex-none text-[14px]\", VARIANT_ICON_CLASSES[item.variant])}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{item.variant === \"loading\" ? (\n\t\t\t\t\t<span className=\"ft-toast-spinner\"></span>\n\t\t\t\t) : item.variant === \"success\" ? (\n\t\t\t\t\t\"✓\"\n\t\t\t\t) : item.variant === \"error\" ? (\n\t\t\t\t\t\"✕\"\n\t\t\t\t) : (\n\t\t\t\t\t\"ℹ\"\n\t\t\t\t)}\n\t\t\t</span>\n\n\t\t\t<div className=\"flex flex-1 flex-col gap-0.5\">\n\t\t\t\t<span className=\"text-[13px] font-medium\">{item.title}</span>\n\t\t\t\t{item.description ? (\n\t\t\t\t\t<span className=\"text-muted-foreground text-[11px]\">{item.description}</span>\n\t\t\t\t) : null}\n\t\t\t</div>\n\n\t\t\t{item.action ? (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"ft-toast-action shrink-0 text-[12px] font-medium hover:underline\"\n\t\t\t\t\tonClick={handleAction}\n\t\t\t\t>\n\t\t\t\t\t{item.action.label}\n\t\t\t\t</button>\n\t\t\t) : null}\n\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"text-muted-foreground hover:text-foreground shrink-0 text-[12px] transition-colors\"\n\t\t\t\taria-label=\"Dismiss\"\n\t\t\t\tonClick={handleDismiss}\n\t\t\t>\n\t\t\t\t✕\n\t\t\t</button>\n\n\t\t\t{item.progress ? (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"ft-toast-progress\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tdata-paused={paused ? \"true\" : \"false\"}\n\t\t\t\t\tstyle={{ \"--toast-duration\": `${item.duration}ms` } as CSSProperties}\n\t\t\t\t></div>\n\t\t\t) : null}\n\t\t</div>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/toast/Toast.tsx"},{"path":"react/src/components/toast/Toaster.tsx","content":"\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useReducer, useRef, useState } from \"react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { Portal } from \"../../internals/Portal\";\nimport { prefersReducedMotion } from \"../../internals/motion/anchored\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { DURATIONS, JS_EASINGS } from \"../../internals/motion/tokens\";\nimport { preset } from \"../../internals/motion/transitions\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { clearAllToastTimers, rearmToastTimers, toastStore } from \"./store\";\nimport type { ToastItem } from \"./store\";\nimport { Toast } from \"./Toast\";\n\nexport type ToasterPosition =\n\t| \"top-left\"\n\t| \"top-center\"\n\t| \"top-right\"\n\t| \"bottom-left\"\n\t| \"bottom-center\"\n\t| \"bottom-right\";\n\nexport interface ToasterProps {\n\t/** Corner (or edge-center) the stack anchors to. Defaults to `\"bottom-right\"`. */\n\tposition?: ToasterPosition;\n\t/** Additional classes for the viewport that stacks the toasts. */\n\tclassName?: string;\n\t/**\n\t * Plays the `success`/`error` cue through the sound controller when a\n\t * toast of that variant appears. Off by default; only audible once the\n\t * user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n// Ids already sounded, kept at *module* scope — unlike `announcedIdsRef`\n// below, which is per-instance and thrown away the moment a `<Toaster>`\n// unmounts, while `toastStore.items` (a module-level singleton) survives\n// that unmount untouched. Without this, a remount while a success/error\n// toast is still on screen replays its cue: the announce effect reruns on\n// the fresh instance with an empty `announcedIds`, so a toast that has\n// already sounded once looks unseen to the sound check too. The live\n// region legitimately wants a fresh instance to re-announce (a screen\n// reader is not attached to the old, destroyed region), so this is\n// checked separately rather than folded into `announcedIds`. Pruned to\n// the current stack on every effect run, same bounding `announcedIds`\n// already does, so this never grows for the life of the page — ids are\n// monotonic and never reused (see `store.ts`'s `toast()`).\nconst soundedIds = new Set<string>();\n\nconst POSITION_CLASSES: Record<ToasterPosition, string> = {\n\t\"top-left\": \"top-4 left-4 items-start\",\n\t\"top-center\": \"top-4 left-1/2 -translate-x-1/2 items-center\",\n\t\"top-right\": \"top-4 right-4 items-end\",\n\t\"bottom-left\": \"bottom-4 left-4 items-start\",\n\t\"bottom-center\": \"bottom-4 left-1/2 -translate-x-1/2 items-center\",\n\t\"bottom-right\": \"bottom-4 right-4 items-end\",\n};\n\n// Entrance and exit are two SEPARATE param sets fed to ONE `preset(\"fade-up\")`\n// leg per toast, resolved by direction at the instant the leg starts — the\n// React spelling of the source's split `in:`/`out:` directives: the exit is\n// its own, shorter, shallower gesture rather than the entrance played\n// backwards. The source's split deliberately traded away reversal smoothing\n// (a toast interrupted mid-exit restarts instead of reversing), and that\n// trade costs nothing here either: `toast()` never reuses an id, so the store\n// can never re-add a toast that is currently leaving.\nconst slide = preset(\"fade-up\");\n\ninterface ToastSlotProps {\n\titem: ToastItem;\n\t/** Still in the store. Flipping false starts the exit; the slot stays\n\t *  mounted until it settles, then reports back through `onExited`. */\n\tpresent: boolean;\n\t/** Whether an entrance should play — false only for toasts already queued\n\t *  when the viewport first renders, mirroring the source rule that a local\n\t *  transition never plays on the initial render of the block that owns it. */\n\tappear: boolean;\n\tonExited: (id: string) => void;\n}\n\n/**\n * One toast's seat in the viewport: the per-item mount clock the source's\n * keyed `{#each}` provided for free. `presence.mounted` holds the dismissed\n * toast in the DOM — inert — for the length of its exit, and only then does\n * `onExited` let the viewport forget it.\n */\nfunction ToastSlot({ item, present, appear, onExited }: ToastSlotProps) {\n\tconst presence = usePresence(present, {\n\t\tappear,\n\t\tonExitEnd: () => onExited(item.id),\n\t});\n\n\t// Params are read at leg start, never at render time — which is also when\n\t// the source resolved `prefersReducedMotion()`, on each directive run,\n\t// never in a render path.\n\tconst toastRef = presence.register(slide, (entering) =>\n\t\tentering\n\t\t\t? {\n\t\t\t\t\tduration: prefersReducedMotion() ? 0 : DURATIONS.base,\n\t\t\t\t\tdistance: 8,\n\t\t\t\t\teasing: JS_EASINGS.out,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\tduration: prefersReducedMotion() ? 0 : DURATIONS.exit,\n\t\t\t\t\tdistance: 4,\n\t\t\t\t\teasing: JS_EASINGS.in,\n\t\t\t\t}\n\t);\n\n\t// A slot whose very first render is already un-present has no exit to run —\n\t// `usePresence(false)` starts at `mounted === false`, so its clock never\n\t// reaches `onExitEnd` and the viewport would hold the entry in its rendered\n\t// list forever. Draining it here is the one path back out.\n\tconst mounted = presence.mounted;\n\tuseEffect(() => {\n\t\tif (!present && !mounted) onExited(item.id);\n\t\t// Mount-time only: every other route out of the list goes through the\n\t\t// exit's `onExitEnd`.\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, []);\n\n\tif (!presence.mounted) return null;\n\n\treturn (\n\t\t<div className=\"pointer-events-auto\">\n\t\t\t<Toast item={item} ref={toastRef} />\n\t\t</div>\n\t);\n}\n\ninterface ToastSlotsProps {\n\titems: readonly ToastItem[];\n\tstoreIds: ReadonlySet<string>;\n\tonExited: (id: string) => void;\n}\n\n/**\n * The slot list, as its OWN component so it lives inside `<Portal>`.\n *\n * That placement is the whole point: `<Portal>` resolves its container in a\n * layout effect and renders null until it has one, so the Toaster's own first\n * commit produces no slots at all. A flag raised from the Toaster's mount\n * effect is therefore already set by the time any slot first renders — the\n * ids that must paint at rest have to be captured from the same render pass\n * that creates them, which is this component's first.\n *\n * Those ids are the source's rule that a local transition never plays on the\n * initial render of the block that owns it: a toast raised before any viewport\n * existed, and one inherited by a viewport that replaced another, are simply\n * there. Everything raised afterwards gets a real entrance.\n */\nfunction ToastSlots({ items, storeIds, onExited }: ToastSlotsProps) {\n\tconst initialIdsRef = useRef<ReadonlySet<string> | null>(null);\n\tif (initialIdsRef.current === null) {\n\t\tinitialIdsRef.current = new Set(items.map((item) => item.id));\n\t}\n\tconst initialIds = initialIdsRef.current;\n\n\treturn (\n\t\t<>\n\t\t\t{items.map((item) => (\n\t\t\t\t<ToastSlot\n\t\t\t\t\tkey={item.id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tpresent={storeIds.has(item.id)}\n\t\t\t\t\tappear={!initialIds.has(item.id)}\n\t\t\t\t\tonExited={onExited}\n\t\t\t\t/>\n\t\t\t))}\n\t\t</>\n\t);\n}\n\nfunction announcementText(item: ToastItem): string {\n\treturn item.description ? `${item.title}. ${item.description}` : item.title;\n}\n\nconst bumpReducer = (n: number) => n + 1;\n\n/**\n * The viewport: portals to `<body>`, renders the store's stack in order, and\n * owns the two live regions plus the timer hand-off protocol (`rearm` on\n * mount, `clear` on unmount).\n */\nexport const Toaster = forwardRef<HTMLDivElement, ToasterProps>(function Toaster(\n\t{ position = \"bottom-right\", className, sound = false },\n\tforwardedRef\n) {\n\tconst playCue = useSoundCue(sound);\n\n\tconst storeItems = useSyncExternalStore(\n\t\ttoastStore.subscribe,\n\t\t() => toastStore.items,\n\t\t// `toast()` is a no-op outside the browser, so the server snapshot is\n\t\t// the never-reassigned initial empty stack — referentially stable.\n\t\t() => toastStore.items\n\t);\n\n\t// The rendered list lags the store by each dismissed toast's exit: an item\n\t// the store forgot stays in `rendered` — at its old position, so siblings\n\t// keep their DOM identity — until its slot reports the exit settled.\n\t// `renderedRef` is a render-time memo of the previous merge; recomputing it\n\t// is idempotent for the same inputs, so a repeated render is safe.\n\tconst renderedRef = useRef<readonly ToastItem[]>([]);\n\tconst exitedRef = useRef(new Set<string>());\n\tconst [, bumpExited] = useReducer(bumpReducer, 0);\n\n\tconst storeIds = new Set(storeItems.map((item) => item.id));\n\tconst prev = renderedRef.current;\n\tconst prevIds = new Set(prev.map((item) => item.id));\n\tconst rendered: ToastItem[] = [];\n\tfor (const item of prev) {\n\t\t// Still in the store, or gone from it but not done leaving the screen.\n\t\tif (storeIds.has(item.id) || !exitedRef.current.has(item.id)) rendered.push(item);\n\t}\n\tfor (const item of storeItems) {\n\t\t// Append-ordered, exactly like the store itself.\n\t\tif (!prevIds.has(item.id)) rendered.push(item);\n\t}\n\trenderedRef.current = rendered;\n\n\tconst handleExited = useCallback((id: string) => {\n\t\texitedRef.current.add(id);\n\t\tbumpExited();\n\t}, []);\n\n\t// A settled exit's id has been dropped from `rendered` by the merge above,\n\t// so after each commit the set can forget everything it holds — pruned here\n\t// rather than during render, so a re-render between the two stays pure.\n\tuseEffect(() => {\n\t\tconst live = new Set(renderedRef.current.map((item) => item.id));\n\t\tfor (const id of [...exitedRef.current]) {\n\t\t\tif (!live.has(id)) exitedRef.current.delete(id);\n\t\t}\n\t});\n\n\t// Two regions, mounted empty and never re-created — only their *content*\n\t// changes. A live region created at the moment of the announcement is not\n\t// reliably picked up by screen readers; existing from mount and being\n\t// updated in place is what makes the announcement land. Two of them, not\n\t// one, because politeness is fixed per region: `polite` must never be\n\t// upgraded to `assertive` (or the reverse) by mutating the same element's\n\t// aria-live attribute, which most screen readers do not pick up reliably\n\t// after the region already exists.\n\tconst [politeAnnouncement, setPoliteAnnouncement] = useState(\"\");\n\tconst [assertiveAnnouncement, setAssertiveAnnouncement] = useState(\"\");\n\n\t// Ids already announced, refreshed to exactly the current stack on every\n\t// run — bounded to \"currently visible\", rather than growing forever for\n\t// the life of the page.\n\tconst announcedIdsRef = useRef(new Set<string>());\n\n\tuseEffect(() => {\n\t\tconst current = storeItems;\n\n\t\t// Forget any sounded id that has left the stack (dismissed, auto-\n\t\t// dismissed, or evicted past `MAX_VISIBLE`) — bounds `soundedIds` to\n\t\t// \"currently visible\", same as `announcedIds` below, instead of\n\t\t// growing forever for the life of the page.\n\t\tconst currentIds = new Set(current.map((item) => item.id));\n\t\tfor (const id of soundedIds) {\n\t\t\tif (!currentIds.has(id)) soundedIds.delete(id);\n\t\t}\n\n\t\tfor (const item of current) {\n\t\t\t// Only success/error toasts get a cue — info and loading stay silent.\n\t\t\t// `sound` lives on `<Toaster>`, never on `toast()`'s own options: the\n\t\t\t// caller who raises the toast has no per-call say over whether it\n\t\t\t// plays. Gated on the module-level `soundedIds`, not the\n\t\t\t// per-instance `announcedIds`, so a remount can't replay a cue for a\n\t\t\t// toast that already got one.\n\t\t\t// The ID is recorded whether or not sound is currently opted in:\n\t\t\t// the cue marks a toast APPEARING, so flipping `sound` on later\n\t\t\t// must not retroactively replay outcomes already on screen —\n\t\t\t// `playCue` is the no-op half of that, reading `sound` live.\n\t\t\tif (item.variant === \"success\" || item.variant === \"error\") {\n\t\t\t\tif (!soundedIds.has(item.id)) playCue(item.variant);\n\t\t\t\tsoundedIds.add(item.id);\n\t\t\t}\n\t\t\tif (announcedIdsRef.current.has(item.id)) continue;\n\t\t\tif (item.variant === \"error\") {\n\t\t\t\tsetAssertiveAnnouncement(announcementText(item));\n\t\t\t} else {\n\t\t\t\tsetPoliteAnnouncement(announcementText(item));\n\t\t\t}\n\t\t}\n\t\tannouncedIdsRef.current = new Set(current.map((item) => item.id));\n\t}, [storeItems, playCue]);\n\n\t// The two halves of the timer hand-off. Re-arming on mount is what stops a\n\t// toast inherited from a previous viewport from being stuck on screen\n\t// forever with nothing left to dismiss it; the cleanup stops the live\n\t// timers, not the toasts or their deadlines — see `clearAllToastTimers`'s\n\t// own doc comment for why unmounting isn't treated as pausing. An effect,\n\t// so neither ever runs during SSR.\n\tuseEffect(() => {\n\t\trearmToastTimers();\n\t\treturn () => {\n\t\t\tclearAllToastTimers();\n\t\t};\n\t}, []);\n\n\t// `<Portal>` stays ABOVE any mount gating: it resolves its container in a\n\t// layout effect and renders null until it has one, and each slot's\n\t// presence gate lives INSIDE it.\n\treturn (\n\t\t<Portal>\n\t\t\t<div ref={forwardedRef} className={cn(\"ft-toaster contents\", className)}>\n\t\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n\t\t\t\t\t{politeAnnouncement}\n\t\t\t\t</div>\n\t\t\t\t<div aria-live=\"assertive\" aria-atomic=\"true\" className=\"sr-only\">\n\t\t\t\t\t{assertiveAnnouncement}\n\t\t\t\t</div>\n\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"ft-toaster-viewport pointer-events-none fixed z-50 flex flex-col gap-2\",\n\t\t\t\t\t\tPOSITION_CLASSES[position]\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<ToastSlots items={rendered} storeIds={storeIds} onExited={handleExited} />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Portal>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/toast/Toaster.tsx"},{"path":"react/src/components/toast/index.ts","content":"\"use client\";\n\nexport { Toast } from \"./Toast\";\nexport type { ToastProps } from \"./Toast\";\nexport { Toaster } from \"./Toaster\";\nexport type { ToasterProps, ToasterPosition } from \"./Toaster\";\nexport { toast, dismissToast } from \"./store\";\nexport type {\n\tToastVariant,\n\tToastAction,\n\tToastCloseReason,\n\tToastOptions,\n\tToastItem,\n} from \"./store\";\n","type":"registry:ui","target":"@ui/fancy/components/toast/index.ts"},{"path":"react/src/components/toast/store.ts","content":"\"use client\";\n\n/**\n * Toast store — a module-level singleton, unlike every other reactive helper\n * in this library (which is a per-instance factory a component calls).\n * `toast()` has to be callable from anywhere — a submit handler, a promise\n * rejection, code with no component instance in scope at all — so there can\n * only be one stack per app, shared by whichever `<Toaster>` is mounted.\n *\n * This file owns the data and the timers. `Toast.tsx` and `Toaster.tsx`\n * only render what this exposes; they never touch `items` or the timer map\n * directly.\n *\n * Port note: the source's reactive module state becomes a plain module\n * variable plus a listener set — `toastStore.subscribe` is port-added so a\n * `<Toaster>` can read the stack through `useSyncExternalStore`. The\n * consumer-facing surface (`toast`, `dismissToast`, `toastStore.items`) is\n * unchanged.\n */\n\nexport type ToastVariant = \"success\" | \"error\" | \"info\" | \"loading\";\n\nexport interface ToastAction {\n\t/** Label for the toast's single optional action button, e.g. \"Retry\". */\n\tlabel: string;\n\t/** Called on activation. Does not dismiss the toast on its own — call `dismissToast` from inside if the action should also close it. */\n\tonClick: () => void;\n}\n\n/**\n * Why a toast closed, as handed to `onClose`:\n * - `\"timeout\"` — its unpaused on-screen time ran out.\n * - `\"dismiss\"` — the close button, or a `dismissToast` call.\n * - `\"action\"` — its action button was pressed (the toast itself stays until dismissed).\n * - `\"evicted\"` — pushed out by a newer toast once the stack was full.\n */\nexport type ToastCloseReason = \"timeout\" | \"dismiss\" | \"action\" | \"evicted\";\n\nexport interface ToastOptions {\n\t/** Primary line. */\n\ttitle: string;\n\t/** Secondary line, shown under the title. */\n\tdescription?: string;\n\t/** Controls the icon, the accent color, and the live region's politeness. Defaults to `\"info\"`. */\n\tvariant?: ToastVariant;\n\t/**\n\t * Milliseconds before auto-dismiss. `Infinity` means sticky — it never\n\t * auto-dismisses. Defaults to 5000, except for `variant: \"loading\"`, which\n\t * defaults to `Infinity`: a loading toast has no natural \"done\" time, and\n\t * auto-dismissing it after a fixed delay would misreport an operation\n\t * that is still running as finished.\n\t */\n\tduration?: number;\n\t/** A single optional action button. */\n\taction?: ToastAction;\n\t/**\n\t * Called exactly once, with the reason the toast closed. Pressing the\n\t * action settles it with `\"action\"` before the action's own `onClick`\n\t * runs; a later `dismissToast` on that same toast does not fire it again.\n\t */\n\tonClose?: (reason: ToastCloseReason) => void;\n\t/**\n\t * Shows a thin bar that drains over `duration`, pausing while the toast\n\t * is paused and static under reduced motion. Ignored for a sticky toast.\n\t */\n\tprogress?: boolean;\n}\n\nexport interface ToastItem {\n\treadonly id: string;\n\treadonly title: string;\n\treadonly description?: string;\n\treadonly variant: ToastVariant;\n\treadonly duration: number;\n\treadonly action?: ToastAction;\n\treadonly onClose?: (reason: ToastCloseReason) => void;\n\treadonly progress: boolean;\n}\n\n/** Toasts visible at once. Past this, the oldest is dismissed to make room for the newest. */\nconst MAX_VISIBLE = 4;\nconst DEFAULT_DURATION = 5000;\n\nlet items: ToastItem[] = [];\nlet seq = 0;\n\n/** `<Toaster>` subscribers, notified on every replacement of `items`. */\nconst listeners = new Set<() => void>();\n\n/** Replaces `items` and notifies subscribers — the one write path, standing in\n *  for the source's reactive assignment. */\nfunction setItems(next: ToastItem[]): void {\n\titems = next;\n\tfor (const listener of [...listeners]) {\n\t\tlistener();\n\t}\n}\n\ninterface TimerEntry {\n\thandle: ReturnType<typeof setTimeout> | undefined;\n\t/**\n\t * Absolute epoch ms this toast is due to auto-dismiss. Kept as a fixed\n\t * point in time rather than a \"remaining ms\" that decays on its own —\n\t * that's what lets a `<Toaster>` remount just compare it to `Date.now()`\n\t * and re-arm, instead of needing to have been watching continuously.\n\t */\n\tdeadline: number;\n\t/**\n\t * Epoch ms pausing began, or `undefined` when not paused. Only pausing\n\t * (hover/focus, via `pauseToast`) sets this — an unmount clearing this\n\t * entry's `handle` does not, because there is no live pointer to have\n\t * frozen anything: the deadline keeps meaning what it says regardless of\n\t * whether a `<Toaster>` is currently mounted to watch it.\n\t */\n\tpausedAt: number | undefined;\n}\n\n// Timers live outside `items` — they are bookkeeping the UI never renders,\n// not state a template reads, so there is nothing to gain from making this\n// reactive and a `Map` mutated from a `setTimeout` callback is simpler than\n// juggling render state for it.\nconst timers = new Map<string, TimerEntry>();\n\nfunction clearHandle(id: string) {\n\tconst entry = timers.get(id);\n\tif (entry?.handle !== undefined) {\n\t\tclearTimeout(entry.handle);\n\t\tentry.handle = undefined;\n\t}\n}\n\n/** Arms (or re-arms) the live timer counting down to an entry's stored `deadline`. */\nfunction armHandle(id: string) {\n\tconst entry = timers.get(id);\n\tif (!entry) return;\n\tconst ms = Math.max(0, entry.deadline - Date.now());\n\tentry.handle = setTimeout(() => removeToast(id, \"timeout\"), ms);\n}\n\n/** Ids whose `onClose` already fired (only `\"action\"` does so while the toast is still listed). */\nconst settled = new Set<string>();\n\nfunction fireClose(item: ToastItem, reason: ToastCloseReason): void {\n\ttry {\n\t\titem.onClose?.(reason);\n\t} catch (error) {\n\t\t// A throwing handler must not wedge the store; surface it out of band.\n\t\tqueueMicrotask(() => {\n\t\t\tthrow error;\n\t\t});\n\t}\n}\n\nfunction removeToast(id: string, reason: ToastCloseReason): void {\n\tconst item = items.find((candidate) => candidate.id === id);\n\tif (!item) return;\n\tsetItems(items.filter((candidate) => candidate.id !== id));\n\tclearHandle(id);\n\ttimers.delete(id);\n\tif (settled.delete(id)) return;\n\tfireClose(item, reason);\n}\n\n/** Settles a toast as `\"action\"` — called by `<Toast>` just before it runs the action's `onClick`. Does not close it. */\nexport function settleToastAction(id: string): void {\n\tconst item = items.find((candidate) => candidate.id === id);\n\tif (!item || settled.has(id)) return;\n\tsettled.add(id);\n\tfireClose(item, \"action\");\n}\n\n/**\n * Queues a toast and returns its id (usable with `dismissToast`). A no-op\n * that returns `\"\"` when called outside the browser: there is no client to\n * show it to, and a module-level list surviving across requests on a\n * long-lived server would leak toasts between unrelated users.\n */\nexport function toast(options: ToastOptions): string {\n\tif (typeof window === \"undefined\") {\n\t\treturn \"\";\n\t}\n\n\tseq += 1;\n\tconst id = `toast-${seq}`;\n\tconst variant = options.variant ?? \"info\";\n\tconst duration = options.duration ?? (variant === \"loading\" ? Infinity : DEFAULT_DURATION);\n\tconst item: ToastItem = {\n\t\tid,\n\t\ttitle: options.title,\n\t\tdescription: options.description,\n\t\tvariant,\n\t\tduration,\n\t\taction: options.action,\n\t\tonClose: options.onClose,\n\t\tprogress: options.progress === true && Number.isFinite(duration),\n\t};\n\n\t// Append-ordered, so index 0 is always the longest-standing toast — the\n\t// one evicted when a new toast would push the stack past the limit.\n\tconst next = [...items, item];\n\tlet evicted: ToastItem | undefined;\n\tif (next.length > MAX_VISIBLE) {\n\t\tevicted = next.shift();\n\t\tif (evicted) {\n\t\t\tclearHandle(evicted.id);\n\t\t\ttimers.delete(evicted.id);\n\t\t}\n\t}\n\tsetItems(next);\n\tif (evicted && !settled.delete(evicted.id)) fireClose(evicted, \"evicted\");\n\n\tif (Number.isFinite(duration)) {\n\t\ttimers.set(id, { handle: undefined, deadline: Date.now() + duration, pausedAt: undefined });\n\t\tarmHandle(id);\n\t}\n\n\treturn id;\n}\n\n/** Dismisses a toast immediately and clears its timer. Safe to call with an id that no longer exists. */\nexport function dismissToast(id: string): void {\n\tremoveToast(id, \"dismiss\");\n}\n\n/**\n * Pauses a toast's auto-dismiss countdown — e.g. while the pointer or focus\n * is on it — so `resumeToast` can continue it rather than restart it. A\n * no-op for a sticky toast or one already paused (or already unarmed for\n * some other reason, e.g. no `<Toaster>` currently mounted to run it).\n */\nexport function pauseToast(id: string): void {\n\tconst entry = timers.get(id);\n\tif (!entry || entry.handle === undefined) return;\n\tclearHandle(id);\n\tentry.pausedAt = Date.now();\n}\n\n/**\n * Resumes a paused toast's countdown from where it left off — the deadline\n * shifts forward by exactly how long it spent paused, so the toast still\n * gets the same amount of on-screen, unpaused time it was promised. A no-op\n * if a live timer is already counting down (nothing to resume).\n */\nexport function resumeToast(id: string): void {\n\tconst entry = timers.get(id);\n\tif (!entry || entry.handle !== undefined) return;\n\tif (entry.pausedAt !== undefined) {\n\t\tentry.deadline += Date.now() - entry.pausedAt;\n\t\tentry.pausedAt = undefined;\n\t}\n\tif (entry.deadline <= Date.now()) {\n\t\tremoveToast(id, \"timeout\");\n\t\treturn;\n\t}\n\tarmHandle(id);\n}\n\n/**\n * Clears every pending timer's *handle* without touching the toasts or their\n * deadlines. `<Toaster>` calls this when it unmounts. Deliberately does not\n * touch `deadline`: unmounting isn't pausing — there is no pointer left\n * hovering anything for a destroyed viewport, so the countdown a toast was\n * promised keeps running in real time whether or not something is currently\n * rendering it. `rearmToastTimers` is the other half of this: whatever a new\n * `<Toaster>` inherits gets a live timer again, counting toward the same\n * deadline. Also clears any stale `pausedAt` — a pause tied to a hover/focus\n * on a now-destroyed element cannot mean anything to whatever mounts next.\n */\nexport function clearAllToastTimers(): void {\n\tfor (const entry of timers.values()) {\n\t\tif (entry.handle !== undefined) {\n\t\t\tclearTimeout(entry.handle);\n\t\t\tentry.handle = undefined;\n\t\t}\n\t\tentry.pausedAt = undefined;\n\t}\n}\n\n/**\n * Re-arms every timer left unarmed with nothing currently pausing it — i.e.\n * every finite-duration toast whose previous `<Toaster>` went away before\n * its time was up. Call this from a `<Toaster>`'s own mount: without it, a\n * toast that outlives one viewport and is picked up by another stays\n * visible forever with no timer ever going to dismiss it, since nothing else\n * re-arms what `clearAllToastTimers` cleared. A deadline already in the past\n * (the gap between viewports was longer than the time left) re-arms to a\n * near-immediate dismiss through the same path, rather than a special case.\n */\nexport function rearmToastTimers(): void {\n\tfor (const [id, entry] of timers) {\n\t\tif (entry.handle !== undefined) continue; // already counting down\n\t\tif (entry.pausedAt !== undefined) continue; // genuinely paused; resumeToast's job\n\t\tarmHandle(id);\n\t}\n}\n\n/** Read-only view of the current toast stack, oldest first. `<Toaster>` renders this directly; nothing else should need to. */\nexport const toastStore = {\n\tget items(): readonly ToastItem[] {\n\t\treturn items;\n\t},\n\t/** Port-added: notifies on every stack change, for `useSyncExternalStore`. */\n\tsubscribe(listener: () => void): () => void {\n\t\tlisteners.add(listener);\n\t\treturn () => {\n\t\t\tlisteners.delete(listener);\n\t\t};\n\t},\n};\n","type":"registry:ui","target":"@ui/fancy/components/toast/store.ts"},{"path":"react/src/components/toast/toast.css","content":"/*\n * Declared locally, once, from the shared consumer-facing `--ft-accent` —\n * the same indirection `Button` (`--ft-btn-accent`) and `Popover`\n * (`--ft-overlay-accent` itself) use, and for the same reason: the brand\n * accent has no semantic Tailwind token, so retinting it has to go\n * through a real custom property a consumer can set, not a value baked\n * into each rule below. Every other rule in this file just reads\n * `var(--ft-overlay-accent)` with no fallback of its own — the fallback\n * lives here, exactly once, so retinting only ever means overriding\n * `--ft-accent` and never chasing three separate literals.\n *\n * Every selector is anchored on the component's own `ft-toast` vocabulary,\n * exactly as the source names it — plain CSS is global where the source's\n * was compiler-scoped.\n */\n.ft-toast {\n\t--ft-overlay-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n}\n\n.ft-toast-icon--success {\n\tcolor: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.7729 0.1535 163.22)));\n}\n\n.ft-toast-icon--error {\n\tcolor: var(--ft-status-error, light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216)));\n}\n\n.ft-toast-icon--info {\n\tcolor: var(--ft-overlay-accent);\n}\n\n.ft-toast-action {\n\tcolor: var(--ft-overlay-accent);\n}\n\n.ft-toast-spinner {\n\tdisplay: inline-block;\n\twidth: 14px;\n\theight: 14px;\n\tborder-radius: 50%;\n\tborder: 2px solid color-mix(in oklab, currentColor 20%, transparent);\n\tborder-top-color: var(--ft-overlay-accent);\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-toast-spinner {\n\t\tanimation: ft-toast-spin 0.8s linear infinite;\n\t}\n}\n\n@keyframes ft-toast-spin {\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n.ft-toast-progress {\n\tposition: absolute;\n\tinset-inline: 0;\n\tbottom: 0;\n\theight: 2px;\n\tbackground: var(--ft-overlay-accent);\n\topacity: 0.7;\n\ttransform-origin: left center;\n}\n\n/* Static (full) under reduced motion: the bar only ever animates when motion is welcome. */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-toast-progress {\n\t\tanimation: ft-toast-drain linear forwards;\n\t\tanimation-duration: var(--toast-duration);\n\t}\n\n\t.ft-toast-progress[data-paused=\"true\"] {\n\t\tanimation-play-state: paused;\n\t}\n}\n\n@keyframes ft-toast-drain {\n\tto {\n\t\ttransform: scaleX(0);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/toast/toast.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/toast?framework=react"}}