{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "description": "An animated toast stack for React and shadcn, with an action button such as Undo.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-overlay-core.json",
    "https://eased.dev/r/button.json",
    "https://eased.dev/r/spinner.json",
    "https://eased.dev/r/text-swap.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/toast/toast.tsx",
      "content": "\"use client\";\n\nimport { Toast as BaseToast } from \"@base-ui/react/toast\";\nimport type { ToastManager, ToastManagerAddOptions, ToastManagerUpdateOptions, ToastObject } from \"@base-ui/react/toast\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useMemo, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from \"react\";\nimport { panelSurface, TOAST } from \"../_primitives/overlay-core/overlay-core\";\nimport { Button } from \"../button/button\";\nimport { exitFast, fade, reducedFade, snappy, useSafeReducedMotion } from \"../lib/motion\";\nimport { pressIcon } from \"../lib/press\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { TextSwap } from \"../text-swap/text-swap\";\n\n/**\n * A stacked toast manager on Base UI Toast. Toasts rise 12px at 0.97 on the smooth spring and\n * stack behind the newest, each peeking 10px and 5% smaller; hovering or focusing the stack\n * fans it out 8px apart and pauses every timer. Promise toasts morph in place: the spinner\n * becomes a check that draws in, and the title swaps with TextSwap.\n */\n\nexport type ToastType = \"default\" | \"success\" | \"error\" | \"loading\";\nexport type ToastPosition = \"bottom-right\" | \"bottom-center\" | \"top-center\" | \"top-right\";\nexport type ToastSwipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\n/** Custom data Eased reads from a toast. */\nexport type ToastData = {\n  /** Replaces the status icon (16px). Default: none for \"default\", a glyph for the other types. */\n  icon?: ReactNode;\n};\n\nexport type ToastAction = {\n  /** The button's label, e.g. \"Undo\". */\n  label: string;\n  /** Runs on click. The toast closes afterwards unless you return `false`. */\n  onClick: () => void | false;\n};\n\nexport type ToastOptions = Omit<ToastManagerAddOptions<ToastData>, \"type\"> & {\n  /** \"default\" (no icon), \"success\", \"error\" or \"loading\" (a spinner; never times out). */\n  type?: ToastType;\n  /** One action button, e.g. `{ label: \"Undo\", onClick: restore }`. Shorthand for `actionProps`. */\n  action?: ToastAction;\n};\n\nexport type ToastUpdate = Omit<ToastManagerUpdateOptions<ToastData>, \"type\"> & { type?: ToastType; action?: ToastAction };\n\nexport type ToastPromiseOptions<Value> = {\n  /** Shown while the promise is pending: a title, or options. */\n  loading: string | ToastUpdate;\n  /** Shown when it resolves: a title, options, or a function of the result. */\n  success: string | ToastUpdate | ((result: Value) => string | ToastUpdate);\n  /** Shown when it rejects: a title, options, or a function of the error. */\n  error: string | ToastUpdate | ((error: unknown) => string | ToastUpdate);\n};\n\n/** What `useToast()` and `createToastApi()` return. Every method returns or takes a toast id. */\nexport type ToastApi = {\n  /** Shows a toast and returns its id. Passing an existing `id` updates that toast in place. */\n  add: (options: ToastOptions) => string;\n  /** A success toast: a check icon. */\n  success: (title: ReactNode, options?: Omit<ToastOptions, \"title\" | \"type\">) => string;\n  /** An error toast: an alert icon, announced urgently (`priority: \"high\"`) unless you say otherwise. */\n  error: (title: ReactNode, options?: Omit<ToastOptions, \"title\" | \"type\">) => string;\n  /** One toast that follows a promise: loading, then success or error, morphing in place. Returns the promise. */\n  promise: <Value>(promise: Promise<Value>, options: ToastPromiseOptions<Value>) => Promise<Value>;\n  /** Changes a toast in place (title, type, description …); its timer restarts. */\n  update: (id: string, options: ToastUpdate) => void;\n  /** Closes one toast, or every toast when `id` is left out. */\n  close: (id?: string) => void;\n};\n\ntype Methods = Pick<ToastManager<ToastData>, \"add\" | \"update\" | \"close\">;\n\nlet seq = 0;\n\nconst asOptions = (o: string | ToastUpdate): ToastUpdate => (typeof o === \"string\" ? { title: o } : o);\n\nfunction bind(manager: Methods): ToastApi {\n  // `action` needs the id before the toast exists, so it can close the toast after running.\n  const resolve = <T extends ToastOptions | ToastUpdate>(id: string, { action, ...options }: T) =>\n    action\n      ? {\n          ...options,\n          actionProps: {\n            children: action.label,\n            onClick: () => {\n              if (action.onClick() !== false) manager.close(id);\n            },\n          },\n        }\n      : options;\n  const add = (options: ToastOptions) => {\n    const id = options.id ?? `eased-toast-${++seq}`;\n    return manager.add({ ...resolve(id, options), id });\n  };\n  return {\n    add,\n    success: (title, options) => add({ ...options, title, type: \"success\" }),\n    error: (title, options) => add({ priority: \"high\", ...options, title, type: \"error\" }),\n    update: (id, options) => manager.update(id, resolve(id, options)),\n    close: (id) => manager.close(id),\n    // Built on add + update rather than Base UI's `promise`, so `action` works in every state.\n    promise: (promise, { loading, success, error }) => {\n      const id = add({ ...asOptions(loading), type: \"loading\" });\n      promise.then(\n        (result) => manager.update(id, resolve(id, { ...asOptions(typeof success === \"function\" ? success(result) : success), type: \"success\" })),\n        (reason: unknown) =>\n          manager.update(id, resolve(id, { priority: \"high\", ...asOptions(typeof error === \"function\" ? error(reason) : error), type: \"error\" })),\n      );\n      return promise;\n    },\n  };\n}\n\n/** Creates a manager to pass to `Toaster`'s `toastManager`, for toasts queued outside React. */\nexport const createToastManager = BaseToast.createToastManager<ToastData>;\n\n/**\n * The same methods as `useToast()`, bound to a global manager, so any module can toast:\n * `export const toastManager = createToastManager(); export const toast = createToastApi(toastManager);`\n * then `<Toaster toastManager={toastManager} />` and `toast.success(\"Saved\")` anywhere.\n */\nexport function createToastApi(manager: ToastManager<ToastData>): ToastApi {\n  return bind(manager);\n}\n\n/** Toast methods for components inside a `Toaster`: `{ add, success, error, promise, update, close }`. */\nexport function useToast(): ToastApi {\n  const { add, update, close } = BaseToast.useToastManager<ToastData>();\n  return useMemo(() => bind({ add, update, close }), [add, update, close]);\n}\n\nconst MOBILE = \"(max-width: 639.98px)\";\nconst subscribeMobile = (onChange: () => void) => {\n  const mq = window.matchMedia(MOBILE);\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n\n// Where the viewport sits. Under 640px every position becomes full-width at the bottom, above the\n// home indicator (the bottom offset clears `safe-area-inset-bottom`).\nconst PLACEMENT: Record<ToastPosition, string> = {\n  \"bottom-right\": \"bottom-4 right-4\",\n  \"bottom-center\": \"bottom-4 inset-x-0 mx-auto\",\n  \"top-center\": \"top-4 inset-x-0 mx-auto\",\n  \"top-right\": \"top-4 right-4\",\n};\nconst MOBILE_PLACEMENT =\n  \"max-sm:inset-x-4 max-sm:top-auto max-sm:bottom-[max(16px,env(safe-area-inset-bottom))] max-sm:mx-0 max-sm:w-auto max-sm:max-w-none\";\n\n/*\n * Which edge the stack grows from. Each toast reads these from the viewport: the stack's\n * direction (`--dir`, −1 grows upward), the edge toasts anchor to and scale from, and which side\n * of a collapsed toast is cut (see TOAST_ROOT).\n */\nconst SIDE = {\n  bottom: \"[--dir:-1] [--toast-top:auto] [--toast-bottom:0px] [--toast-origin:bottom] [--toast-far-top:0] [--toast-near:12px]\",\n  top: \"[--dir:1] [--toast-top:0px] [--toast-bottom:auto] [--toast-origin:top] [--toast-far-top:1] [--toast-near:36px]\",\n} as const;\n\n// TOAST's numbers as the variables the classes below read (an inline style is safe here:\n// nothing overrides these; the reduced branch overrides the per-toast copies).\nconst TOAST_VARS = {\n  \"--toast-rise\": `${TOAST.y}px`,\n  \"--toast-from\": TOAST.scale,\n  \"--toast-fade\": `${TOAST.fade}ms`,\n  \"--toast-exit\": `${TOAST.exit}ms`,\n  \"--toast-peek\": `${TOAST.peek}px`,\n  \"--toast-shrink\": TOAST.shrink,\n  \"--toast-gap\": `${TOAST.gap}px`,\n} as CSSProperties;\n\n/*\n * The stack, without animating height (rule 75).\n *\n * Base UI's demo clamps every toast to the frontmost toast's height and transitions `height`.\n * Here each toast keeps its natural height (Base UI still measures it for `--toast-offset-y`).\n * The card's material is a separate layer (`TOAST_CARD`); while collapsed, a back toast taller\n * than the front slides that layer down by the difference (`--c`), so its top edge, border and\n * rounded corners sit exactly where a front-height card would, and the toast itself moves so\n * that edge peeks 10px past the one in front. The part of the layer that now overhangs the\n * toast's anchored edge is cut with `clip-path: inset()`, at a line that lies behind the front\n * toast; the other three sides get negative insets so the shadow is never clipped, and the\n * near side's inset also trims the toast's invisible upper part out of hit-testing. Expanding\n * slides the layer back on the same spring as the position; the clip itself switches at once,\n * since a clip that interpolates would show the cut mid-flight. Everything that moves is\n * `transform`, so a 3-toast stack costs nothing per frame.\n *\n * Swipe: Base UI writes `--toast-swipe-movement-x/y` (and an inline transform while dragging);\n * the transform below always adds them, so the release carries on from where the finger left.\n */\nconst TOAST_ROOT = [\n  \"group absolute inset-x-0 top-(--toast-top) bottom-(--toast-bottom) origin-(--toast-origin) z-[calc(1000_-_var(--toast-index))] select-none outline-none\",\n  // Geometry: index, scale, natural and front heights, the visible height, the cut.\n  \"[--i:var(--toast-index)] [--s:max(0,_1_-_var(--i)_*_var(--toast-shrink))]\",\n  \"[--h:var(--toast-height,0px)] [--front:var(--toast-frontmost-height,var(--h))] [--v:min(var(--h),var(--front))] [--c:calc(var(--h)_-_var(--v))]\",\n  \"[--y:calc(var(--dir)_*_(var(--front)_-_var(--v)_*_var(--s)_+_var(--i)_*_var(--toast-peek)))]\",\n  \"data-expanded:[--s:1] data-expanded:[--c:0px] data-expanded:[--y:calc(var(--dir)_*_(var(--toast-offset-y)_+_var(--i)_*_var(--toast-gap)))]\",\n  // Enter / exit offsets; the reduced branch zeroes them so only opacity moves.\n  \"[--rise:var(--toast-rise)] [--from:var(--toast-from)] [--fling:100%] [--ex:0px] [--ey:0px] [--es:1]\",\n  \"reduced:[--rise:0px] reduced:[--from:1] reduced:[--fling:0%]\",\n  \"[transform:translateX(calc(var(--toast-swipe-movement-x)_+_var(--ex)))_translateY(calc(var(--y)_+_var(--toast-swipe-movement-y)_+_var(--ey)))_scale(calc(var(--s)_*_var(--es)))]\",\n  // Clip: the far side cuts at the anchored edge only while the card layer overhangs it.\n  \"[--far:min(0px,calc(var(--c)_*_100_-_48px))] [--near:calc(var(--c)_-_var(--toast-near))]\",\n  \"[clip-path:inset(calc(var(--toast-far-top)_*_var(--far)_+_(1_-_var(--toast-far-top))_*_var(--near))_-48px_calc((1_-_var(--toast-far-top))_*_var(--far)_+_var(--toast-far-top)_*_var(--near))_-48px)]\",\n  // Position on the smooth spring, opacity on a 200ms tween.\n  \"[transition-property:transform,opacity] [transition-duration:var(--eased-duration-spring-smooth),var(--toast-fade)] [transition-timing-function:var(--eased-spring-smooth),var(--eased-ease-enter)]\",\n  \"data-starting-style:opacity-0 data-starting-style:[--ey:calc(var(--dir)_*_-1_*_var(--rise))] data-starting-style:[--es:var(--from)]\",\n  // Exit: half the distance and half the scale, 150ms, no overshoot. A swipe carries on its way.\n  \"data-ending-style:opacity-0 data-ending-style:[--ey:calc(var(--dir)_*_-0.5_*_var(--rise))] data-ending-style:[--es:calc(0.5_+_var(--from)_*_0.5)] data-ending-style:[transition-duration:var(--toast-exit)] data-ending-style:[transition-timing-function:var(--eased-ease-standard)]\",\n  \"data-ending-style:data-swipe-direction:[--es:1] data-ending-style:data-swipe-direction:[transition-duration:var(--eased-duration-standard)]\",\n  \"data-ending-style:data-[swipe-direction=down]:[--ey:var(--fling)] data-ending-style:data-[swipe-direction=up]:[--ey:calc(var(--fling)_*_-1)]\",\n  \"data-ending-style:data-[swipe-direction=right]:[--ey:0px] data-ending-style:data-[swipe-direction=right]:[--ex:var(--fling)] data-ending-style:data-[swipe-direction=left]:[--ey:0px] data-ending-style:data-[swipe-direction=left]:[--ex:calc(var(--fling)_*_-1)]\",\n  // Past the limit: Base UI keeps it mounted and inert; it fades out.\n  \"data-limited:opacity-0\",\n  \"reduced:[transition-duration:0ms,var(--eased-duration-instant)] reduced:data-ending-style:[transition-duration:0ms,var(--eased-duration-instant)]\",\n  // While expanded, fill the gaps between toasts so the pointer never falls through and collapses the stack.\n  \"after:pointer-events-auto after:absolute after:inset-x-0 after:-inset-y-(--toast-gap) after:-z-1 after:hidden after:content-[''] data-expanded:after:block\",\n].join(\" \");\n\n// The focus ring lives on the card, not the root: the root's transition list is the motion's.\nconst TOAST_CARD = `${panelSurface} absolute inset-0 outline-(--eased-ring) group-focus-visible:outline-2 group-focus-visible:outline-solid group-focus-visible:outline-offset-2 [transform:translateY(calc(var(--dir)_*_-1_*_var(--c)))] [transition:transform_var(--eased-duration-spring-smooth)_var(--eased-spring-smooth)] reduced:[transition:none]`;\n\n// Behind the front, content fades out; it fades back when the stack expands.\nconst TOAST_CONTENT =\n  \"relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-start px-4 py-3 text-ui-sm [transition:opacity_var(--eased-duration-standard)_var(--eased-ease-standard)] data-behind:not-data-expanded:opacity-0 data-behind:not-data-expanded:pointer-events-none\";\n\n// Shown on hover (fine pointers) and keyboard focus; always shown where there is no hover.\n// `opacity`, not `invisible`, so it holds its place (R108) and stays in the tab order.\nconst CLOSE = `inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-control-xs text-text-muted opacity-0 hover-fine:bg-surface-hover hover-fine:text-text group-hover-fine:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100 [&_svg]:size-4 ${pressIcon}`;\n\nexport type ToasterProps = {\n  /** Where the stack sits. Under 640px it is always full-width at the bottom. Default \"bottom-right\". */\n  position?: ToastPosition;\n  /** How many toasts show at once; older ones fade out (`data-limited`) until a slot frees. Default 3. */\n  limit?: number;\n  /** Default auto-dismiss time in ms; 0 keeps toasts until closed. Loading toasts never time out. Default 5000. */\n  timeout?: number;\n  /** A global manager from `createToastManager()`, to toast from outside React. */\n  toastManager?: ToastManager<ToastData>;\n  /**\n   * Portal target. Default `document.body`, with the stack fixed to the window. When set, the\n   * stack is `absolute` inside it instead (the container needs `position: relative`).\n   */\n  container?: HTMLElement | null;\n  /** Swipe directions that dismiss. Default down + right at the bottom, up + right at the top right, up at the top centre. */\n  swipeDirection?: ToastSwipeDirection | ToastSwipeDirection[];\n  /** Accessible name of each toast's close button. Default \"Dismiss\". */\n  closeLabel?: string;\n  /** Appended to the viewport's classes. */\n  className?: string;\n  /** Your app. Components inside can call `useToast()`. */\n  children?: ReactNode;\n};\n\nconst SWIPE: Record<ToastPosition, ToastSwipeDirection[]> = {\n  \"bottom-right\": [\"down\", \"right\"],\n  \"bottom-center\": [\"down\", \"right\"],\n  \"top-center\": [\"up\"],\n  \"top-right\": [\"up\", \"right\"],\n};\n\n/**\n * Mounts the toast stack: Base UI's Provider (wrap your app, or pass a global `toastManager`),\n * a portal and the viewport landmark. F6 moves focus into the toasts.\n */\nexport function Toaster({\n  position = \"bottom-right\",\n  limit = 3,\n  timeout = 5000,\n  toastManager,\n  container,\n  swipeDirection,\n  closeLabel = \"Dismiss\",\n  className = \"\",\n  children,\n}: ToasterProps) {\n  const mobile = useSyncExternalStore(subscribeMobile, () => window.matchMedia(MOBILE).matches, () => false);\n  const top = position.startsWith(\"top\") && !mobile;\n  const swipe = swipeDirection ?? (mobile ? SWIPE[\"bottom-right\"] : SWIPE[position]);\n  return (\n    <BaseToast.Provider limit={limit} timeout={timeout} toastManager={toastManager}>\n      {children}\n      <BaseToast.Portal container={container}>\n        <BaseToast.Viewport\n          data-side={top ? \"top\" : \"bottom\"}\n          style={TOAST_VARS}\n          className={`${container ? \"absolute z-10\" : \"fixed z-50\"} w-[22rem] max-w-[calc(100%_-_32px)] outline-none ${PLACEMENT[position]} ${MOBILE_PLACEMENT} ${top ? SIDE.top : SIDE.bottom} ${className}`}\n        >\n          <ToastList swipe={swipe} closeLabel={closeLabel} />\n        </BaseToast.Viewport>\n      </BaseToast.Portal>\n    </BaseToast.Provider>\n  );\n}\n\nfunction ToastList({ swipe, closeLabel }: { swipe: ToastSwipeDirection | ToastSwipeDirection[]; closeLabel: string }) {\n  const { toasts } = BaseToast.useToastManager<ToastData>();\n  return toasts.map((t) => <ToastItem key={t.id} toast={t} swipe={swipe} closeLabel={closeLabel} />);\n}\n\nfunction ToastItem({ toast, swipe, closeLabel }: { toast: ToastObject<ToastData>; swipe: ToastSwipeDirection | ToastSwipeDirection[]; closeLabel: string }) {\n  // A toast that started as loading will change its title, so it swaps (one line, TextSwap);\n  // any other toast lays its title out normally and may wrap.\n  const [morphs] = useState(toast.type === \"loading\");\n  const type = (toast.type ?? \"default\") as ToastType;\n  const icon = toast.data?.icon ?? (type === \"default\" ? null : <StatusIcon type={type} />);\n  return (\n    <BaseToast.Root toast={toast} swipeDirection={swipe} className={TOAST_ROOT}>\n      <span aria-hidden className={TOAST_CARD} />\n      <BaseToast.Content className={TOAST_CONTENT}>\n        {icon && (\n          // Centred on the title's first line (R107).\n          <span aria-hidden className=\"col-start-1 me-3 flex h-[1lh] items-center [&_svg]:size-4 [&_svg]:shrink-0\">\n            {icon}\n          </span>\n        )}\n        <span className=\"col-start-2 flex min-w-0 flex-col gap-0.5\">\n          <BaseToast.Title className={`font-medium text-text ${morphs ? \"overflow-hidden\" : \"text-pretty break-words\"}`}>\n            {morphs && typeof toast.title === \"string\" ? <TextSwap animateWidth={false}>{toast.title}</TextSwap> : toast.title}\n          </BaseToast.Title>\n          {toast.description !== undefined && (\n            <BaseToast.Description className=\"text-pretty text-text-secondary\">\n              {morphs ? <Fade swapKey={typeof toast.description === \"string\" ? toast.description : toast.updateKey}>{toast.description}</Fade> : toast.description}\n            </BaseToast.Description>\n          )}\n        </span>\n        <span className=\"col-start-3 ms-3 flex h-[1lh] items-center gap-1\">\n          <BaseToast.Action render={<Button size=\"sm\" variant=\"secondary\" />} />\n          <BaseToast.Close aria-label={closeLabel} className={`${CLOSE} -me-1`}>\n            <CloseIcon />\n          </BaseToast.Close>\n        </span>\n      </BaseToast.Content>\n    </BaseToast.Root>\n  );\n}\n\n/** A description that changes fades in (opacity only, so its line count never jumps). */\nfunction Fade({ swapKey, children }: { swapKey: string | number | undefined; children: ReactNode }) {\n  const reduced = useSafeReducedMotion();\n  return (\n    <motion.span key={swapKey} className=\"block\" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={reduced ? reducedFade : fade}>\n      {children}\n    </motion.span>\n  );\n}\n\n/**\n * The status glyph. A change of type cross-fades the glyphs with a 0.6 → 1 scale on the snappy\n * spring; the success check then draws its stroke in. The first render does not animate (the\n * toast's own entrance carries it). Reduced motion: a 100ms cross-fade.\n */\nfunction StatusIcon({ type }: { type: Exclude<ToastType, \"default\"> }) {\n  const reduced = useSafeReducedMotion();\n  return (\n    <span className=\"relative size-4\">\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={type}\n          className=\"absolute inset-0 flex items-center justify-center\"\n          initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}\n          animate={reduced ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n          exit={reduced ? { opacity: 0, transition: reducedFade } : { opacity: 0, scale: 0.6, transition: exitFast }}\n          transition={reduced ? reducedFade : { ...snappy, opacity: fade }}\n        >\n          {type === \"loading\" ? (\n            <Spinner size=\"md\" label={false} />\n          ) : type === \"success\" ? (\n            <CheckIcon draw={!reduced} />\n          ) : (\n            <AlertIcon />\n          )}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction CheckIcon({ draw }: { draw: boolean }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"text-success-text\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n      <motion.path\n        d=\"M5.5 8.25 7.25 10l3.25-3.75\"\n        initial={draw ? { pathLength: 0 } : false}\n        animate={{ pathLength: 1 }}\n        transition={{ ...snappy, delay: 0.06 }}\n      />\n    </svg>\n  );\n}\n\nfunction AlertIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" className=\"text-danger-text\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n      <path d=\"M8 4.75v3.75\" />\n      <circle cx=\"8\" cy=\"11\" r=\"0.25\" fill=\"currentColor\" />\n    </svg>\n  );\n}\n\nfunction CloseIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" aria-hidden>\n      <path d=\"m4.5 4.5 7 7m0-7-7 7\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/toast/toast.tsx"
    }
  ],
  "meta": {
    "details": "Toasts rise 12px on the smooth spring and stack behind the newest, peeking 10px and 5% smaller; hover fans them out and pauses every timer. Promise toasts morph from spinner to check in place.",
    "industries": [
      "saas",
      "devtools",
      "ecommerce",
      "finance"
    ],
    "releaseAt": "2026-10-21"
  },
  "type": "registry:ui"
}