{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "handoff-card",
  "title": "Handoff Card",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The packet one agent passes to another: who hands over to whom, the task, the context that travels with it and its size, the questions still open and the rules to keep, with a status and room for actions.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/handoff-card.tsx",
      "content": "// Ballmac UI: Handoff Card. https://ui.ballmac.com/components/handoff-card\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, CircleHelp, CornerUpLeft, FileText, Lock } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype HandoffAgent = {\n  name: string\n  /** What it is for, for example \"Writes the code\". */\n  role?: string\n}\n\ntype HandoffContext = {\n  /** What was passed, for example \"The plan\". */\n  label: string\n  /** More about it, in a few words. */\n  detail?: string\n  /** Size or length, already formatted, such as \"1.2k tokens\". */\n  size?: string\n}\n\ntype HandoffStatus = \"pending\" | \"accepted\" | \"returned\"\n\nconst STATUS: Record<HandoffStatus, string> = { pending: \"Waiting to be picked up\", accepted: \"Accepted\", returned: \"Sent back\" }\n\nfunction initials(name: string) {\n  return name\n    .split(/\\s+/)\n    .map((w) => w[0])\n    .slice(0, 2)\n    .join(\"\")\n    .toUpperCase()\n}\n\ntype HandoffCardProps = Omit<React.ComponentProps<\"article\">, \"title\"> & {\n  /** The agent handing the work over. */\n  from: HandoffAgent\n  /** The agent receiving it. */\n  to: HandoffAgent\n  /** What the work is. */\n  title: string\n  /** Everything passed along. */\n  context?: HandoffContext[]\n  /** Questions the sender could not settle. */\n  questions?: string[]\n  /** Rules the receiver must keep to, shown as chips. */\n  constraints?: string[]\n  status?: HandoffStatus\n  /** Text for the status, if you want your own words. */\n  statusLabel?: string\n  /** Time or place of the handoff, already formatted, such as \"2:14 pm\". */\n  at?: string\n  /** Buttons or other actions under the card. */\n  actions?: React.ReactNode\n}\n\n/**\n * The packet one agent passes to another: who to who, what the work is, what context travels with it, the questions still open and the rules to\n * keep. It is a plain server-friendly card, so a transcript or a dashboard can render a hundred of them.\n */\nfunction HandoffCard({ from, to, title, context = [], questions = [], constraints = [], status = \"pending\", statusLabel, at, actions, className, ...props }: HandoffCardProps) {\n  const id = React.useId()\n  const msg = useMessages()\n  return (\n    <article data-slot=\"handoff-card\" data-status={status} aria-labelledby={`${id}-t`} className={cn(\"grid w-full gap-4 rounded-xl border bg-card p-4 text-card-foreground sm:p-5\", className)} {...props}>\n      <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2\">\n        <div className=\"flex min-w-0 items-center gap-2.5\">\n          <span aria-hidden=\"true\" className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold\">{initials(from.name)}</span>\n          <span className=\"min-w-0\">\n            <span className=\"block truncate text-sm font-medium text-foreground\">{from.name}</span>\n            {from.role && <span className=\"block truncate text-xs text-muted-foreground\">{from.role}</span>}\n          </span>\n        </div>\n        <ArrowRight aria-label={msg(\"handoff-card.handsOver\", \"hands over to\")} role=\"img\" className=\"size-4 shrink-0 text-muted-foreground rtl:rotate-180\" />\n        <div className=\"flex min-w-0 items-center gap-2.5\">\n          <span aria-hidden=\"true\" className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-foreground text-xs font-semibold text-background\">{initials(to.name)}</span>\n          <span className=\"min-w-0\">\n            <span className=\"block truncate text-sm font-medium text-foreground\">{to.name}</span>\n            {to.role && <span className=\"block truncate text-xs text-muted-foreground\">{to.role}</span>}\n          </span>\n        </div>\n        <p className=\"ms-auto flex items-center gap-2 text-xs text-muted-foreground\">\n          <span className=\"rounded-full border px-2 py-0.5 font-medium text-foreground\">{statusLabel ?? msg(`handoff-card.status.${status}`, STATUS[status])}</span>\n          {at && <time className=\"tabular-nums\">{at}</time>}\n        </p>\n      </div>\n\n      <h3 id={`${id}-t`} className=\"text-base leading-snug font-semibold tracking-tight text-balance\">\n        {title}\n      </h3>\n\n      {context.length > 0 && (\n        <section aria-label={msg(\"handoff-card.context\", \"Context passed\")}>\n          <p className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n            <FileText aria-hidden=\"true\" className=\"size-3.5\" /> {msg(\"handoff-card.context\", \"Context passed\")}\n          </p>\n          <ul className=\"mt-2 divide-y rounded-lg border\">\n            {context.map((c) => (\n              <li key={c.label} className=\"flex items-baseline justify-between gap-3 px-3 py-2 text-[13px]\">\n                <span className=\"min-w-0\">\n                  <span className=\"font-medium text-foreground\">{c.label}</span>\n                  {c.detail && <span className=\"text-muted-foreground\"> · {c.detail}</span>}\n                </span>\n                {c.size && <span className=\"shrink-0 font-mono text-xs text-muted-foreground tabular-nums\">{c.size}</span>}\n              </li>\n            ))}\n          </ul>\n        </section>\n      )}\n\n      {questions.length > 0 && (\n        <section aria-label={msg(\"handoff-card.questions\", \"Open questions\")}>\n          <p className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n            <CircleHelp aria-hidden=\"true\" className=\"size-3.5\" /> {msg(\"handoff-card.questions\", \"Open questions\")}\n          </p>\n          <ul className=\"mt-2 grid gap-1.5 text-[13px] leading-snug\">\n            {questions.map((q) => (\n              <li key={q} className=\"flex gap-2\">\n                <CornerUpLeft aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground rtl:-scale-x-100\" />\n                {q}\n              </li>\n            ))}\n          </ul>\n        </section>\n      )}\n\n      {constraints.length > 0 && (\n        <section aria-label={msg(\"handoff-card.rules\", \"Rules to keep\")}>\n          <p className=\"flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n            <Lock aria-hidden=\"true\" className=\"size-3.5\" /> {msg(\"handoff-card.rules\", \"Rules to keep\")}\n          </p>\n          <ul className=\"mt-2 flex flex-wrap gap-1.5\">\n            {constraints.map((c) => (\n              <li key={c} className=\"rounded-full border bg-background px-2.5 py-1 text-xs\">{c}</li>\n            ))}\n          </ul>\n        </section>\n      )}\n\n      {actions && <div className=\"flex flex-wrap justify-end gap-2 border-t pt-4\">{actions}</div>}\n    </article>\n  )\n}\n\nexport { HandoffCard, type HandoffAgent, type HandoffCardProps, type HandoffContext, type HandoffStatus }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/handoff-card.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agents",
      "handoff",
      "multi-agent",
      "delegation",
      "context"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "from and to are { name, role? }; title; context [{ label, detail?, size? }]; questions string[]; constraints string[]; status pending | accepted | returned; at; actions slot. A server-friendly card with no state.",
      "whenToUse": [
        "Showing what one agent passed to another, in a transcript, an inbox or a run view",
        "A human review of a delegation before it is accepted"
      ],
      "whenNotToUse": [
        "The timeline of who did what when; use agent-lanes",
        "A single step of a plan; use agent-plan"
      ],
      "composesWith": [
        "agent-lanes",
        "approval-card",
        "trace",
        "agent-roster"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "An article named by its title; the arrow between agents reads 'hands over to'; each list has a labelled section"
        },
        {
          "keys": "Status",
          "action": "Written as words in a pill, never colour alone"
        },
        {
          "keys": "Actions",
          "action": "Whatever buttons you pass keep their own keyboard behaviour"
        }
      ],
      "customization": [
        "context, questions and constraints",
        "status and statusLabel",
        "actions slot for Accept and Send back buttons"
      ]
    },
    "statesNone": "A presentational card: the handoff is passed in, so it has nothing of its own to load or fail.",
    "examples": [
      "handoff-card-demo",
      "handoff-card-returned"
    ],
    "url": "https://ui.ballmac.com/components/handoff-card"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}