{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast-deck",
  "title": "Toast deck",
  "description": "Toasts in practice: a revoke that removes the row at once and offers Undo (also ⌘Z), a deploy that morphs one toast from Uploaded to Live or to a failure with Retry, and the history feed.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/button.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/icon-button.json",
    "https://sproutboat.com/ikada/r/icons.json",
    "https://sproutboat.com/ikada/r/status-glyph.json",
    "https://sproutboat.com/ikada/r/toast.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/toast-deck/toast-deck.tsx",
      "content": "\"use client\";\n\n/**\n * Toasts in practice: a revoke that removes the row at once and offers Undo (also ⌘Z), a deploy\n * that morphs one toast from Uploaded to Live or to a failure with Retry, and the history feed.\n *\n * The toasts render in ikada's deck, which belongs to the app, not to this block: mount\n * `<ToastProvider>` once near your app root. The store behind `toast` is shared, so a second\n * provider would draw every toast twice; this block only calls `toast` and `deployToast`.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ikada/button\";\nimport { IconButton } from \"@/components/ikada/icon-button\";\nimport { StatusGlyph } from \"@/components/ikada/status-glyph\";\nimport { deployToast, toast } from \"@/components/ikada/toast\";\nimport type { DeployToast, ToastEvent, ToastTone } from \"@/components/ikada/toast\";\nimport { cn } from \"@/components/ikada/lib/cn\";\nimport { KeyIcon, RocketIcon, TrashIcon } from \"@/components/ikada/lib/icons\";\n\nexport interface ApiToken {\n  id: string;\n  name: string;\n  /** The visible tail of the secret, never the secret itself. */\n  hint: string;\n  lastUsed: string;\n}\n\nexport interface ToastDeckProps {\n  tokens?: ApiToken[];\n  /** The deploy's name in its toast, e.g. \"field-notes v48\". */\n  deployName?: string;\n  /** Called when a revoke is final (its Undo window closed without an undo). */\n  onRevoke?: (token: ApiToken) => void;\n  className?: string;\n}\n\n/** Sample data: four API tokens on a small team account. */\nexport const sampleTokens: ApiToken[] = [\n  { id: \"tok_1\", name: \"ci-deploy\", hint: \"sb_live_…4f2a\", lastUsed: \"2 min ago\" },\n  { id: \"tok_2\", name: \"robin-laptop\", hint: \"sb_live_…91c0\", lastUsed: \"yesterday\" },\n  { id: \"tok_3\", name: \"staging-bot\", hint: \"sb_live_…07de\", lastUsed: \"3 days ago\" },\n  { id: \"tok_4\", name: \"grafana-read\", hint: \"sb_live_…b3e8\", lastUsed: \"never\" },\n];\n\ninterface Seen {\n  key: string;\n  tone: ToastTone;\n  title: string;\n  description?: string;\n  at: number;\n}\n\nconst clock = (t: number) =>\n  new Date(t).toLocaleTimeString(\"en\", {\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    second: \"2-digit\",\n    hour12: false,\n    timeZone: \"UTC\",\n  });\n\n/** The last few toasts, from `toast.subscribe`: the hook an Activity feed or a bell would use. */\nfunction useToastHistory(limit = 4): Seen[] {\n  const [seen, setSeen] = useState<Seen[]>([]);\n  useEffect(\n    () =>\n      toast.subscribe((event: ToastEvent) => {\n        if (event.type === \"close\" || event.toast.silent) return;\n        const { id, tone, title, description } = event.toast;\n        setSeen((list) => {\n          const entry = { key: `${id}:${title}`, tone, title, description, at: event.at };\n          return [entry, ...list.filter((s) => s.key !== entry.key)].slice(0, limit);\n        });\n      }),\n    [limit],\n  );\n  return seen;\n}\n\nexport function ToastDeck({\n  tokens: initial = sampleTokens,\n  deployName = \"field-notes v48\",\n  onRevoke,\n  className,\n}: ToastDeckProps) {\n  const [tokens, setTokens] = useState(initial);\n  const [deploying, setDeploying] = useState(false);\n  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);\n  const history = useToastHistory();\n\n  useEffect(() => setTokens(initial), [initial]);\n  useEffect(() => () => timers.current.forEach(clearTimeout), []);\n  const later = (ms: number, fn: () => void) => timers.current.push(setTimeout(fn, ms));\n\n  function revoke(token: ApiToken) {\n    const index = tokens.findIndex((t) => t.id === token.id);\n    setTokens((list) => list.filter((t) => t.id !== token.id));\n    let undone = false;\n    toast.undo({\n      title: `Revoked ${token.name}`,\n      description: \"Requests with it now get 401.\",\n      confirmTitle: `Restored ${token.name}`,\n      onUndo: () => {\n        undone = true;\n        setTokens((list) => {\n          if (list.some((t) => t.id === token.id)) return list;\n          const next = [...list];\n          next.splice(Math.min(index, next.length), 0, token);\n          return next;\n        });\n      },\n    });\n    // The undo toast stays 8s; after that the revoke is final.\n    later(8200, () => !undone && onRevoke?.(token));\n  }\n\n  /** One toast for the whole deploy; Retry morphs the same toast back to Uploaded. */\n  function deploy(fail: boolean) {\n    const run: DeployToast = deployToast({ name: deployName, onRetry: () => steps(run, false) });\n    steps(run, fail);\n  }\n\n  /** Uploaded, the progress line creeping on, then Live or a failure with Retry. */\n  function steps(run: DeployToast, fail: boolean) {\n    setDeploying(true);\n    run.uploaded(0.2);\n    later(500, () => run.progress(0.45));\n    later(1000, () => run.progress(0.75));\n    later(1500, () => {\n      if (fail) run.failed(\"build exited with 1\");\n      else run.live();\n      setDeploying(false);\n    });\n  }\n\n  return (\n    <div className={cn(\"@container text-fg\", className)}>\n      <div className=\"grid gap-4 @3xl:grid-cols-[minmax(0,1fr)_18rem]\">\n        <section\n          aria-labelledby=\"toast-deck-tokens\"\n          className=\"flex min-w-0 flex-col rounded-xl bg-surface shadow-card\"\n        >\n          <header className=\"flex flex-wrap items-center gap-3 px-4 pt-4 pb-3\">\n            <div className=\"me-auto min-w-0\">\n              <h2 id=\"toast-deck-tokens\" className=\"text-sb-md font-semibold\">\n                API tokens\n              </h2>\n              <p className=\"text-sb-sm text-fg-muted\">Revoke one: it goes at once, and Undo (or ⌘Z) brings it back.</p>\n            </div>\n            <div className=\"flex flex-wrap gap-2\">\n              <Button variant=\"secondary\" size=\"sm\" disabled={deploying} onClick={() => deploy(true)}>\n                Deploy a broken build\n              </Button>\n              <Button variant=\"primary\" size=\"sm\" disabled={deploying} onClick={() => deploy(false)}>\n                <RocketIcon />\n                Deploy\n              </Button>\n            </div>\n          </header>\n          {tokens.length ? (\n            <ul className=\"divide-y divide-hairline border-t border-hairline\">\n              {tokens.map((token) => (\n                <li key={token.id} className=\"flex min-h-(--sb-row-h) items-center gap-3 px-4 py-2\">\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-well text-fg-muted\"\n                  >\n                    <KeyIcon />\n                  </span>\n                  <div className=\"flex min-w-0 flex-1 flex-col @md:flex-row @md:items-center @md:gap-3\">\n                    <span className=\"truncate font-semibold\" title={token.name}>\n                      {token.name}\n                    </span>\n                    <span className=\"truncate font-mono text-sb-sm text-fg-muted\" title={token.hint}>\n                      {token.hint}\n                    </span>\n                  </div>\n                  <span className=\"hidden shrink-0 text-sb-sm text-fg-muted @lg:inline\">Used {token.lastUsed}</span>\n                  <IconButton label={`Revoke ${token.name}`} variant=\"ghost\" size=\"sm\" onClick={() => revoke(token)}>\n                    <TrashIcon />\n                  </IconButton>\n                </li>\n              ))}\n            </ul>\n          ) : (\n            <div className=\"flex flex-col items-center gap-2 border-t border-hairline px-4 py-8 text-center\">\n              <p className=\"text-fg-muted\">Every token is revoked.</p>\n              <Button size=\"sm\" variant=\"ghost\" onClick={() => setTokens(initial)}>\n                Bring the samples back\n              </Button>\n            </div>\n          )}\n        </section>\n\n        <section\n          aria-labelledby=\"toast-deck-history\"\n          className=\"flex min-w-0 flex-col gap-3 rounded-xl bg-surface p-4 shadow-card\"\n        >\n          <div>\n            <h2 id=\"toast-deck-history\" className=\"text-sb-md font-semibold\">\n              Recent\n            </h2>\n            <p className=\"text-sb-sm text-fg-muted\">What the toasts said this session.</p>\n          </div>\n          {history.length ? (\n            <ol className=\"flex flex-col gap-2.5\" aria-label=\"Recent notifications\">\n              {history.map((s) => (\n                <li key={s.key} className=\"flex items-start gap-2.5\">\n                  <StatusGlyph tone={s.tone} className=\"mt-1.5\" />\n                  <div className=\"min-w-0 flex-1\">\n                    <p className=\"truncate text-sb-sm font-semibold\" title={s.title}>\n                      {s.title}\n                    </p>\n                    {s.description ? <p className=\"line-clamp-2 text-sb-sm text-fg-muted\">{s.description}</p> : null}\n                  </div>\n                  <time\n                    className=\"shrink-0 text-sb-xs text-fg-muted tabular-nums\"\n                    dateTime={new Date(s.at).toISOString()}\n                  >\n                    {clock(s.at)}\n                  </time>\n                </li>\n              ))}\n            </ol>\n          ) : (\n            <p className=\"rounded-lg bg-well px-3 py-4 text-center text-sb-sm text-fg-muted shadow-tray\">\n              Nothing yet. Revoke a token or deploy.\n            </p>\n          )}\n        </section>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/toast-deck/toast-deck.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}