{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-roster",
  "title": "Agent Roster",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Every agent on a team of agents as a card: name and role, a status in words (working, waiting, blocked, idle, offline), what it is doing, a load meter and when it was last active, with status counts that filter the grid.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/agent-roster.tsx",
      "content": "// Ballmac UI: Agent Roster. https://ui.ballmac.com/components/agent-roster\n\"use client\"\n\nimport * as React from \"react\"\nimport { AlertOctagon, CheckCircle2, CircleSlash, Hourglass, Loader2, type LucideIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype AgentStatus = \"working\" | \"idle\" | \"waiting\" | \"blocked\" | \"offline\"\n\ntype RosterAgent = {\n  id: string\n  name: string\n  /** What it is for, for example \"Writes the code\". */\n  role?: string\n  status: AgentStatus\n  /** What it is doing right now, or last did. */\n  task?: string\n  /** How busy it is, 0 to 100. */\n  load?: number\n  /** When it was last active, already formatted, such as \"just now\". */\n  lastActive?: string\n}\n\nconst STATUS: Record<AgentStatus, { icon: LucideIcon; fallback: string; tone: string }> = {\n  working: { icon: Loader2, fallback: \"Working\", tone: \"text-foreground\" },\n  idle: { icon: CheckCircle2, fallback: \"Idle\", tone: \"text-muted-foreground\" },\n  waiting: { icon: Hourglass, fallback: \"Waiting\", tone: \"text-foreground\" },\n  blocked: { icon: AlertOctagon, fallback: \"Blocked\", tone: \"text-destructive\" },\n  offline: { icon: CircleSlash, fallback: \"Offline\", tone: \"text-muted-foreground\" },\n}\nconst ORDER: AgentStatus[] = [\"working\", \"waiting\", \"blocked\", \"idle\", \"offline\"]\n\nconst initials = (n: string) => n.split(/\\s+/).map((w) => w[0]).slice(0, 2).join(\"\").toUpperCase()\n\ntype AgentRosterProps = Omit<React.ComponentProps<\"section\">, \"onSelect\" | \"title\"> & {\n  /** The agents to show. */\n  agents: RosterAgent[]\n  /** The selected agent id. Controlled. */\n  selected?: string | null\n  /** The selected agent id for uncontrolled use. */\n  defaultSelected?: string | null\n  /** Called when an agent is picked, or null when it is cleared. */\n  onSelectedChange?: (id: string | null) => void\n  /** Heading. */\n  title?: string\n}\n\n/**\n * Every agent on a team of agents as a card: who it is, whether it is working, waiting or blocked, what it is doing and how busy it is. Status counts\n * are filters, a card is a toggle button, and a blocked agent is called out in words. A selection is yours to use, for example to open its run.\n */\nfunction AgentRoster({ agents, selected: selectedProp, defaultSelected = null, onSelectedChange, title, className, ...props }: AgentRosterProps) {\n  const msg = useMessages()\n  const uid = React.useId()\n  title ??= msg(\"agent-roster.title\", \"Agents\")\n  const [inner, setInner] = React.useState<string | null>(defaultSelected)\n  const selected = selectedProp !== undefined ? selectedProp : inner\n  const [filter, setFilter] = React.useState<AgentStatus | \"all\">(\"all\")\n  const count = (s: AgentStatus) => agents.filter((a) => a.status === s).length\n  const shown = filter === \"all\" ? agents : agents.filter((a) => a.status === filter)\n  const text = (s: AgentStatus) => msg(`agent-roster.${s}`, STATUS[s].fallback)\n\n  function pick(id: string) {\n    const next = id === selected ? null : id\n    if (selectedProp === undefined) setInner(next)\n    onSelectedChange?.(next)\n  }\n\n  return (\n    <section data-slot=\"agent-roster\" aria-labelledby={`${uid}-t`} className={cn(\"grid w-full min-w-0 gap-4\", className)} {...props}>\n      <div className=\"flex flex-wrap items-center justify-between gap-3\">\n        <h3 id={`${uid}-t`} className=\"text-sm font-medium text-foreground\">{title}</h3>\n        <div role=\"group\" aria-label={msg(\"agent-roster.filter\", \"Filter by status\")} className=\"flex max-w-full flex-wrap gap-1.5\">\n          {([\"all\", ...ORDER] as const).map((k) => {\n            const n = k === \"all\" ? agents.length : count(k)\n            if (k !== \"all\" && n === 0) return null\n            return (\n              <button key={k} type=\"button\" aria-pressed={filter === k} onClick={() => setFilter(k)} className={cn(\"h-8 rounded-full border px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\", filter === k ? \"border-foreground bg-foreground text-background\" : \"hover:bg-accent\")}>\n                {k === \"all\" ? msg(\"agent-roster.all\", \"All\") : text(k)} <span className={cn(\"tabular-nums\", filter === k ? \"opacity-70\" : \"text-muted-foreground\")}>{n}</span>\n              </button>\n            )\n          })}\n        </div>\n      </div>\n      <p aria-live=\"polite\" className=\"sr-only\">{msg(\"agent-roster.count\", \"{count} agents shown\", { count: shown.length })}</p>\n\n      <ul className=\"grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))]\">\n        {shown.map((a) => {\n          const s = STATUS[a.status]\n          const Icon = s.icon\n          const on = selected === a.id\n          return (\n            <li key={a.id}>\n              <button\n                type=\"button\"\n                data-status={a.status}\n                aria-pressed={on}\n                onClick={() => pick(a.id)}\n                className={cn(\"flex h-full w-full flex-col gap-3 rounded-xl border bg-card p-4 text-start text-card-foreground outline-none transition-[border-color,background-color] duration-150 hover:bg-accent/40 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\", on && \"border-foreground bg-accent/40\", a.status === \"blocked\" && !on && \"border-destructive/50\")}\n              >\n                <span className=\"flex items-start gap-3\">\n                  <span aria-hidden=\"true\" className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold\">{initials(a.name)}</span>\n                  <span className=\"min-w-0 flex-1\">\n                    <span className=\"block truncate text-sm font-semibold text-foreground\">{a.name}</span>\n                    {a.role && <span className=\"block truncate text-xs text-muted-foreground\">{a.role}</span>}\n                  </span>\n                  <span className={cn(\"inline-flex shrink-0 items-center gap-1.5 text-xs font-medium\", s.tone)}>\n                    <Icon aria-hidden=\"true\" className={cn(\"size-3.5\", a.status === \"working\" && \"motion-safe:animate-spin\")} style={a.status === \"working\" ? { animationPlayState: \"var(--bm-ambient-state, running)\" } : undefined} />\n                    {text(a.status)}\n                  </span>\n                </span>\n                {a.task && <span className=\"line-clamp-2 text-[13px] leading-snug text-muted-foreground\">{a.task}</span>}\n                <span className=\"mt-auto flex items-center gap-3\">\n                  {typeof a.load === \"number\" && (\n                    <span className=\"flex flex-1 items-center gap-2\">\n                      <span role=\"meter\" aria-label={msg(\"agent-roster.load\", \"{name} load\", { name: a.name })} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(a.load)} className=\"h-1.5 flex-1 overflow-hidden rounded-full bg-muted\">\n                        <span className=\"block h-full rounded-full bg-foreground\" style={{ width: `${Math.min(100, Math.max(0, a.load))}%` }} />\n                      </span>\n                      <span className=\"font-mono text-[11px] text-muted-foreground tabular-nums\">{Math.round(a.load)}%</span>\n                    </span>\n                  )}\n                  {a.lastActive && <span className=\"ms-auto text-[11px] text-muted-foreground\">{a.lastActive}</span>}\n                </span>\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n    </section>\n  )\n}\n\nexport { AgentRoster, type AgentRosterProps, type AgentStatus, type RosterAgent }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/agent-roster.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agents",
      "roster",
      "status",
      "team",
      "multi-agent",
      "monitor"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "agents is [{ id, name, role?, status: working | idle | waiting | blocked | offline, task?, load?: 0..100, lastActive? }]. selected / defaultSelected / onSelectedChange track the picked agent (a second click clears it). Status counts are filters; statuses with no agents are hidden.",
      "whenToUse": [
        "An overview of the agents in a multi-agent product and what each is doing",
        "Picking an agent to open its run, logs or settings"
      ],
      "whenNotToUse": [
        "One agent's steps; use trace",
        "A table of users; use data-table",
        "Who handed what to whom; use agent-lanes or handoff-card"
      ],
      "composesWith": [
        "agent-lanes",
        "handoff-card",
        "run-ledger",
        "trace"
      ],
      "a11y": [
        {
          "keys": "Tab, Enter / Space",
          "action": "Filters and cards are buttons; a card is a toggle (aria-pressed)"
        },
        {
          "keys": "Screen readers",
          "action": "Status is written as a word beside its icon; the load is a labelled meter; the shown count is a polite live region"
        },
        {
          "keys": "Blocked",
          "action": "Called out in words and an icon, with a red border only as reinforcement"
        }
      ],
      "customization": [
        "agents and their wording",
        "title",
        "Controlled or uncontrolled selection"
      ]
    },
    "statesNone": "A presentational overview: the agents are passed in, so it has nothing of its own to load or fail.",
    "examples": [
      "agent-roster-demo",
      "agent-roster-select"
    ],
    "url": "https://ui.ballmac.com/components/agent-roster"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}