{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-card",
  "type": "registry:component",
  "title": "Approval card",
  "description": "Proposed agent action with preview, details and approve, edit, reject.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@wibo/utils",
    "@wibo/badge",
    "@wibo/button",
    "@wibo/key-value-list"
  ],
  "files": [
    {
      "path": "src/patterns/approval-card.tsx",
      "type": "registry:component",
      "content": "import { useId, type ComponentProps, type ReactNode } from \"react\";\nimport { Ban, Check, Clock, Pencil, ShieldCheck } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { KeyValueList, type KeyValueEntry } from \"@/components/ui/key-value-list\";\n\ntype ApprovalState = \"pending\" | \"approved\" | \"rejected\" | \"expired\";\ntype ApprovalAction = \"approve\" | \"edit\" | \"reject\";\n\ninterface ApprovalCardLabels {\n  approve: string;\n  edit: string;\n  reject: string;\n  preview: string;\n  pending: string;\n  approved: string;\n  rejected: string;\n  expired: string;\n  expires: string;\n}\n\nconst defaultLabels: ApprovalCardLabels = {\n  approve: \"Approve\",\n  edit: \"Edit\",\n  reject: \"Reject\",\n  preview: \"Preview\",\n  pending: \"Awaiting approval\",\n  approved: \"Approved\",\n  rejected: \"Rejected\",\n  expired: \"Expired\",\n  expires: \"Expires\",\n};\n\nconst stateVariant = {\n  pending: \"warning\",\n  approved: \"success\",\n  rejected: \"danger\",\n  expired: \"outline\",\n} as const;\n\ninterface ApprovalCardProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n  title: ReactNode;\n  description?: ReactNode;\n  state: ApprovalState;\n  /** Exactly what will run if approved (message, record change, payload). */\n  preview?: ReactNode;\n  /** Key facts: target system, record, amount, requested by. */\n  details?: KeyValueEntry[];\n  /** Formatted expiry, shown while pending. */\n  expiresAt?: ReactNode;\n  /** Who decided and when, shown once decided. */\n  decision?: ReactNode;\n  /** Who proposed the action, before the title (32 px, an `AgentAvatar`); a shield icon otherwise. Decorative: name it in `description`. */\n  mark?: ReactNode;\n  onApprove?: () => void;\n  onEdit?: () => void;\n  onReject?: () => void;\n  /** The action in progress: shows a spinner and disables the other buttons. */\n  busy?: ApprovalAction;\n  labels?: Partial<ApprovalCardLabels>;\n}\n\n/** An agent's proposed action waiting for a human: preview, details, approve / edit / reject. */\nfunction ApprovalCard({\n  title,\n  description,\n  state,\n  preview,\n  details,\n  expiresAt,\n  decision,\n  mark,\n  onApprove,\n  onEdit,\n  onReject,\n  busy,\n  labels: labelOverrides,\n  className,\n  ...props\n}: ApprovalCardProps) {\n  const labels = { ...defaultLabels, ...labelOverrides };\n  const titleId = useId();\n  const previewId = useId();\n  const pending = state === \"pending\";\n  const StateIcon = state === \"approved\" ? Check : state === \"rejected\" ? Ban : state === \"expired\" ? Clock : null;\n\n  return (\n    <div\n      data-slot=\"approval-card\"\n      data-state={state}\n      role=\"group\"\n      aria-labelledby={titleId}\n      aria-busy={busy ? true : undefined}\n      className={cn(\n        \"flex flex-col gap-4 rounded-xl border border-border bg-card p-4 text-card-foreground shadow-card\",\n        state === \"expired\" && \"bg-muted/40\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-start gap-3\">\n        {mark ? (\n          <span aria-hidden=\"true\" className=\"flex size-8 shrink-0 items-center justify-center\">\n            {mark}\n          </span>\n        ) : (\n          <span\n            aria-hidden=\"true\"\n            className=\"flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground\"\n          >\n            <ShieldCheck className=\"size-4\" />\n          </span>\n        )}\n        <div className=\"grid min-w-0 flex-1 gap-0.5\">\n          <div className=\"flex flex-wrap items-center justify-between gap-2\">\n            <div id={titleId} className=\"text-sm font-medium text-foreground\">\n              {title}\n            </div>\n            <Badge variant={stateVariant[state]}>\n              {StateIcon ? <StateIcon aria-hidden=\"true\" /> : null}\n              {labels[state]}\n            </Badge>\n          </div>\n          {description ? <p className=\"text-sm text-muted-foreground\">{description}</p> : null}\n        </div>\n      </div>\n\n      {preview ? (\n        <div className=\"grid gap-1.5\">\n          <div id={previewId} className=\"font-mono text-xs tracking-wide text-muted-foreground uppercase\">\n            {labels.preview}\n          </div>\n          <div\n            aria-labelledby={previewId}\n            role=\"region\"\n            className={cn(\n              \"rounded-lg border border-border bg-background p-3 text-sm text-foreground\",\n              !pending && \"opacity-80\",\n            )}\n          >\n            {preview}\n          </div>\n        </div>\n      ) : null}\n\n      {details && details.length > 0 ? <KeyValueList items={details} /> : null}\n\n      {pending ? (\n        <div className=\"flex flex-wrap items-center gap-2 border-t border-border pt-4\">\n          {expiresAt ? (\n            <span className=\"text-xs text-muted-foreground\">\n              {labels.expires} <span className=\"font-mono text-foreground tabular-nums\">{expiresAt}</span>\n            </span>\n          ) : null}\n          <div className=\"ml-auto flex flex-wrap items-center gap-2\">\n            {onEdit ? (\n              <Button variant=\"ghost\" size=\"sm\" onClick={onEdit} loading={busy === \"edit\"} disabled={Boolean(busy)}>\n                {busy === \"edit\" ? null : <Pencil aria-hidden=\"true\" />}\n                {labels.edit}\n              </Button>\n            ) : null}\n            {onReject ? (\n              <Button variant=\"outline\" size=\"sm\" onClick={onReject} loading={busy === \"reject\"} disabled={Boolean(busy)}>\n                {labels.reject}\n              </Button>\n            ) : null}\n            {onApprove ? (\n              <Button size=\"sm\" onClick={onApprove} loading={busy === \"approve\"} disabled={Boolean(busy)}>\n                {busy === \"approve\" ? null : <Check aria-hidden=\"true\" />}\n                {labels.approve}\n              </Button>\n            ) : null}\n          </div>\n        </div>\n      ) : decision ? (\n        <div className=\"border-t border-border pt-3 text-xs text-muted-foreground\">{decision}</div>\n      ) : null}\n    </div>\n  );\n}\n\nexport { ApprovalCard };\nexport type { ApprovalAction, ApprovalCardLabels, ApprovalCardProps, ApprovalState };\n"
    }
  ]
}
