{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inbox-1",
  "title": "Inbox 1: notifications page",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A notifications page grouped by day: unread marked by an accent, mentions and comments with excerpts, requests answered inline (Approve, Deny, Accept, Decline), keyboard triage and an Undo bar after archiving.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/blur-fade.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/inbox-1/inbox-1.tsx",
      "content": "// Ballmac UI: Inbox 1. https://ui.ballmac.com/blocks/inbox-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Archive, AtSign, BellOff, Check, CheckCheck, CircleDot, CreditCard, MessageSquare, Settings, ShieldCheck, Sparkles, Undo2, UserPlus } from \"lucide-react\"\n\nimport { BlurFadeGroup } from \"@/components/ballmac/blur-fade\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { Kbd } from \"@/components/ballmac/kbd\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Inbox1Kind = \"mention\" | \"comment\" | \"approval\" | \"invite\" | \"system\" | \"billing\"\n\ntype Inbox1Item = {\n  id: string\n  kind: Inbox1Kind\n  /** Who did it. Leave out for something the system did. */\n  actor?: string\n  /** What happened, as a phrase that follows the actor: \"mentioned you in\". For the system, the whole sentence. */\n  action: string\n  /** The thing it happened to, shown in bold after the action. */\n  target?: string\n  /** A quoted excerpt, such as the comment itself. */\n  preview?: string\n  /** The group heading (\"Today\", \"Yesterday\", \"Earlier this week\"). Items with the same day are grouped. */\n  day: string\n  /** Already written for people (\"09:42\"). */\n  time: string\n  unread?: boolean\n  /** Inline buttons for something that needs an answer. */\n  actions?: { primary: string; secondary?: string }\n  /** What happened after it was answered (\"Approved\"). Hides the buttons. */\n  outcome?: string\n  href?: string\n}\n\ntype Inbox1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    items?: Inbox1Item[]\n    title?: string\n    /** Called when an item is opened with a click or Enter. Opening marks it read. */\n    onOpen?: (item: Inbox1Item) => void\n    /** Called when an item is marked read or unread. */\n    onMarkRead?: (id: string, read: boolean) => void\n    /** Called straight away when an item is archived. The block offers Undo for a few seconds. */\n    onArchive?: (id: string) => void\n    /** Called when Undo is pressed, with the item to put back. */\n    onRestore?: (item: Inbox1Item) => void\n    /** Called by \"Mark all as read\". */\n    onMarkAllRead?: () => void\n    /** Called when an inline button is pressed. The block shows its outcome straight away. */\n    onAction?: (id: string, which: \"primary\" | \"secondary\") => void\n    /** Called by the settings button. Without it the button is hidden. */\n    onSettings?: () => void\n    /** The view shown first. */\n    defaultTab?: \"all\" | \"unread\" | \"mentions\"\n  }\n\nconst kinds: Record<Inbox1Kind, { icon: typeof AtSign; tile: string; label: string }> = {\n  mention: { icon: AtSign, tile: \"bg-chart-1/20\", label: \"Mention\" },\n  comment: { icon: MessageSquare, tile: \"bg-chart-4/20\", label: \"Comment\" },\n  approval: { icon: ShieldCheck, tile: \"bg-chart-3/25\", label: \"Approval\" },\n  invite: { icon: UserPlus, tile: \"bg-chart-2/20\", label: \"Invitation\" },\n  system: { icon: Sparkles, tile: \"bg-chart-5/20\", label: \"Update\" },\n  billing: { icon: CreditCard, tile: \"bg-chart-3/25\", label: \"Billing\" },\n}\n\nconst sample: Inbox1Item[] = [\n  { id: \"n1\", kind: \"approval\", actor: \"Support agent\", action: \"wants to refund $129.00 on\", target: \"order 4821\", preview: \"The second charge is a duplicate made 40 seconds after the first, with the same card and amount.\", day: \"Today\", time: \"09:42\", unread: true, actions: { primary: \"Approve\", secondary: \"Deny\" } },\n  { id: \"n2\", kind: \"mention\", actor: \"Priya Shah\", action: \"mentioned you in\", target: \"Launch checklist\", preview: \"@Jordan can you approve the announcement email before noon?\", day: \"Today\", time: \"09:12\", unread: true },\n  { id: \"n3\", kind: \"comment\", actor: \"Alex Kim\", action: \"commented on\", target: \"Release notes 2.5\", preview: \"Screenshots still need the new sidebar. I’ll add them after lunch.\", day: \"Today\", time: \"08:30\", unread: true },\n  { id: \"n4\", kind: \"invite\", actor: \"Mara Ellison\", action: \"invited you to join\", target: \"Design systems\", day: \"Yesterday\", time: \"17:20\", actions: { primary: \"Accept\", secondary: \"Decline\" } },\n  { id: \"n5\", kind: \"system\", action: \"Your data export is ready to download\", preview: \"142 MB · available for 7 days\", day: \"Yesterday\", time: \"11:02\" },\n  { id: \"n6\", kind: \"mention\", actor: \"Sam Okafor\", action: \"mentioned you in\", target: \"Support macros\", preview: \"@Jordan this one needs your sign-off too.\", day: \"Yesterday\", time: \"10:15\" },\n  { id: \"n7\", kind: \"billing\", action: \"Your invoice for October is ready\", preview: \"$296.10 · paid with Visa ending 4242\", day: \"Earlier this week\", time: \"Mon\", },\n  { id: \"n8\", kind: \"comment\", actor: \"Theo Nakamura\", action: \"replied in\", target: \"Webhook retries\", preview: \"Moving the backoff to the queue makes this much simpler.\", day: \"Earlier this week\", time: \"Mon\" },\n]\n\nconst initials = (name: string) => name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n\nfunction InboxSkeleton() {\n  return (\n    <div className=\"grid gap-3\" aria-hidden=\"true\">\n      {[0, 1, 2, 3, 4].map((i) => (\n        <div key={i} className=\"flex gap-4 rounded-xl border p-4\">\n          <Skeleton className=\"size-10 shrink-0 rounded-full\" />\n          <div className=\"grid flex-1 gap-2\">\n            <Skeleton className=\"h-4 w-3/5\" />\n            <Skeleton className=\"h-3 w-4/5\" />\n          </div>\n          <Skeleton className=\"h-3 w-10\" />\n        </div>\n      ))}\n    </div>\n  )\n}\n\n/**\n * A notifications page: grouped by day with unread marked by an accent and a dot, mentions and comments with their excerpts, requests\n * you can answer without leaving (Approve, Deny, Accept, Decline), keyboard triage (J and K, E to archive, U for read or unread), and\n * an Undo bar after archiving.\n */\nfunction Inbox1({\n  items: itemsProp = sample,\n  title = \"Notifications\",\n  onOpen,\n  onMarkRead,\n  onArchive,\n  onRestore,\n  onMarkAllRead,\n  onAction,\n  onSettings,\n  defaultTab = \"all\",\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Inbox1Props) {\n  const id = React.useId()\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const [items, setItems] = React.useState(itemsProp)\n  React.useEffect(() => setItems(itemsProp), [itemsProp])\n  const [tab, setTab] = React.useState<\"all\" | \"unread\" | \"mentions\">(defaultTab)\n  const [undo, setUndo] = React.useState<{ item: Inbox1Item; index: number } | null>(null)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const ready = state === \"ready\" || state === \"empty\"\n  const unreadCount = items.filter((i) => i.unread).length\n\n  React.useEffect(() => {\n    if (!undo) return\n    const t = setTimeout(() => setUndo(null), 7000)\n    return () => clearTimeout(t)\n  }, [undo])\n\n  const visible = state === \"empty\" ? [] : items.filter((i) => (tab === \"all\" ? true : tab === \"unread\" ? i.unread : i.kind === \"mention\"))\n  const days = visible.reduce<{ day: string; items: Inbox1Item[] }[]>((acc, i) => {\n    const last = acc[acc.length - 1]\n    if (last && last.day === i.day) last.items.push(i)\n    else acc.push({ day: i.day, items: [i] })\n    return acc\n  }, [])\n\n  const setRead = (item: Inbox1Item, read: boolean) => {\n    setItems((l) => l.map((x) => (x.id === item.id ? { ...x, unread: !read } : x)))\n    onMarkRead?.(item.id, read)\n    setAnnounce(read ? \"Marked as read\" : \"Marked as unread\")\n  }\n\n  function archive(item: Inbox1Item) {\n    const index = items.findIndex((x) => x.id === item.id)\n    // move focus to a neighbour so keyboard triage can carry on\n    const mains = [...(listRef.current?.querySelectorAll<HTMLElement>(\"[data-inbox-main]\") ?? [])]\n    const at = mains.findIndex((m) => m.dataset.inboxMain === item.id)\n    const next = mains[at + 1] ?? mains[at - 1]\n    setItems((l) => l.filter((x) => x.id !== item.id))\n    onArchive?.(item.id)\n    setUndo({ item, index })\n    setAnnounce(\"Archived. Undo is available for a few seconds.\")\n    requestAnimationFrame(() => next?.focus())\n  }\n\n  function restore() {\n    if (!undo) return\n    const { item, index } = undo\n    setItems((l) => [...l.slice(0, index), item, ...l.slice(index)])\n    onRestore?.(item)\n    setUndo(null)\n    setAnnounce(\"Restored\")\n  }\n\n  function onKeyDown(e: React.KeyboardEvent) {\n    if (e.metaKey || e.ctrlKey || e.altKey) return\n    const target = e.target as HTMLElement\n    const key = e.key.toLowerCase()\n    const mains = [...(listRef.current?.querySelectorAll<HTMLElement>(\"[data-inbox-main]\") ?? [])]\n    const at = mains.findIndex((m) => m.dataset.inboxMain === (target.closest(\"[data-inbox-row]\") as HTMLElement | null)?.dataset.inboxRow)\n    if (key === \"j\" || key === \"k\" || key === \"arrowdown\" || key === \"arrowup\") {\n      if (at === -1 && target.tagName === \"BUTTON\" && !target.hasAttribute(\"data-inbox-main\")) return\n      e.preventDefault()\n      const delta = key === \"j\" || key === \"arrowdown\" ? 1 : -1\n      mains[Math.max(0, Math.min(mains.length - 1, (at === -1 ? 0 : at) + delta))]?.focus()\n      return\n    }\n    const row = target.closest<HTMLElement>(\"[data-inbox-row]\")\n    const item = items.find((i) => i.id === row?.dataset.inboxRow)\n    if (!item) return\n    if (key === \"e\") {\n      e.preventDefault()\n      archive(item)\n    } else if (key === \"u\") {\n      e.preventDefault()\n      setRead(item, !!item.unread)\n    }\n  }\n\n  const empty = ready && visible.length === 0\n  const emptyCopy =\n    tab === \"unread\"\n      ? { title: \"No unread notifications\", description: \"You have read everything. New activity will show up here.\" }\n      : tab === \"mentions\"\n        ? { title: \"Nobody has mentioned you\", description: \"When someone @mentions you, it lands here first.\" }\n        : { title: \"You’re all caught up\", description: \"Nothing new right now. Enjoy the quiet.\" }\n\n  const content = (\n    <>\n      <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n        <SegmentedControl value={tab} onValueChange={(v) => setTab(v as typeof tab)} aria-label=\"Show\" size=\"sm\" disabled={!ready}>\n          <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n          <SegmentedControlItem value=\"unread\">Unread{ready && unreadCount ? ` ${unreadCount}` : \"\"}</SegmentedControlItem>\n          <SegmentedControlItem value=\"mentions\">Mentions</SegmentedControlItem>\n        </SegmentedControl>\n        <p className=\"text-muted-foreground ms-auto hidden items-center gap-1.5 text-xs md:flex\">\n          <Kbd>J</Kbd>\n          <Kbd>K</Kbd> move <span aria-hidden=\"true\">·</span> <Kbd>E</Kbd> archive <span aria-hidden=\"true\">·</span> <Kbd>U</Kbd> read\n        </p>\n      </div>\n\n      {empty ? (\n        <div className=\"bg-card rounded-2xl border px-6 py-16 text-center\">\n          <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n            {tab === \"all\" ? <BellOff className=\"size-6\" /> : <CheckCheck className=\"size-6\" />}\n          </span>\n          <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? emptyCopy.title}</h2>\n          <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty\">{emptyState?.description ?? emptyCopy.description}</p>\n          {emptyState?.action}\n        </div>\n      ) : (\n        <div ref={listRef} onKeyDown={onKeyDown} className=\"grid gap-8\">\n          {days.map((d) => (\n            <section key={d.day} aria-labelledby={`${id}-${d.day}`}>\n              <h2 id={`${id}-${d.day}`} className=\"text-muted-foreground mb-3 text-xs font-medium\">\n                {d.day}\n              </h2>\n              <BlurFadeGroup role=\"list\" itemAs=\"li\" stagger={0.04} item={{ direction: \"up\", offset: 6, blur: 3 }} className=\"grid list-none grid-cols-1 gap-2\">\n                {d.items.map((n) => {\n                  const K = kinds[n.kind]\n                  const human = !!n.actor && (n.kind === \"mention\" || n.kind === \"comment\" || n.kind === \"invite\")\n                  return (\n                    <div\n                      key={n.id}\n                      data-inbox-row={n.id}\n                      className={cn(\n                        \"group/row focus-within:ring-ring/50 relative flex gap-4 rounded-xl border p-4 transition-colors duration-200 focus-within:ring-[3px]\",\n                        n.unread ? \"bg-card border-foreground/15\" : \"bg-card/50 hover:bg-card\",\n                        \"hover:border-foreground/25\"\n                      )}\n                    >\n                      {n.unread ? <span aria-hidden=\"true\" className=\"bg-chart-1 absolute inset-y-4 -start-px w-[3px] rounded-full\" /> : null}\n                      <span aria-hidden=\"true\" className={cn(\"flex size-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold [&_svg]:size-[1.15rem]\", K.tile)}>\n                        {human ? initials(n.actor!) : <K.icon />}\n                      </span>\n                      <div className=\"min-w-0 flex-1\">\n                        <p className=\"text-sm leading-relaxed text-pretty\">\n                          <button\n                            type=\"button\"\n                            data-inbox-main={n.id}\n                            onClick={() => {\n                              if (n.unread) setRead(n, true)\n                              onOpen?.(n)\n                            }}\n                            className=\"rounded-sm text-start outline-none after:absolute after:inset-0 after:content-['']\"\n                          >\n                            {n.unread ? <><span className=\"sr-only\">Unread.</span>{\" \"}</> : null}\n                            <span className=\"sr-only\">{K.label}: </span>\n                            {n.actor ? (\n                              <>\n                                <strong className=\"font-semibold\">{n.actor}</strong>{\" \"}\n                              </>\n                            ) : null}\n                            <span className={cn(n.actor ? \"text-muted-foreground\" : \"font-medium\")}>{n.action}</span>\n                            {n.target ? (\n                              <>\n                                {\" \"}\n                                <strong className=\"font-semibold\">{n.target}</strong>\n                              </>\n                            ) : null}\n                          </button>\n                        </p>\n                        {n.preview ? (\n                          <p className={cn(\"text-muted-foreground mt-1.5 line-clamp-2 text-[13px] text-pretty\", n.kind !== \"system\" && n.kind !== \"billing\" && \"border-border border-s-2 ps-3\")}>{n.preview}</p>\n                        ) : null}\n                        {n.actions && !n.outcome ? (\n                          <div className=\"relative z-10 mt-3 flex flex-wrap gap-2\">\n                            <Button\n                              size=\"sm\"\n                              onClick={() => {\n                                setItems((l) => l.map((x) => (x.id === n.id ? { ...x, outcome: n.actions!.primary === \"Approve\" ? \"Approved\" : n.actions!.primary === \"Accept\" ? \"Accepted\" : n.actions!.primary, unread: false } : x)))\n                                onAction?.(n.id, \"primary\")\n                                setAnnounce(`${n.actions!.primary} confirmed`)\n                              }}\n                            >\n                              {n.actions.primary}\n                            </Button>\n                            {n.actions.secondary ? (\n                              <Button\n                                variant=\"outline\"\n                                size=\"sm\"\n                                onClick={() => {\n                                  setItems((l) => l.map((x) => (x.id === n.id ? { ...x, outcome: n.actions!.secondary === \"Deny\" ? \"Denied\" : n.actions!.secondary === \"Decline\" ? \"Declined\" : n.actions!.secondary, unread: false } : x)))\n                                  onAction?.(n.id, \"secondary\")\n                                  setAnnounce(`${n.actions!.secondary} confirmed`)\n                                }}\n                              >\n                                {n.actions.secondary}\n                              </Button>\n                            ) : null}\n                          </div>\n                        ) : null}\n                        {n.outcome ? (\n                          <p className=\"mt-2.5 flex items-center gap-1.5 text-xs font-medium\">\n                            <Check aria-hidden=\"true\" className=\"size-3.5\" />\n                            {n.outcome}\n                          </p>\n                        ) : null}\n                      </div>\n                      <div className=\"flex shrink-0 flex-col items-end gap-2\">\n                        <span className=\"text-muted-foreground text-xs tabular-nums\">{n.time}</span>\n                        <span className=\"relative z-10 flex gap-0.5 opacity-100 transition-opacity sm:opacity-0 sm:group-focus-within/row:opacity-100 sm:group-hover/row:opacity-100\">\n                          <Button variant=\"ghost\" size=\"icon-sm\" aria-label={n.unread ? \"Mark as read\" : \"Mark as unread\"} aria-pressed={!n.unread} onClick={() => setRead(n, !!n.unread)}>\n                            {n.unread ? <Check aria-hidden=\"true\" /> : <CircleDot aria-hidden=\"true\" />}\n                          </Button>\n                          <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Archive\" onClick={() => archive(n)}>\n                            <Archive aria-hidden=\"true\" />\n                          </Button>\n                        </span>\n                      </div>\n                    </div>\n                  )\n                })}\n              </BlurFadeGroup>\n            </section>\n          ))}\n        </div>\n      )}\n    </>\n  )\n\n  return (\n    <section data-slot=\"inbox-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-3xl px-4 py-8 pb-24 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-6 flex flex-wrap items-center gap-3\">\n        <h1 id={`${id}-title`} className=\"flex items-center gap-3 text-3xl font-semibold tracking-[-0.035em]\">\n          {title}\n          {ready && unreadCount ? (\n            <span className=\"bg-primary text-primary-foreground rounded-full px-2.5 py-0.5 text-xs font-semibold tabular-nums\">\n              {unreadCount}\n              {\" \"}<span className=\"sr-only\">unread</span>\n            </span>\n          ) : null}\n        </h1>\n        <div className=\"ms-auto flex items-center gap-2\">\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            disabled={!ready || !unreadCount}\n            onClick={() => {\n              setItems((l) => l.map((x) => ({ ...x, unread: false })))\n              onMarkAllRead?.()\n              setAnnounce(\"All marked as read\")\n            }}\n          >\n            <CheckCheck aria-hidden=\"true\" />\n            Mark all as read\n          </Button>\n          {onSettings ? (\n            <Button variant=\"ghost\" size=\"icon\" aria-label=\"Notification settings\" onClick={onSettings}>\n              <Settings aria-hidden=\"true\" />\n            </Button>\n          ) : null}\n        </div>\n      </header>\n\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading notifications\" loading={<InboxSkeleton />} error={{ title: errorState?.title ?? \"Notifications could not be loaded\", description: errorState?.description ?? \"Nothing was lost. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n\n      {undo ? (\n        <div role=\"status\" className=\"bg-popover text-popover-foreground fixed inset-x-4 bottom-4 z-30 mx-auto flex max-w-md items-center gap-3 rounded-xl border p-3 ps-4 shadow-lg\">\n          <p className=\"min-w-0 flex-1 truncate text-sm\">Archived “{undo.item.target ?? undo.item.action}”</p>\n          <Button variant=\"outline\" size=\"sm\" onClick={restore}>\n            <Undo2 aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />\n            Undo\n          </Button>\n        </div>\n      ) : null}\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Inbox1, type Inbox1Props, type Inbox1Item, type Inbox1Kind }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/inbox-1/inbox-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "notifications",
      "inbox",
      "activity",
      "alerts",
      "mentions",
      "unread",
      "archive",
      "approvals",
      "triage",
      "keyboard"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "A notifications page. <Inbox1 items={[{ id, kind: 'mention' | 'comment' | 'approval' | 'invite' | 'system' | 'billing', actor, action, target, preview, day, time, unread, actions: { primary, secondary }, outcome }]} onOpen onMarkRead onArchive onRestore onMarkAllRead onAction onSettings />. Items render as \"<b>actor</b> action <b>target</b>\"; give a system item only an action. Answering an inline request shows its outcome at once.",
      "whenToUse": [
        "The full page behind a notification bell",
        "Mentions, comments and requests that people answer without opening each one"
      ],
      "whenNotToUse": [
        "A dropdown from a bell (use notification-center)",
        "Email-style threads (use mail-1)",
        "Notification preferences (use settings-2)"
      ],
      "composesWith": [
        "app-shell-1",
        "settings-2",
        "agent-2",
        "chat-1"
      ],
      "a11y": [
        {
          "keys": "J / K or Down / Up",
          "action": "Moves between notifications; each is one button whose name starts with Unread and its kind"
        },
        {
          "keys": "E / U on a notification",
          "action": "Archives it (with Undo) / marks it read or unread; every change is announced politely"
        },
        {
          "keys": "Tab",
          "action": "Inline Approve and Deny sit after the notification; hover actions appear on focus and are always visible on touch screens"
        }
      ],
      "customization": [
        "title; defaultTab: 'all' | 'unread' | 'mentions'",
        "day groups items in the order you pass them, so sort newest first",
        "Wire onAction to your approval or invitation API; the outcome text is Approved, Denied, Accepted or Declined"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "inbox-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton notifications under the real heading, announced as loading; the filter and Mark all as read are disabled."
      },
      {
        "id": "empty",
        "title": "All caught up",
        "example": "inbox-1-empty",
        "trigger": "state=\"empty\" (or nothing in the chosen view)",
        "note": "A friendly message that changes with the view (all, unread, mentions); emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "inbox-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was lost, with Try again (onRetry)."
      }
    ],
    "examples": [
      "inbox-1-demo",
      "inbox-1-mentions",
      "inbox-1-empty",
      "inbox-1-loading",
      "inbox-1-error"
    ],
    "url": "https://ui.ballmac.com/components/inbox-1"
  },
  "categories": [
    "blocks",
    "notifications"
  ],
  "type": "registry:block"
}