{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-inbox",
  "title": "Agent Inbox",
  "description": "A human-in-the-loop queue of everything your agents parked on a person — triage ranking, overdue flags, one-shot inline Approve/Deny, grouping by agent or urgency, and four data states.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/agent-inbox.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowDown,\n  ArrowUp,\n  ArrowUpRight,\n  Ban,\n  Check,\n  ChevronRight,\n  CircleAlert,\n  CircleCheck,\n  Filter,\n  Hourglass,\n  Inbox,\n  LoaderCircle,\n  MessageCircleQuestionMark,\n  Minus,\n  RefreshCcw,\n  ScanEye,\n  ShieldAlert,\n  TimerOff,\n  TriangleAlert,\n  X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type {\n  AgentInboxDecision,\n  AgentInboxInstant,\n  AgentInboxItem,\n  AgentInboxItemState,\n  AgentInboxKind,\n  AgentInboxStatus,\n  AgentInboxUrgency,\n} from \"./agent-inbox.contract\"\n\n/** How rows are bucketed. A view concern, not data — the same queue regroups without refetching. */\nexport type AgentInboxGroupBy = \"none\" | \"agent\" | \"urgency\"\n\n/**\n * `triage` = what will hurt if ignored (waiting first, urgency down, oldest\n * first). `newest` = arrival order. `none` = your data layer already sorted and\n * the component must not second-guess it.\n */\nexport type AgentInboxSort = \"triage\" | \"newest\" | \"none\"\n\nexport type AgentInboxDensity = \"comfortable\" | \"compact\"\n\n/** Which quick actions a row is allowed to grow. Derived from the kind, never from urgency. */\ntype AgentInboxAction = \"approve\" | \"deny\" | \"answer\"\n\n/**\n * The one focus registry callback shared by every cell. Each element carries its\n * own `data-inbox-cell=\"<id>::<col>\"`, so a single stable function can index the\n * whole grid and tear each entry down through the cleanup React 19 calls.\n */\ntype CellRegistrar = (node: HTMLElement | null) => (() => void) | undefined\n\nconst KIND_LABEL: Record<AgentInboxKind, string> = {\n  approval: \"Approval\",\n  question: \"Question\",\n  review: \"Review\",\n}\n\nconst URGENCY_RANK: Record<AgentInboxUrgency, number> = { critical: 3, high: 2, low: 0, normal: 1 }\n\nconst URGENCY_LABEL: Record<AgentInboxUrgency, string> = {\n  critical: \"Critical\",\n  high: \"High\",\n  low: \"Low\",\n  normal: \"Normal\",\n}\n\n/**\n * Urgency is a shape + a word first and a hue second: an arrow that points up is\n * still readable in a monochrome theme, printed, or by a reader who cannot tell\n * the destructive token from the foreground one.\n */\nconst URGENCY_CLASS: Record<AgentInboxUrgency, string> = {\n  critical: \"border-destructive/50 text-destructive\",\n  high: \"border-border text-foreground\",\n  low: \"border-transparent bg-muted text-muted-foreground\",\n  normal: \"border-border text-muted-foreground\",\n}\n\nconst STATE_WORD: Record<AgentInboxItemState, string> = {\n  answered: \"Answered\",\n  approved: \"Approved\",\n  denied: \"Denied\",\n  expired: \"Expired\",\n  pending: \"Waiting\",\n}\n\n/** What a screen reader hears for a row, spelled out instead of implied by a colour. */\nconst STATE_SENTENCE: Record<AgentInboxItemState, string> = {\n  answered: \"answered\",\n  approved: \"approved by you\",\n  denied: \"denied by you\",\n  expired: \"expired without an answer\",\n  pending: \"waiting for you\",\n}\n\nconst GROUP_OPTIONS: { label: string; value: AgentInboxGroupBy }[] = [\n  { label: \"Flat\", value: \"none\" },\n  { label: \"Agent\", value: \"agent\" },\n  { label: \"Urgency\", value: \"urgency\" },\n]\n\nconst ACTION_BASE =\n  \"inline-flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n\nfunction toEpoch(value: AgentInboxInstant): number {\n  if (typeof value === \"number\") return value\n  // Date.parse, not `new Date(...)`: a pure call, safe to run during render.\n  if (typeof value === \"string\") return Date.parse(value)\n  return value.getTime()\n}\n\nfunction epochOf(item: AgentInboxItem): number {\n  const parsed = Date.parse(item.createdAt)\n  // An unparseable instant sorts as \"infinitely new\" instead of poisoning the\n  // comparator with NaN, which would leave the whole queue in an arbitrary order.\n  return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY\n}\n\n/** Never subtracts, so two Infinity values compare as equal instead of producing NaN. */\nfunction compareEpoch(a: number, b: number): number {\n  if (a === b) return 0\n  return a < b ? -1 : 1\n}\n\n/**\n * Ages, not timestamps: \"waiting 41m\" is the number that makes someone act, and\n * a queue is read by comparing those numbers to each other.\n */\nfunction defaultFormatAge(ms: number): string {\n  if (!Number.isFinite(ms) || ms < 0) return \"0s\"\n  const seconds = Math.floor(ms / 1000)\n  if (seconds < 60) return `${seconds}s`\n  const minutes = Math.floor(seconds / 60)\n  if (minutes < 60) return `${minutes}m`\n  const hours = Math.floor(minutes / 60)\n  if (hours < 24) return `${hours}h`\n  const days = Math.floor(hours / 24)\n  if (days < 7) return `${days}d`\n  return `${Math.floor(days / 7)}w`\n}\n\nfunction sortItems(items: AgentInboxItem[], sort: AgentInboxSort): AgentInboxItem[] {\n  const next = items.slice()\n  if (sort === \"none\") return next\n  if (sort === \"newest\") {\n    next.sort((a, b) => compareEpoch(epochOf(b), epochOf(a)))\n    return next\n  }\n  next.sort((a, b) => {\n    const aWaiting = a.state === \"pending\" ? 0 : 1\n    const bWaiting = b.state === \"pending\" ? 0 : 1\n    if (aWaiting !== bWaiting) return aWaiting - bWaiting\n    const rank = URGENCY_RANK[b.urgency] - URGENCY_RANK[a.urgency]\n    if (rank !== 0) return rank\n    // Waiting rows: OLDEST first — the one ignored longest is the one about to\n    // expire. Settled rows: NEWEST first — \"what did I just decide\".\n    return aWaiting === 0 ? compareEpoch(epochOf(a), epochOf(b)) : compareEpoch(epochOf(b), epochOf(a))\n  })\n  return next\n}\n\ninterface AgentInboxGroup {\n  key: string\n  label: string\n  items: AgentInboxItem[]\n  waiting: number\n}\n\n/**\n * Groups are built in FIRST-APPEARANCE order over the already-sorted list, so\n * they inherit the sort instead of fighting it: under `sort=\"triage\"` the agent\n * holding the most urgent request leads, under `sort=\"newest\"` the most recent\n * one does. One rule, no surprises.\n */\nfunction buildGroups(items: AgentInboxItem[], groupBy: AgentInboxGroupBy): AgentInboxGroup[] {\n  const groups: AgentInboxGroup[] = []\n  const index = new Map<string, number>()\n  for (const item of items) {\n    const key = groupBy === \"agent\" ? `agent:${item.agentName}` : groupBy === \"urgency\" ? `urgency:${item.urgency}` : \"all\"\n    let at = index.get(key)\n    if (at === undefined) {\n      at = groups.length\n      index.set(key, at)\n      groups.push({\n        items: [],\n        key,\n        label: groupBy === \"agent\" ? item.agentName : `${URGENCY_LABEL[item.urgency]} urgency`,\n        waiting: 0,\n      })\n    }\n    groups[at].items.push(item)\n    if (item.state === \"pending\") groups[at].waiting += 1\n  }\n  return groups\n}\n\n/* -------------------------------------------------------------------- pieces */\n\nfunction KindMark({ kind }: { kind: AgentInboxKind }) {\n  const Icon = kind === \"approval\" ? ShieldAlert : kind === \"question\" ? MessageCircleQuestionMark : ScanEye\n  return <Icon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n}\n\nfunction UrgencyBadge({ urgency }: { urgency: AgentInboxUrgency }) {\n  const Icon = urgency === \"critical\" ? TriangleAlert : urgency === \"high\" ? ArrowUp : urgency === \"low\" ? ArrowDown : Minus\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center gap-1 rounded-full border px-1.5 py-px text-[11px] leading-4\",\n        URGENCY_CLASS[urgency],\n      )}\n      data-urgency={urgency}\n    >\n      <Icon aria-hidden=\"true\" className=\"size-3\" />\n      {URGENCY_LABEL[urgency]}\n    </span>\n  )\n}\n\n/** The right-hand word for a row that has no buttons: a settled outcome, or a gate nobody wired. */\nfunction OutcomeText({ state }: { state: AgentInboxItemState }) {\n  const Icon =\n    state === \"approved\"\n      ? CircleCheck\n      : state === \"denied\"\n        ? Ban\n        : state === \"answered\"\n          ? Check\n          : state === \"expired\"\n            ? TimerOff\n            : Hourglass\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1.5 whitespace-nowrap text-xs\",\n        state === \"expired\" ? \"text-destructive\" : \"text-muted-foreground\",\n      )}\n      data-outcome={state}\n    >\n      <Icon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n      {STATE_WORD[state]}\n    </span>\n  )\n}\n\n/**\n * One quick action, living in its own grid cell.\n *\n * It is a real component rather than a closure the row builds, so the focus\n * registry it receives is a single stable callback for the whole inbox instead\n * of one function per button per render.\n */\nfunction RowAction({\n  action,\n  answered,\n  cellKey,\n  decision,\n  label,\n  onAnswer,\n  onDecide,\n  onFocusCell,\n  registerCell,\n  tabIndex,\n}: {\n  action: AgentInboxAction\n  answered: boolean\n  cellKey: string\n  decision: AgentInboxDecision | undefined\n  label: string\n  onAnswer: () => void\n  onDecide: (decision: AgentInboxDecision) => void\n  onFocusCell: () => void\n  registerCell: CellRegistrar\n  tabIndex: number\n}) {\n  if (action === \"answer\") {\n    return (\n      <button\n        className={cn(ACTION_BASE, \"border hover:bg-muted\")}\n        data-action=\"answer\"\n        data-inbox-cell={cellKey}\n        onClick={onAnswer}\n        onFocus={onFocusCell}\n        ref={registerCell}\n        tabIndex={tabIndex}\n        type=\"button\"\n      >\n        <ArrowUpRight aria-hidden=\"true\" className=\"size-3.5\" />\n        {label}\n      </button>\n    )\n  }\n\n  const approving = action === \"approve\"\n  const chosen = decision === action\n\n  return (\n    <button\n      // aria-disabled, never the native attribute: `disabled` blurs the button\n      // the instant it flips, dropping focus to <body> in the middle of the\n      // decision and taking the element out of the tab order entirely.\n      aria-disabled={answered || undefined}\n      className={cn(\n        ACTION_BASE,\n        approving\n          ? \"bg-primary text-primary-foreground hover:bg-primary/90\"\n          : \"border hover:border-destructive/50 hover:bg-muted hover:text-destructive\",\n        answered && \"cursor-default opacity-60\",\n        answered && approving && \"hover:bg-primary\",\n        answered && !approving && \"hover:border-border hover:bg-transparent hover:text-foreground\",\n      )}\n      data-action={action}\n      data-inbox-cell={cellKey}\n      onClick={() => onDecide(approving ? \"approve\" : \"deny\")}\n      onFocus={onFocusCell}\n      ref={registerCell}\n      tabIndex={tabIndex}\n      type=\"button\"\n    >\n      {chosen ? (\n        <LoaderCircle aria-hidden=\"true\" className=\"size-3.5 animate-spin motion-reduce:animate-none\" />\n      ) : approving ? (\n        <Check aria-hidden=\"true\" className=\"size-3.5\" />\n      ) : (\n        <X aria-hidden=\"true\" className=\"size-3.5\" />\n      )}\n      {chosen ? (approving ? \"Approving…\" : \"Denying…\") : label}\n    </button>\n  )\n}\n\n/**\n * A hand-rolled radio group (one tab stop, arrows move AND select), because the\n * grouping axis is a single choice out of three — a set of independent toggles\n * would let a reader ask for two groupings at once.\n */\nfunction GroupControl({\n  onChange,\n  value,\n}: {\n  onChange: (value: AgentInboxGroupBy) => void\n  value: AgentInboxGroupBy\n}) {\n  const refs = React.useRef(new Map<AgentInboxGroupBy, HTMLButtonElement>())\n\n  const move = (delta: number) => {\n    const index = GROUP_OPTIONS.findIndex(option => option.value === value)\n    const next = GROUP_OPTIONS[(index + delta + GROUP_OPTIONS.length) % GROUP_OPTIONS.length]\n    onChange(next.value)\n    refs.current.get(next.value)?.focus()\n  }\n\n  return (\n    <div aria-label=\"Group requests by\" className=\"inline-flex items-center gap-0.5 rounded-md border p-0.5\" role=\"radiogroup\">\n      {GROUP_OPTIONS.map(option => {\n        const checked = option.value === value\n        return (\n          <button\n            aria-checked={checked}\n            className={cn(\n              \"cursor-pointer rounded-sm px-2 py-0.5 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n              checked ? \"bg-primary text-primary-foreground\" : \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n            )}\n            key={option.value}\n            onClick={() => onChange(option.value)}\n            onKeyDown={event => {\n              if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n                event.preventDefault()\n                move(1)\n              } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n                event.preventDefault()\n                move(-1)\n              }\n            }}\n            ref={node => {\n              if (node) refs.current.set(option.value, node)\n              else refs.current.delete(option.value)\n            }}\n            role=\"radio\"\n            tabIndex={checked ? 0 : -1}\n            type=\"button\"\n          >\n            {option.label}\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* ---------------------------------------------------------------- component */\n\nexport interface AgentInboxProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** The queue. Empty is legal in every status; `status=\"ready\"` with zero rows renders the empty body. */\n  items: AgentInboxItem[]\n  /** Envelope state — whether there is a queue at all. Not a row's own `state`. */\n  status: AgentInboxStatus\n  /**\n   * \"Now\", injected. Every age on screen is `now - createdAt`, recomputed from\n   * the instant, never accumulated. Omit it and rows simply show no age — the\n   * component owns no clock, which is what keeps SSR, a replay and a screenshot\n   * byte-identical. Tick it once per minute in your app if you want ages to run.\n   */\n  now?: AgentInboxInstant\n  /** Controlled grouping axis. Omit to let the header control own it. */\n  groupBy?: AgentInboxGroupBy\n  /** Starting grouping axis when uncontrolled. Default: \"none\". */\n  defaultGroupBy?: AgentInboxGroupBy\n  /** Fires on every grouping change, controlled or not — persist it per user if you like. */\n  onGroupByChange?: (groupBy: AgentInboxGroupBy) => void\n  /** Start with settled rows filtered out. The header toggle flips it. Default: false. */\n  defaultPendingOnly?: boolean\n  /** Ranking rule. Default: \"triage\". */\n  sort?: AgentInboxSort\n  /** \"compact\" drops the preview line and tightens the rows; the impact line always survives. */\n  density?: AgentInboxDensity\n  /** A waiting row older than this is called out as overdue. Default: 30 minutes. */\n  staleAfterMs?: number\n  /** Called at most ONCE per waiting request, no matter how fast the button is clicked. */\n  onDecide?: (id: string, decision: AgentInboxDecision) => void\n  /** Row click-through. Fires on every activation — the component never mutates your `unread`. */\n  onOpen?: (id: string) => void\n  /** Inline detail slot. Supplying it turns each row into a disclosure; omit it to route away instead. */\n  renderDetail?: (item: AgentInboxItem) => React.ReactNode\n  /** Renders \"Try again\" in the `status=\"error\"` branch; omit it to hide the affordance. */\n  onRetry?: () => void\n  approveLabel?: string\n  denyLabel?: string\n  answerLabel?: string\n  /** Override the age wording (\"41m\", \"41 minutes ago\", a relative-time formatter). */\n  formatAge?: (ms: number) => string\n  /** Replaces the default `status=\"empty\"` body. */\n  emptyState?: React.ReactNode\n  /** Message shown in the `status=\"error\"` branch. */\n  errorMessage?: string\n  /** Accessible name and visible title of the panel. */\n  label?: string\n  /** Hide the grouping / filter header controls (a read-only dashboard tile). Default: shown. */\n  showControls?: boolean\n}\n\n/**\n * A human-in-the-loop inbox: everything a fleet of agents has parked on a\n * person — approvals, questions, reviews — ranked by what will hurt if ignored,\n * with one-shot Approve / Deny quick actions on the row itself.\n */\nexport const AgentInbox = React.forwardRef<HTMLElement, AgentInboxProps>(\n  (\n    {\n      items,\n      status,\n      now,\n      groupBy: groupByProp,\n      defaultGroupBy = \"none\",\n      onGroupByChange,\n      defaultPendingOnly = false,\n      sort = \"triage\",\n      density = \"comfortable\",\n      staleAfterMs = 1_800_000,\n      onDecide,\n      onOpen,\n      renderDetail,\n      onRetry,\n      approveLabel = \"Approve\",\n      denyLabel = \"Deny\",\n      answerLabel = \"Answer\",\n      formatAge = defaultFormatAge,\n      emptyState,\n      errorMessage = \"This inbox couldn't be loaded.\",\n      label = \"Agent inbox\",\n      showControls = true,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const uid = React.useId()\n\n    const [internalGroupBy, setInternalGroupBy] = React.useState<AgentInboxGroupBy>(defaultGroupBy)\n    const [pendingOnly, setPendingOnly] = React.useState(defaultPendingOnly)\n    const [openId, setOpenId] = React.useState<string | null>(null)\n    const [focus, setFocus] = React.useState<{ id: string; col: number }>({ col: 0, id: \"\" })\n    /**\n     * Generation of the decision gate. It ticks whenever the SET of waiting\n     * requests changes, which is what makes an answered row expire by itself —\n     * no reset effect, no ref written during render.\n     */\n    const [gate, setGate] = React.useState(0)\n    const [decisions, setDecisions] = React.useState<Record<string, AgentInboxDecision>>({})\n    const [lastDecided, setLastDecided] = React.useState<{ decision: AgentInboxDecision; id: string; title: string } | null>(\n      null,\n    )\n\n    /** Focus targets by `${id}::${col}`. Written only by ref callbacks, read only in handlers. */\n    const cellRefs = React.useRef(new Map<string, HTMLElement>())\n    /**\n     * The one-shot lock. It lives in a REF, not in state: five clicks dispatched\n     * inside a single task all read the same stale state, so a state-only guard\n     * would let four of them through and approve a production deploy five times.\n     */\n    const lockRef = React.useRef(new Map<string, number>())\n    /** The row a decision was just taken on, so focus can come back to it when its buttons disappear. */\n    const restoreRef = React.useRef<string | null>(null)\n\n    const waitingSignature = items\n      .filter(item => item.state === \"pending\")\n      .map(item => item.id)\n      .join(\"|\")\n    const [prevSignature, setPrevSignature] = React.useState(waitingSignature)\n\n    // Adjust state during render (no effect, so there is never a frame where a\n    // settled row still shows \"Approving…\").\n    if (prevSignature !== waitingSignature) {\n      const stillWaiting = new Set(waitingSignature.split(\"|\").filter(Boolean))\n      setPrevSignature(waitingSignature)\n      setGate(value => value + 1)\n      setDecisions(current => {\n        const next: Record<string, AgentInboxDecision> = {}\n        let dropped = false\n        for (const [id, decision] of Object.entries(current)) {\n          if (stillWaiting.has(id)) next[id] = decision\n          else dropped = true\n        }\n        return dropped ? next : current\n      })\n      setLastDecided(current => (current !== null && stillWaiting.has(current.id) ? current : null))\n    }\n\n    const groupBy = groupByProp ?? internalGroupBy\n    const changeGroupBy = (next: AgentInboxGroupBy) => {\n      if (groupByProp === undefined) setInternalGroupBy(next)\n      onGroupByChange?.(next)\n    }\n\n    const staleMs = Number.isFinite(staleAfterMs) ? Math.max(0, staleAfterMs) : 1_800_000\n    const nowEpoch = now === undefined ? undefined : toEpoch(now)\n    const clock = nowEpoch !== undefined && Number.isFinite(nowEpoch) ? nowEpoch : undefined\n\n    const ordered = React.useMemo(() => sortItems(items, sort), [items, sort])\n    const visible = React.useMemo(\n      () => (pendingOnly ? ordered.filter(item => item.state === \"pending\") : ordered),\n      [ordered, pendingOnly],\n    )\n    const groups = React.useMemo(() => buildGroups(visible, groupBy), [groupBy, visible])\n    const indexById = React.useMemo(() => {\n      const map = new Map<string, number>()\n      visible.forEach((item, index) => map.set(item.id, index))\n      return map\n    }, [visible])\n\n    const canDecide = onDecide !== undefined\n    const canOpen = onOpen !== undefined || renderDetail !== undefined\n\n    const actionsFor = (item: AgentInboxItem): AgentInboxAction[] => {\n      if (item.state !== \"pending\") return []\n      // A question has no \"approve\": sending an empty yes back to an agent that\n      // asked \"which Stripe account?\" is worse than making the human open it.\n      if (item.kind === \"question\") return canOpen ? [\"answer\"] : []\n      return canDecide ? [\"approve\", \"deny\"] : []\n    }\n\n    const order = visible.map(item => item.id)\n    const activeId = order.includes(focus.id) ? focus.id : (order[0] ?? \"\")\n    const activeIndex = order.indexOf(activeId)\n    const activeItem = activeIndex >= 0 ? visible[activeIndex] : undefined\n    const activeCol =\n      focus.id === activeId && activeItem !== undefined\n        ? Math.min(Math.max(focus.col, 0), actionsFor(activeItem).length)\n        : 0\n\n    const waitingCount = items.reduce((total, item) => total + (item.state === \"pending\" ? 1 : 0), 0)\n    const overdueCount =\n      clock === undefined\n        ? 0\n        : items.reduce(\n            (total, item) =>\n              total + (item.state === \"pending\" && clock - Date.parse(item.createdAt) >= staleMs ? 1 : 0),\n            0,\n          )\n    const hiddenCount = items.length - visible.length\n    const expandedId = renderDetail !== undefined && openId !== null && order.includes(openId) ? openId : null\n\n    /**\n     * ONE stable ref callback for every cell in the grid: each element carries\n     * its own `data-inbox-cell=\"<id>::<col>\"`, so the registry needs no\n     * per-element closure created during render, and React 19 tears the entry\n     * down through the returned cleanup instead of a null call.\n     */\n    const registerCell: CellRegistrar = React.useCallback((node: HTMLElement | null) => {\n      const key = node?.dataset.inboxCell\n      if (node === null || key === undefined || key === \"\") return\n      const registry = cellRefs.current\n      registry.set(key, node)\n      return () => {\n        registry.delete(key)\n      }\n    }, [])\n\n    const focusCell = (id: string, col: number) => {\n      setFocus(current => (current.id === id && current.col === col ? current : { col, id }))\n      cellRefs.current.get(`${id}::${col}`)?.focus()\n    }\n\n    /**\n     * Answering a request destroys the button you answered it with — the row\n     * settles and its quick actions unmount, which drops focus to <body> and\n     * loses a keyboard user's place in the queue. When (and only when) that has\n     * happened, focus returns to the row itself.\n     */\n    React.useEffect(() => {\n      const id = restoreRef.current\n      if (id === null) return\n      restoreRef.current = null\n      const node = cellRefs.current.get(`${id}::0`)\n      if (node === undefined) return\n      const active = document.activeElement\n      if (active !== null && active !== document.body) return\n      setFocus({ col: 0, id })\n      node.focus()\n    }, [waitingSignature])\n\n    const decide = (item: AgentInboxItem, decision: AgentInboxDecision) => {\n      if (onDecide === undefined || item.state !== \"pending\") return\n      if (lockRef.current.get(item.id) === gate || decisions[item.id] !== undefined) return\n      // Locks from an expired generation are dead weight; drop them here rather\n      // than in render, where writing a ref is not allowed.\n      for (const [key, value] of lockRef.current) if (value !== gate) lockRef.current.delete(key)\n      lockRef.current.set(item.id, gate)\n      restoreRef.current = item.id\n      setDecisions(current => ({ ...current, [item.id]: decision }))\n      setLastDecided({ decision, id: item.id, title: item.title })\n      onDecide(item.id, decision)\n    }\n\n    const activate = (item: AgentInboxItem) => {\n      onOpen?.(item.id)\n      // Read state is YOURS. Opening a row never rewrites `unread` behind your back.\n      if (renderDetail !== undefined) setOpenId(current => (current === item.id ? null : item.id))\n    }\n\n    const handleGridKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.altKey || event.ctrlKey || event.metaKey) return\n      const target = event.target as HTMLElement | null\n      // Only OUR cells drive navigation: whatever the consumer renders in the\n      // detail slot keeps its own keys, so typing \"d\" in a textarea never denies.\n      const cell = target?.dataset?.inboxCell\n      if (cell === undefined || cell === \"\") return\n\n      // The origin is read off the EVENT TARGET, not off focus state: a click\n      // that focuses a cell and a key pressed in the same task would otherwise\n      // navigate from the previous row.\n      const separator = cell.lastIndexOf(\"::\")\n      const rowId = cell.slice(0, separator)\n      const col = Number.parseInt(cell.slice(separator + 2), 10)\n      const index = order.indexOf(rowId)\n      const item = index >= 0 ? visible[index] : undefined\n      if (item === undefined || !Number.isFinite(col)) return\n\n      const actions = actionsFor(item)\n      const jump = (nextIndex: number, nextCol: number) => {\n        const next = visible[nextIndex]\n        if (next === undefined) return\n        // Column is CLAMPED, not preserved blindly: stepping from a waiting row\n        // with two buttons onto a settled row must land on the row itself.\n        focusCell(next.id, Math.min(nextCol, actionsFor(next).length))\n      }\n\n      switch (event.key) {\n        case \"ArrowDown\":\n          event.preventDefault()\n          jump(Math.min(index + 1, visible.length - 1), col)\n          break\n        case \"ArrowUp\":\n          event.preventDefault()\n          jump(Math.max(index - 1, 0), col)\n          break\n        case \"ArrowRight\":\n          event.preventDefault()\n          focusCell(item.id, Math.min(col + 1, actions.length))\n          break\n        case \"ArrowLeft\":\n          event.preventDefault()\n          focusCell(item.id, Math.max(col - 1, 0))\n          break\n        case \"Home\":\n          event.preventDefault()\n          jump(0, 0)\n          break\n        case \"End\":\n          event.preventDefault()\n          jump(visible.length - 1, 0)\n          break\n        case \"a\":\n        case \"A\":\n          if (!actions.includes(\"approve\")) return\n          event.preventDefault()\n          decide(item, \"approve\")\n          break\n        case \"d\":\n        case \"D\":\n          if (!actions.includes(\"deny\")) return\n          event.preventDefault()\n          decide(item, \"deny\")\n          break\n        default:\n          break\n      }\n    }\n\n    /* ------------------------------------------------------------ envelopes */\n\n    // @container, not viewport breakpoints: the same inbox is a full-width page\n    // and a 320px side panel, and it is the CARD's width that decides whether\n    // Approve/Deny still fit beside the request.\n    const rootClass = cn(\n      \"@container/inbox w-full min-w-0 overflow-hidden rounded-lg border bg-card text-sm text-card-foreground\",\n      className,\n    )\n\n    if (status === \"loading\") {\n      return (\n        <section aria-busy=\"true\" aria-label={label} className={rootClass} ref={ref} {...props}>\n          <span className=\"sr-only\" role=\"status\">\n            Loading {label}\n          </span>\n          <div aria-hidden=\"true\" className=\"flex items-center justify-between gap-3 border-b p-3\">\n            <div className=\"h-3.5 w-28 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n            <div className=\"h-6 w-36 animate-pulse rounded-md bg-muted motion-reduce:animate-none\" />\n          </div>\n          <div aria-hidden=\"true\" className=\"divide-y\">\n            {[\"w-3/5\", \"w-4/5\", \"w-2/5\", \"w-3/4\"].map(width => (\n              <div className=\"flex items-start gap-3 p-3\" key={width}>\n                <div className=\"mt-0.5 size-4 shrink-0 animate-pulse rounded-full bg-muted motion-reduce:animate-none\" />\n                <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                  <div className={cn(\"h-3.5 animate-pulse rounded bg-muted motion-reduce:animate-none\", width)} />\n                  <div className=\"h-3 w-32 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n                </div>\n                <div className=\"h-7 w-24 shrink-0 animate-pulse rounded-md bg-muted motion-reduce:animate-none\" />\n              </div>\n            ))}\n          </div>\n        </section>\n      )\n    }\n\n    if (status === \"error\") {\n      return (\n        <section aria-label={label} className={rootClass} ref={ref} {...props}>\n          <div className=\"flex flex-col items-start gap-2 p-4\" role=\"alert\">\n            <p className=\"flex items-center gap-2 font-medium\">\n              <CircleAlert aria-hidden=\"true\" className=\"size-4 shrink-0 text-destructive\" />\n              This inbox failed to load\n            </p>\n            <p className=\"min-w-0 whitespace-pre-wrap wrap-anywhere text-muted-foreground\">{errorMessage}</p>\n            <p className=\"text-xs text-muted-foreground\">\n              Requests are still queued on the agent side — nothing here was approved or denied.\n            </p>\n            {onRetry && (\n              <button\n                className={cn(ACTION_BASE, \"border hover:bg-muted\")}\n                onClick={onRetry}\n                type=\"button\"\n              >\n                <RefreshCcw aria-hidden=\"true\" className=\"size-3.5\" />\n                Try again\n              </button>\n            )}\n          </div>\n        </section>\n      )\n    }\n\n    const isEmpty = status === \"empty\" || items.length === 0\n\n    if (isEmpty) {\n      return (\n        <section aria-label={label} className={rootClass} ref={ref} {...props}>\n          {emptyState ?? (\n            <div className=\"flex flex-col items-start gap-1 p-6\">\n              <p className=\"flex items-center gap-2 font-medium\">\n                <Inbox aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n                Nothing is waiting for you\n              </p>\n              <p className=\"text-xs text-muted-foreground\">\n                Your agents will park approvals, questions and reviews here when they need a person.\n              </p>\n            </div>\n          )}\n        </section>\n      )\n    }\n\n    /* ---------------------------------------------------------------- ready */\n\n    const rowCount = visible.length + (groupBy === \"none\" ? 0 : groups.length) + (expandedId === null ? 0 : 1)\n    const summary =\n      waitingCount === 0\n        ? `${label}: nothing is waiting for you.`\n        : `${label}: ${waitingCount} request${waitingCount === 1 ? \"\" : \"s\"} waiting${\n            overdueCount > 0 ? `, ${overdueCount} longer than ${formatAge(staleMs)}` : \"\"\n          }.`\n    const decisionPhrase =\n      lastDecided === null\n        ? \"\"\n        : ` ${lastDecided.decision === \"approve\" ? \"Approving\" : \"Denying\"} ${lastDecided.title}. Waiting for your app to confirm.`\n\n    const renderRow = (item: AgentInboxItem, index: number) => {\n      const actions = actionsFor(item)\n      const decision = decisions[item.id]\n      const answered = decision !== undefined\n      const waiting = item.state === \"pending\"\n      const createdEpoch = Date.parse(item.createdAt)\n      const ageMs =\n        clock !== undefined && Number.isFinite(createdEpoch) ? Math.max(0, clock - createdEpoch) : undefined\n      const overdue = waiting && ageMs !== undefined && ageMs >= staleMs\n      const expanded = expandedId === item.id\n      const panelId = `${uid}-detail-${index}`\n      const ageText = ageMs === undefined ? null : formatAge(ageMs)\n\n      const mainClass = cn(\n        \"flex w-full min-w-0 items-start gap-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none\",\n        density === \"compact\" ? \"px-3 py-2\" : \"px-3 py-2.5\",\n        canOpen && \"cursor-pointer hover:bg-muted/50\",\n      )\n\n      const mainInner = (\n        <>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\"mt-1.5 size-2 shrink-0 rounded-full\", item.unread ? \"bg-primary\" : \"bg-transparent\")}\n          />\n          <KindMark kind={item.kind} />\n          <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            <span\n              className={cn(\n                // wrap-anywhere, not break-words: only `overflow-wrap: anywhere`\n                // lowers the min-content width, which is what stops a 90-character\n                // ask from making the whole panel wider than its column.\n                \"min-w-0 wrap-anywhere text-sm\",\n                item.unread ? \"font-semibold text-foreground\" : waiting ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n              )}\n            >\n              {item.unread && <span className=\"sr-only\">Unread. </span>}\n              {item.title}\n            </span>\n            <span className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground\">\n              <span className=\"min-w-0 wrap-anywhere font-mono\">{item.agentName}</span>\n              <span aria-hidden=\"true\">·</span>\n              <span>{KIND_LABEL[item.kind]}</span>\n              {ageText !== null && (\n                <>\n                  <span aria-hidden=\"true\">·</span>\n                  <span\n                    className={cn(\"tabular-nums\", overdue && \"font-medium text-destructive\")}\n                    data-overdue={overdue ? \"\" : undefined}\n                  >\n                    {overdue ? `waiting ${ageText}` : ageText}\n                  </span>\n                </>\n              )}\n              <UrgencyBadge urgency={item.urgency} />\n              <span className=\"sr-only\">{STATE_SENTENCE[item.state]}</span>\n            </span>\n            {density === \"comfortable\" && item.preview.trim() !== \"\" && (\n              // The preview is a producer-trimmed one-liner; the full body is the\n              // detail slot's job, so clamping here hides nothing you cannot reach.\n              <span className=\"line-clamp-2 min-w-0 wrap-anywhere text-xs text-muted-foreground\">{item.preview}</span>\n            )}\n            {waiting && item.impact !== undefined && item.impact !== \"\" && (\n              // Blast radius above the button, always: you cannot approve what you\n              // cannot read, so this line survives even in compact density.\n              <span className=\"flex min-w-0 items-start gap-1.5 rounded-md bg-muted/60 px-2 py-1 text-xs\">\n                <TriangleAlert aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground\" />\n                <span className=\"min-w-0 wrap-anywhere\">{item.impact}</span>\n              </span>\n            )}\n            {!waiting && item.resolutionNote !== undefined && item.resolutionNote !== \"\" && (\n              <span className=\"min-w-0 wrap-anywhere text-xs text-muted-foreground\">{item.resolutionNote}</span>\n            )}\n          </span>\n          {renderDetail !== undefined && (\n            <ChevronRight\n              aria-hidden=\"true\"\n              className={cn(\n                \"mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-150 motion-reduce:transition-none\",\n                expanded && \"rotate-90\",\n              )}\n            />\n          )}\n        </>\n      )\n\n      const isActive = item.id === activeId\n      const labels = { answer: answerLabel, approve: approveLabel, deny: denyLabel }\n\n      return (\n        <React.Fragment key={item.id}>\n          <div\n            className={cn(\n              // Below ~28rem the two quick actions would eat more than half the\n              // row and the ask would wrap letter by letter, so the actions drop\n              // to their own line instead of squeezing the thing being decided.\n              \"flex min-w-0 flex-wrap items-stretch border-l-2\",\n              waiting && item.urgency === \"critical\" ? \"border-l-destructive\" : \"border-l-transparent\",\n              !waiting && \"bg-muted/20\",\n            )}\n            data-state={item.state}\n            role=\"row\"\n          >\n            <div className=\"flex w-full min-w-0 @md/inbox:w-auto @md/inbox:flex-1\" role=\"gridcell\">\n              {canOpen ? (\n                <button\n                  aria-controls={renderDetail !== undefined && expanded ? panelId : undefined}\n                  aria-expanded={renderDetail === undefined ? undefined : expanded}\n                  className={mainClass}\n                  data-inbox-cell={`${item.id}::0`}\n                  onClick={() => activate(item)}\n                  onFocus={() => focusCell(item.id, 0)}\n                  ref={registerCell}\n                  tabIndex={isActive && activeCol === 0 ? 0 : -1}\n                  type=\"button\"\n                >\n                  {mainInner}\n                </button>\n              ) : (\n                // No handler, no button: a row that looks clickable and does\n                // nothing is worse than a row that never pretended. The cell is\n                // still a focus stop, so keyboard reading of the queue survives.\n                <div\n                  className={mainClass}\n                  data-inbox-cell={`${item.id}::0`}\n                  onFocus={() => focusCell(item.id, 0)}\n                  ref={registerCell}\n                  tabIndex={isActive && activeCol === 0 ? 0 : -1}\n                >\n                  {mainInner}\n                </div>\n              )}\n            </div>\n            <div className=\"flex shrink-0 items-center pb-2 pl-3 @md/inbox:py-2 @md/inbox:pl-2\" role=\"gridcell\">\n              {actions[0] === undefined ? (\n                <OutcomeText state={item.state} />\n              ) : (\n                <RowAction\n                  action={actions[0]}\n                  answered={answered}\n                  cellKey={`${item.id}::1`}\n                  decision={decision}\n                  label={labels[actions[0]]}\n                  onAnswer={() => activate(item)}\n                  onDecide={next => decide(item, next)}\n                  onFocusCell={() => focusCell(item.id, 1)}\n                  registerCell={registerCell}\n                  tabIndex={isActive && activeCol === 1 ? 0 : -1}\n                />\n              )}\n            </div>\n            <div\n              className={cn(\"flex shrink-0 items-center pb-2 pr-3 @md/inbox:py-2\", actions.length > 1 && \"pl-1.5\")}\n              role=\"gridcell\"\n            >\n              {actions[1] === undefined ? null : (\n                <RowAction\n                  action={actions[1]}\n                  answered={answered}\n                  cellKey={`${item.id}::2`}\n                  decision={decision}\n                  label={labels[actions[1]]}\n                  onAnswer={() => activate(item)}\n                  onDecide={next => decide(item, next)}\n                  onFocusCell={() => focusCell(item.id, 2)}\n                  registerCell={registerCell}\n                  tabIndex={isActive && activeCol === 2 ? 0 : -1}\n                />\n              )}\n            </div>\n          </div>\n          {expanded && renderDetail !== undefined && (\n            <div className=\"bg-muted/30\" role=\"row\">\n              <div aria-colspan={3} className=\"min-w-0 px-3 py-3\" id={panelId} role=\"gridcell\">\n                {renderDetail(item)}\n              </div>\n            </div>\n          )}\n        </React.Fragment>\n      )\n    }\n\n    return (\n      <section aria-label={label} className={rootClass} ref={ref} {...props}>\n        {/* One persistent live region: mounted for the whole ready branch, so a\n            CHANGE (a decision, a request landing) is announced from an element\n            the screen reader is already watching. */}\n        <p className=\"sr-only\" role=\"status\">\n          {summary}\n          {decisionPhrase}\n        </p>\n\n        <header className=\"flex flex-wrap items-center gap-x-3 gap-y-2 border-b p-3\">\n          <h3 className=\"text-sm font-medium\">{label}</h3>\n          <span\n            className={cn(\n              \"rounded-full px-2 py-0.5 text-xs font-medium tabular-nums\",\n              waitingCount > 0 ? \"bg-primary/10 text-primary\" : \"bg-muted text-muted-foreground\",\n            )}\n            data-waiting-count={waitingCount}\n          >\n            {waitingCount} waiting\n          </span>\n          {overdueCount > 0 && (\n            <span className=\"inline-flex items-center gap-1 text-xs font-medium text-destructive\">\n              <TriangleAlert aria-hidden=\"true\" className=\"size-3.5\" />\n              {overdueCount} over {formatAge(staleMs)}\n            </span>\n          )}\n          {showControls && (\n            <div className=\"ml-auto flex flex-wrap items-center gap-2\">\n              <GroupControl onChange={changeGroupBy} value={groupBy} />\n              <button\n                aria-pressed={pendingOnly}\n                className={cn(\n                  \"inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n                  pendingOnly ? \"border-primary/50 bg-primary/10 text-primary\" : \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n                )}\n                onClick={() => setPendingOnly(value => !value)}\n                type=\"button\"\n              >\n                <Filter aria-hidden=\"true\" className=\"size-3\" />\n                Needs you only\n              </button>\n            </div>\n          )}\n        </header>\n\n        {visible.length === 0 ? (\n          // A filter that hides everything explains ITSELF — otherwise a full\n          // queue reads as an empty one and the reader files a bug.\n          <div className=\"flex flex-col items-start gap-2 p-6\">\n            <p className=\"flex items-center gap-2 font-medium\">\n              <CircleCheck aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n              Nothing is waiting for you\n            </p>\n            <p className=\"text-xs text-muted-foreground\">\n              {hiddenCount} settled request{hiddenCount === 1 ? \"\" : \"s\"} hidden by the filter.\n            </p>\n            <button\n              className={cn(ACTION_BASE, \"border hover:bg-muted\")}\n              onClick={() => setPendingOnly(false)}\n              type=\"button\"\n            >\n              Show everything\n            </button>\n          </div>\n        ) : (\n          <div\n            aria-colcount={3}\n            aria-label={`${label} requests`}\n            aria-rowcount={rowCount}\n            className=\"divide-y\"\n            onKeyDown={handleGridKeyDown}\n            role=\"grid\"\n          >\n            {groups.map(group => (\n              <React.Fragment key={group.key}>\n                {groupBy !== \"none\" && (\n                  <div className=\"bg-muted/40\" role=\"row\">\n                    <div\n                      aria-colspan={3}\n                      className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-3 py-1.5 text-xs\"\n                      role=\"gridcell\"\n                    >\n                      <span className={cn(\"min-w-0 wrap-anywhere font-medium\", groupBy === \"agent\" && \"font-mono\")}>\n                        {group.label}\n                      </span>\n                      <span className=\"text-muted-foreground tabular-nums\">\n                        {group.waiting} of {group.items.length} waiting\n                      </span>\n                    </div>\n                  </div>\n                )}\n                {group.items.map(item => renderRow(item, indexById.get(item.id) ?? 0))}\n              </React.Fragment>\n            ))}\n          </div>\n        )}\n\n        {visible.length > 0 && canDecide && (\n          <p className=\"border-t px-3 py-2 text-[11px] text-muted-foreground\">\n            <span className=\"font-mono\">↑↓</span> move between requests · <span className=\"font-mono\">←→</span> reach the\n            quick actions · <span className=\"font-mono\">A</span> approve · <span className=\"font-mono\">D</span> deny\n          </p>\n        )}\n      </section>\n    )\n  },\n)\n\nAgentInbox.displayName = \"AgentInbox\"\n\nexport default AgentInbox\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/agent-inbox.contract.ts",
      "content": "import { z } from \"zod\"\n\n/**\n * WHAT the agent stopped for. The kind is not decoration — it decides which\n * quick actions a row is even allowed to grow:\n *\n * - `approval` / `review` → Approve + Deny, because there is a yes/no gate.\n * - `question` → Answer only. A question has no \"approve\"; offering one would\n *   send an empty yes back to an agent that asked \"which Stripe account?\".\n */\nexport const AGENT_INBOX_KINDS = [\"approval\", \"question\", \"review\"] as const\nexport const agentInboxKindSchema = z.enum(AGENT_INBOX_KINDS)\nexport type AgentInboxKind = z.infer<typeof agentInboxKindSchema>\n\n/**\n * How much it costs to keep ignoring this. Ordered most → least severe; the\n * component ranks with this order, so adding a level means adding it here.\n * Urgency is set by the PRODUCER (your policy engine), never inferred from age:\n * a four-hour-old \"low\" is still low, it is just old.\n */\nexport const AGENT_INBOX_URGENCIES = [\"critical\", \"high\", \"normal\", \"low\"] as const\nexport const agentInboxUrgencySchema = z.enum(AGENT_INBOX_URGENCIES)\nexport type AgentInboxUrgency = z.infer<typeof agentInboxUrgencySchema>\n\n/**\n * Where one request ended up.\n *\n * `pending` is the only state that carries quick actions. The four terminal\n * states stay in the list on purpose — an inbox that deletes a row the moment\n * you answer it gives you no way to see what you just did, and `expired`\n * (nobody answered in time, the agent moved on without you) is the outcome a\n * human-in-the-loop system most needs to be able to count.\n */\nexport const AGENT_INBOX_ITEM_STATES = [\"pending\", \"approved\", \"denied\", \"answered\", \"expired\"] as const\nexport const agentInboxItemStateSchema = z.enum(AGENT_INBOX_ITEM_STATES)\nexport type AgentInboxItemState = z.infer<typeof agentInboxItemStateSchema>\n\n/** What a quick action sends back to you. `answer` is not here: answering is navigation, not a verdict. */\nexport const agentInboxDecisionSchema = z.enum([\"approve\", \"deny\"])\nexport type AgentInboxDecision = z.infer<typeof agentInboxDecisionSchema>\n\n/** ISO string, epoch ms, or a Date — whatever your transport already speaks. Used for the injected `now`. */\nexport const agentInboxInstantSchema = z.union([z.string(), z.number(), z.date()])\nexport type AgentInboxInstant = z.infer<typeof agentInboxInstantSchema>\n\n/**\n * ONE request an agent parked for a human.\n *\n * `createdAt` stays a raw ISO instant instead of a pre-formatted label because\n * the queue has to *rank* by it (an old critical request outranks a fresh one)\n * and because \"waiting 41m\" is the number that makes someone act. Determinism\n * is bought a different way: the component measures every instant against a\n * `now` you inject, so it never reads the wall clock during render and SSR and\n * hydration produce byte-identical output.\n *\n * `unread` is DATA YOU OWN. The component never flips it — opening a row calls\n * `onOpen` and your data layer decides what \"read\" means (opened? scrolled?\n * acknowledged on another device?).\n */\nexport const agentInboxItemSchema = z.object({\n  /** Stable identity. Also the key a one-shot decision is locked against. */\n  id: z.string(),\n  /** Which agent is waiting, e.g. `release-bot`. Rendered monospace; also the grouping key for `groupBy=\"agent\"`. */\n  agentName: z.string().min(1),\n  kind: agentInboxKindSchema,\n  /** The ask, in one line of the user's language — not the tool name. */\n  title: z.string().min(1),\n  /** First line of the request body, already trimmed by your data layer. The full text belongs in the detail slot. */\n  preview: z.string(),\n  urgency: agentInboxUrgencySchema,\n  /** ISO 8601 with a timezone designator, e.g. \"2026-03-04T09:00:00.000Z\". */\n  createdAt: z.iso.datetime({ offset: true }),\n  state: agentInboxItemStateSchema,\n  /** Drives the dot *and* the title weight — never colour alone. Absent = read. */\n  unread: z.boolean().optional(),\n  /**\n   * The blast radius, spelled out above the Approve button: \"Ships 3 commits to\n   * prod and restarts 12 containers.\" Shown only while `pending` — you cannot\n   * approve what you cannot read, and after the fact it is just noise.\n   */\n  impact: z.string().optional(),\n  /** How a terminal row explains itself tomorrow: \"Denied — the QA run is still using it.\" */\n  resolutionNote: z.string().optional(),\n})\nexport type AgentInboxItem = z.infer<typeof agentInboxItemSchema>\n\n/**\n * The INBOX's own render state — \"is there a queue to show at all\". Independent\n * of any row's `state`: the envelope is `ready` while every row inside it is\n * still `pending`, and `error` here means the queue failed to load, which is a\n * different failure from a request being denied.\n */\nexport const agentInboxStatusSchema = z.enum([\"loading\", \"empty\", \"error\", \"ready\"])\nexport type AgentInboxStatus = z.infer<typeof agentInboxStatusSchema>\n\n/** The envelope a data layer / mock factory hands over; the demo spreads it into the props. */\nexport const agentInboxSchema = z.object({\n  status: agentInboxStatusSchema,\n  items: z.array(agentInboxItemSchema),\n})\nexport type AgentInboxData = z.infer<typeof agentInboxSchema>\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}