{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "handoff-chain",
  "title": "AI Handoff Chain",
  "description": "Visualize agent transition breadcrumbs showing the chain of agents with arrows, active agent highlighting, and clickable nodes for handoff details.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/ai/ai-handoff-chain/components/elements/ai-handoff-chain.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Bot, ChevronRight, Clock, Info } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface Handoff {\n  id: string;\n  fromAgent: string;\n  toAgent: string;\n  reason?: string;\n  timestamp: Date;\n  isActive?: boolean;\n}\n\ninterface AiHandoffChainContextValue {\n  handoffs: Handoff[];\n  selectedId: string | null;\n  onSelect: (id: string | null) => void;\n}\n\nconst AiHandoffChainContext =\n  React.createContext<AiHandoffChainContextValue | null>(null);\n\nfunction useHandoffChainContext() {\n  const context = React.useContext(AiHandoffChainContext);\n  if (!context) {\n    throw new Error(\n      \"AiHandoffChain components must be used within <AiHandoffChain>\",\n    );\n  }\n  return context;\n}\n\ninterface AiHandoffChainProps {\n  handoffs: Handoff[];\n  onSelect?: (handoff: Handoff | null) => void;\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiHandoffChain({\n  handoffs,\n  onSelect,\n  children,\n  className,\n}: AiHandoffChainProps) {\n  const [selectedId, setSelectedId] = React.useState<string | null>(null);\n\n  const handleSelect = React.useCallback(\n    (id: string | null) => {\n      setSelectedId(id);\n      const handoff = id ? (handoffs.find((h) => h.id === id) ?? null) : null;\n      onSelect?.(handoff);\n    },\n    [handoffs, onSelect],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({ handoffs, selectedId, onSelect: handleSelect }),\n    [handoffs, selectedId, handleSelect],\n  );\n\n  const agents = React.useMemo(() => {\n    if (handoffs.length === 0) return [];\n\n    const agentNames = [handoffs[0].fromAgent];\n    for (const handoff of handoffs) {\n      agentNames.push(handoff.toAgent);\n    }\n    return agentNames.map((name, index) => ({\n      name,\n      isActive:\n        index === agentNames.length - 1 &&\n        handoffs[handoffs.length - 1]?.isActive,\n      handoffIndex: index > 0 ? index - 1 : null,\n    }));\n  }, [handoffs]);\n\n  return (\n    <AiHandoffChainContext.Provider value={contextValue}>\n      <div\n        data-slot=\"ai-handoff-chain\"\n        className={cn(\"flex flex-col gap-2\", className)}\n      >\n        <div className=\"flex items-center gap-1 overflow-x-auto pb-2\">\n          {agents.map((agent, index) => (\n            <React.Fragment key={`${agent.name}-${index}`}>\n              <AiHandoffChainNode\n                name={agent.name}\n                isActive={agent.isActive}\n                handoffId={\n                  agent.handoffIndex !== null\n                    ? handoffs[agent.handoffIndex]?.id\n                    : undefined\n                }\n              />\n              {index < agents.length - 1 && (\n                <AiHandoffChainArrow\n                  handoffId={handoffs[index]?.id}\n                  hasReason={!!handoffs[index]?.reason}\n                />\n              )}\n            </React.Fragment>\n          ))}\n        </div>\n        {children}\n      </div>\n    </AiHandoffChainContext.Provider>\n  );\n}\n\ninterface AiHandoffChainNodeProps {\n  name: string;\n  isActive?: boolean;\n  handoffId?: string;\n  icon?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiHandoffChainNode({\n  name,\n  isActive,\n  handoffId,\n  icon,\n  className,\n}: AiHandoffChainNodeProps) {\n  const { selectedId, onSelect } = useHandoffChainContext();\n\n  const isSelected = handoffId !== undefined && selectedId === handoffId;\n\n  const handleClick = React.useCallback(() => {\n    if (handoffId !== undefined) {\n      onSelect(isSelected ? null : handoffId);\n    }\n  }, [handoffId, isSelected, onSelect]);\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"ai-handoff-chain-node\"\n      data-active={isActive}\n      data-selected={isSelected}\n      onClick={handleClick}\n      disabled={handoffId === undefined}\n      className={cn(\n        \"inline-flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-all\",\n        \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        isActive &&\n          \"border-green-300 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300\",\n        !isActive && \"border-border bg-card text-card-foreground\",\n        isSelected &&\n          \"ring-2 ring-primary ring-offset-2 ring-offset-background\",\n        handoffId === undefined && \"cursor-default hover:bg-transparent\",\n        className,\n      )}\n    >\n      <span className=\"relative flex size-4 items-center justify-center\">\n        {icon ?? <Bot className=\"size-4\" />}\n        {isActive && (\n          <span className=\"absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-green-500 animate-pulse\" />\n        )}\n      </span>\n      <span>{name}</span>\n    </button>\n  );\n}\n\ninterface AiHandoffChainArrowProps {\n  handoffId?: string;\n  hasReason?: boolean;\n  className?: string;\n}\n\nfunction AiHandoffChainArrow({\n  handoffId,\n  hasReason,\n  className,\n}: AiHandoffChainArrowProps) {\n  const { selectedId, onSelect, handoffs } = useHandoffChainContext();\n\n  const handoff = handoffId\n    ? handoffs.find((h) => h.id === handoffId)\n    : undefined;\n  const isSelected = handoffId !== undefined && selectedId === handoffId;\n\n  const handleClick = React.useCallback(() => {\n    if (handoffId !== undefined) {\n      onSelect(isSelected ? null : handoffId);\n    }\n  }, [handoffId, isSelected, onSelect]);\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"ai-handoff-chain-arrow\"\n      onClick={handleClick}\n      disabled={!hasReason}\n      title={handoff?.reason}\n      className={cn(\n        \"group relative flex shrink-0 items-center px-1 transition-colors\",\n        hasReason &&\n          \"cursor-pointer hover:text-primary focus-visible:outline-none focus-visible:text-primary\",\n        !hasReason && \"cursor-default text-muted-foreground\",\n        className,\n      )}\n    >\n      <ChevronRight className=\"size-4\" />\n      {hasReason && (\n        <Info className=\"absolute -top-1 -right-1 size-3 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100\" />\n      )}\n    </button>\n  );\n}\n\ninterface AiHandoffChainDetailProps {\n  className?: string;\n}\n\nfunction AiHandoffChainDetail({ className }: AiHandoffChainDetailProps) {\n  const { selectedId, handoffs } = useHandoffChainContext();\n\n  const selectedHandoff = selectedId\n    ? handoffs.find((h) => h.id === selectedId)\n    : null;\n\n  if (!selectedHandoff) {\n    return null;\n  }\n\n  const formatTime = (date: Date) => {\n    return new Intl.DateTimeFormat(\"en-US\", {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n    }).format(date);\n  };\n\n  return (\n    <div\n      data-slot=\"ai-handoff-chain-detail\"\n      className={cn(\n        \"rounded-lg border border-border bg-muted/30 p-3 text-sm animate-in fade-in slide-in-from-top-2\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start gap-3\">\n        <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted\">\n          <ChevronRight className=\"size-4 text-muted-foreground\" />\n        </div>\n        <div className=\"flex-1 min-w-0\">\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"font-medium\">\n              {selectedHandoff.fromAgent}{\" \"}\n              <span className=\"text-muted-foreground\">to</span>{\" \"}\n              {selectedHandoff.toAgent}\n            </div>\n            <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n              <Clock className=\"size-3\" />\n              {formatTime(selectedHandoff.timestamp)}\n            </div>\n          </div>\n          {selectedHandoff.reason && (\n            <p className=\"mt-1 text-sm text-muted-foreground\">\n              {selectedHandoff.reason}\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface AiHandoffChainTimelineProps {\n  className?: string;\n}\n\nfunction AiHandoffChainTimeline({ className }: AiHandoffChainTimelineProps) {\n  const { handoffs, selectedId, onSelect } = useHandoffChainContext();\n\n  const formatTime = (date: Date) => {\n    return new Intl.DateTimeFormat(\"en-US\", {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n    }).format(date);\n  };\n\n  if (handoffs.length === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      data-slot=\"ai-handoff-chain-timeline\"\n      className={cn(\"space-y-2\", className)}\n    >\n      {handoffs.map((handoff, index) => {\n        const isSelected = selectedId === handoff.id;\n        const isLast = index === handoffs.length - 1;\n\n        return (\n          <button\n            key={handoff.id}\n            type=\"button\"\n            onClick={() => onSelect(isSelected ? null : handoff.id)}\n            className={cn(\n              \"relative flex w-full items-start gap-3 rounded-lg border p-3 text-left transition-all\",\n              \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n              isSelected && \"border-primary bg-muted/30\",\n              !isSelected && \"border-border\",\n            )}\n          >\n            <div className=\"flex flex-col items-center\">\n              <div\n                className={cn(\n                  \"flex size-6 items-center justify-center rounded-full border-2\",\n                  handoff.isActive\n                    ? \"border-green-500 bg-green-100 dark:bg-green-950\"\n                    : \"border-muted-foreground/30 bg-muted\",\n                )}\n              >\n                <span className=\"text-xs font-medium\">{index + 1}</span>\n              </div>\n              {!isLast && (\n                <div className=\"mt-1 h-4 w-0.5 rounded-full bg-muted\" />\n              )}\n            </div>\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"flex items-center justify-between gap-2\">\n                <div className=\"flex items-center gap-1.5 text-sm font-medium\">\n                  <span>{handoff.fromAgent}</span>\n                  <ChevronRight className=\"size-3 text-muted-foreground\" />\n                  <span>{handoff.toAgent}</span>\n                </div>\n                <span className=\"text-xs text-muted-foreground\">\n                  {formatTime(handoff.timestamp)}\n                </span>\n              </div>\n              {handoff.reason && (\n                <p className=\"mt-1 text-xs text-muted-foreground line-clamp-2\">\n                  {handoff.reason}\n                </p>\n              )}\n            </div>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nexport {\n  AiHandoffChain,\n  AiHandoffChainNode,\n  AiHandoffChainArrow,\n  AiHandoffChainDetail,\n  AiHandoffChainTimeline,\n};\nexport type { AiHandoffChainProps, Handoff };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Compound component for displaying agent handoff sequences. Features: chain visualization with arrows, active agent highlighting, hover for handoff reason, selectable nodes for details, and optional timeline view.",
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}