{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast-stack",
  "title": "Toast Stack",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A token-based notification stack with dismissal, optional actions, and pause-on-interaction timing.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/toast-stack.tsx",
      "content": "// Ballmac UI: Toast Stack. https://ui.ballmac.com/components/toast-stack\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleCheck, CircleAlert, Info, X } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype ToastMessage = {\n  /** Stable ID used for dismissal and updates. */ id: string\n  /** Short notification headline. */ title: string\n  /** Optional detail below the headline. */ description?: string\n  /** Visual and semantic tone. */ tone?: \"info\" | \"success\" | \"error\"\n  /** Optional action label. */ actionLabel?: string\n  /** Called when the action is selected. */ onAction?: () => void\n}\ntype ToastStackProps = Omit<React.ComponentProps<\"section\">, \"children\"> & {\n  /** Controlled toast list. */\n  toasts?: ToastMessage[]\n  /** Initial toast list when uncontrolled. */\n  defaultToasts?: ToastMessage[]\n  /** Called when a toast is dismissed. */\n  onToastsChange?: (toasts: ToastMessage[]) => void\n  /** Auto-dismiss delay; set to 0 to keep toasts until dismissed. */\n  durationMs?: number\n  /** Accessible label for the stack. */\n  label?: string\n}\n\nfunction ToastStack({\n  className,\n  toasts,\n  defaultToasts = [],\n  onToastsChange,\n  durationMs = 5000,\n  label,\n  ...props\n}: ToastStackProps) {\n  const msg = useMessages()\n  label ??= msg(\"toast-stack.label\", \"Notifications\")\n  const [internal, setInternal] = React.useState(defaultToasts)\n  const [paused, setPaused] = React.useState(false)\n  const current = toasts ?? internal\n  const dismiss = React.useCallback(\n    (id: string) => {\n      const next = current.filter((toast) => toast.id !== id)\n      if (toasts === undefined) setInternal(next)\n      onToastsChange?.(next)\n    },\n    [current, toasts, onToastsChange],\n  )\n  React.useEffect(() => {\n    if (durationMs <= 0 || paused || !current.length) return\n    const timer = window.setTimeout(() => dismiss(current[0].id), durationMs)\n    return () => window.clearTimeout(timer)\n  }, [current, durationMs, paused, dismiss])\n  return (\n    <section\n      data-slot=\"toast-stack\"\n      aria-label={label}\n      onMouseEnter={() => setPaused(true)}\n      onMouseLeave={() => setPaused(false)}\n      onFocus={() => setPaused(true)}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) setPaused(false)\n      }}\n      className={cn(\"w-full max-w-sm\", className)}\n      {...props}\n    >\n      <ol className=\"flex flex-col gap-2\">\n        {current.map((toast) => {\n          const tone = toast.tone ?? \"info\"\n          const Icon =\n            tone === \"success\"\n              ? CircleCheck\n              : tone === \"error\"\n                ? CircleAlert\n                : Info\n          return (\n            <li\n              key={toast.id}\n              data-slot=\"toast-stack-item\"\n              onKeyDown={(event) => {\n                if (event.key === \"Escape\") {\n                  event.preventDefault()\n                  dismiss(toast.id)\n                }\n              }}\n              className=\"bg-background/90 flex min-w-0 items-start gap-3 rounded-xl border border-border p-3 shadow-lg backdrop-blur-xl transition-[transform,opacity] duration-200 motion-reduce:transition-none\"\n            >\n              <span\n                aria-hidden=\"true\"\n                className={cn(\n                  \"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg\",\n                  tone === \"info\" && \"bg-primary/10 text-primary\",\n                  tone === \"success\" && \"bg-chart-2/10 text-chart-2\",\n                  tone === \"error\" && \"bg-destructive/10 text-destructive\",\n                )}\n              >\n                <Icon className=\"size-4\" />\n              </span>\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"text-sm font-semibold\">{toast.title}</p>\n                {toast.description && (\n                  <p className=\"text-muted-foreground mt-0.5 text-xs leading-relaxed\">\n                    {toast.description}\n                  </p>\n                )}\n                {toast.actionLabel && toast.onAction && (\n                  <button\n                    type=\"button\"\n                    onClick={toast.onAction}\n                    className=\"text-primary hover:underline mt-2 rounded-sm text-xs font-semibold outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                  >\n                    {toast.actionLabel}\n                  </button>\n                )}\n              </div>\n              <button\n                type=\"button\"\n                aria-label={msg(\"toast-stack.dismiss\", \"Dismiss {title}\", { title: toast.title })}\n                onClick={() => dismiss(toast.id)}\n                className=\"text-muted-foreground hover:bg-accent hover:text-foreground flex size-8 shrink-0 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n              >\n                <X aria-hidden=\"true\" className=\"size-4\" />\n              </button>\n            </li>\n          )\n        })}\n      </ol>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {current.at(-1)?.title ?? \"\"}\n      </span>\n    </section>\n  )\n}\n\nexport { ToastStack, type ToastStackProps, type ToastMessage }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/toast-stack.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "toast",
      "notification",
      "feedback"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "A token-based notification stack with dismissal, optional actions, and pause-on-interaction timing.",
      "whenToUse": [
        "Confirm an asynchronous action",
        "Offer undo or retry after a task"
      ],
      "whenNotToUse": [
        "Use banner for persistent page-wide messages"
      ],
      "composesWith": [
        "button",
        "notification-stack"
      ],
      "a11y": [
        {
          "keys": "Tab / Enter / Escape",
          "action": "Activate actions or dismiss a focused toast"
        }
      ],
      "customization": [
        "Controlled or uncontrolled list",
        "Auto-dismiss duration and pause"
      ]
    },
    "examples": [
      "toast-stack-demo",
      "toast-stack-states"
    ],
    "url": "https://ui.ballmac.com/components/toast-stack"
  },
  "categories": [
    "feedback"
  ],
  "type": "registry:ui"
}