{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "conversation-tree",
  "title": "AI Conversation Tree",
  "description": "Visualize branching conversations with a tree structure. Shows node types (user, assistant, system, tool), expandable branches, active path highlighting, and click-to-navigate functionality.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/ai/ai-conversation-tree/components/elements/ai-conversation-tree.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  Bot,\n  ChevronRight,\n  GitBranch,\n  Settings,\n  User,\n  Wrench,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype NodeType = \"user\" | \"assistant\" | \"system\" | \"tool\";\n\ninterface ConversationNode {\n  id: string;\n  type: NodeType;\n  content: string;\n  children?: ConversationNode[];\n  isActive?: boolean;\n}\n\ninterface AiConversationTreeContextValue {\n  nodes: ConversationNode[];\n  activeNodeId?: string;\n  expandedIds: Set<string>;\n  toggleExpanded: (id: string) => void;\n  onNodeSelect?: (nodeId: string) => void;\n}\n\nconst AiConversationTreeContext =\n  React.createContext<AiConversationTreeContextValue | null>(null);\n\nfunction useConversationTreeContext() {\n  const context = React.useContext(AiConversationTreeContext);\n  if (!context) {\n    throw new Error(\n      \"AiConversationTree components must be used within <AiConversationTree>\",\n    );\n  }\n  return context;\n}\n\nfunction collectAllNodeIds(nodes: ConversationNode[]): string[] {\n  const ids: string[] = [];\n  const traverse = (node: ConversationNode) => {\n    ids.push(node.id);\n    node.children?.forEach(traverse);\n  };\n  nodes.forEach(traverse);\n  return ids;\n}\n\nfunction findActivePath(\n  nodes: ConversationNode[],\n  targetId: string,\n): Set<string> {\n  const path = new Set<string>();\n\n  const traverse = (node: ConversationNode, currentPath: string[]): boolean => {\n    const newPath = [...currentPath, node.id];\n\n    if (node.id === targetId) {\n      newPath.forEach((id) => path.add(id));\n      return true;\n    }\n\n    if (node.children) {\n      for (const child of node.children) {\n        if (traverse(child, newPath)) {\n          return true;\n        }\n      }\n    }\n\n    return false;\n  };\n\n  nodes.forEach((node) => traverse(node, []));\n  return path;\n}\n\ninterface AiConversationTreeProps {\n  nodes: ConversationNode[];\n  activeNodeId?: string;\n  onNodeSelect?: (nodeId: string) => void;\n  defaultExpandAll?: boolean;\n  className?: string;\n  children?: React.ReactNode;\n}\n\nfunction AiConversationTree({\n  nodes,\n  activeNodeId,\n  onNodeSelect,\n  defaultExpandAll = true,\n  className,\n  children,\n}: AiConversationTreeProps) {\n  const [expandedIds, setExpandedIds] = React.useState<Set<string>>(() => {\n    if (defaultExpandAll) {\n      return new Set(collectAllNodeIds(nodes));\n    }\n    return new Set();\n  });\n\n  const toggleExpanded = React.useCallback((id: string) => {\n    setExpandedIds((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      return next;\n    });\n  }, []);\n\n  const contextValue = React.useMemo(\n    () => ({ nodes, activeNodeId, expandedIds, toggleExpanded, onNodeSelect }),\n    [nodes, activeNodeId, expandedIds, toggleExpanded, onNodeSelect],\n  );\n\n  return (\n    <AiConversationTreeContext.Provider value={contextValue}>\n      <div\n        data-slot=\"ai-conversation-tree\"\n        className={cn(\n          \"rounded-lg border border-border bg-card text-card-foreground\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AiConversationTreeContext.Provider>\n  );\n}\n\ninterface AiConversationTreeHeaderProps {\n  title?: string;\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiConversationTreeHeader({\n  title = \"Conversation Tree\",\n  children,\n  className,\n}: AiConversationTreeHeaderProps) {\n  const { nodes } = useConversationTreeContext();\n\n  const stats = React.useMemo(() => {\n    let totalNodes = 0;\n    let branches = 0;\n\n    const traverse = (node: ConversationNode) => {\n      totalNodes++;\n      if (node.children && node.children.length > 1) {\n        branches++;\n      }\n      node.children?.forEach(traverse);\n    };\n\n    nodes.forEach(traverse);\n    return { totalNodes, branches };\n  }, [nodes]);\n\n  return (\n    <div\n      data-slot=\"ai-conversation-tree-header\"\n      className={cn(\n        \"flex items-center gap-3 px-4 py-3 border-b border-border\",\n        className,\n      )}\n    >\n      <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-cyan-100 dark:bg-cyan-950\">\n        <GitBranch className=\"size-4 text-cyan-600 dark:text-cyan-400\" />\n      </div>\n      <div className=\"flex flex-1 items-center gap-2\">\n        <span className=\"font-medium text-sm\">{title}</span>\n        <span className=\"inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n          {stats.totalNodes} messages\n          {stats.branches > 0 && ` / ${stats.branches} branches`}\n        </span>\n      </div>\n      {children}\n    </div>\n  );\n}\n\ninterface AiConversationTreeContentProps {\n  className?: string;\n}\n\nfunction AiConversationTreeContent({\n  className,\n}: AiConversationTreeContentProps) {\n  const { nodes, activeNodeId } = useConversationTreeContext();\n  const activePath = React.useMemo(\n    () =>\n      activeNodeId ? findActivePath(nodes, activeNodeId) : new Set<string>(),\n    [nodes, activeNodeId],\n  );\n\n  return (\n    <div\n      data-slot=\"ai-conversation-tree-content\"\n      className={cn(\"p-2\", className)}\n    >\n      {nodes.map((node) => (\n        <AiConversationTreeNode\n          key={node.id}\n          node={node}\n          depth={0}\n          activePath={activePath}\n        />\n      ))}\n    </div>\n  );\n}\n\ninterface AiConversationTreeNodeProps {\n  node: ConversationNode;\n  depth: number;\n  activePath: Set<string>;\n  className?: string;\n}\n\nfunction AiConversationTreeNode({\n  node,\n  depth,\n  activePath,\n  className,\n}: AiConversationTreeNodeProps) {\n  const { activeNodeId, expandedIds, toggleExpanded, onNodeSelect } =\n    useConversationTreeContext();\n\n  const hasChildren = node.children && node.children.length > 0;\n  const isExpanded = expandedIds.has(node.id);\n  const isActive = node.id === activeNodeId;\n  const isInActivePath = activePath.has(node.id);\n\n  const nodeConfig = React.useMemo(() => {\n    const configs: Record<\n      NodeType,\n      { icon: React.ReactNode; label: string; className: string }\n    > = {\n      user: {\n        icon: <User className=\"size-3.5\" />,\n        label: \"User\",\n        className:\n          \"bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300\",\n      },\n      assistant: {\n        icon: <Bot className=\"size-3.5\" />,\n        label: \"Assistant\",\n        className:\n          \"bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300\",\n      },\n      system: {\n        icon: <Settings className=\"size-3.5\" />,\n        label: \"System\",\n        className:\n          \"bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300\",\n      },\n      tool: {\n        icon: <Wrench className=\"size-3.5\" />,\n        label: \"Tool\",\n        className:\n          \"bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300\",\n      },\n    };\n    return configs[node.type];\n  }, [node.type]);\n\n  const handleClick = React.useCallback(() => {\n    onNodeSelect?.(node.id);\n  }, [node.id, onNodeSelect]);\n\n  const handleToggle = React.useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      toggleExpanded(node.id);\n    },\n    [node.id, toggleExpanded],\n  );\n\n  return (\n    <div\n      data-slot=\"ai-conversation-tree-node\"\n      data-type={node.type}\n      data-active={isActive}\n      className={cn(\"\", className)}\n    >\n      <div\n        className={cn(\n          \"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors cursor-pointer\",\n          isActive\n            ? \"bg-accent text-accent-foreground\"\n            : isInActivePath\n              ? \"bg-muted/50\"\n              : \"hover:bg-muted/50\",\n          depth > 0 && \"ml-4\",\n        )}\n        style={{ marginLeft: depth > 0 ? `${depth * 16}px` : undefined }}\n        onClick={handleClick}\n      >\n        {hasChildren ? (\n          <button\n            type=\"button\"\n            onClick={handleToggle}\n            className=\"flex size-5 shrink-0 items-center justify-center rounded hover:bg-muted\"\n          >\n            <ChevronRight\n              className={cn(\n                \"size-3.5 text-muted-foreground transition-transform duration-200\",\n                isExpanded && \"rotate-90\",\n              )}\n            />\n          </button>\n        ) : (\n          <div className=\"size-5 shrink-0\" />\n        )}\n        <div\n          className={cn(\n            \"flex size-6 shrink-0 items-center justify-center rounded\",\n            nodeConfig.className,\n          )}\n        >\n          {nodeConfig.icon}\n        </div>\n        <span className=\"flex-1 truncate text-xs\">{node.content}</span>\n        {hasChildren && (\n          <span className=\"text-xs text-muted-foreground shrink-0\">\n            {node.children?.length}\n          </span>\n        )}\n      </div>\n      {hasChildren && isExpanded && (\n        <div className=\"relative\">\n          <div\n            className=\"absolute left-[18px] top-0 bottom-0 w-px bg-border\"\n            style={{ marginLeft: depth > 0 ? `${depth * 16}px` : undefined }}\n          />\n          {node.children?.map((child) => (\n            <AiConversationTreeNode\n              key={child.id}\n              node={child}\n              depth={depth + 1}\n              activePath={activePath}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface AiConversationTreePreviewProps {\n  className?: string;\n}\n\nfunction AiConversationTreePreview({\n  className,\n}: AiConversationTreePreviewProps) {\n  const { nodes, activeNodeId } = useConversationTreeContext();\n\n  const activeNode = React.useMemo(() => {\n    if (!activeNodeId) return null;\n\n    const findNode = (\n      searchNodes: ConversationNode[],\n    ): ConversationNode | null => {\n      for (const node of searchNodes) {\n        if (node.id === activeNodeId) return node;\n        if (node.children) {\n          const found = findNode(node.children);\n          if (found) return found;\n        }\n      }\n      return null;\n    };\n\n    return findNode(nodes);\n  }, [nodes, activeNodeId]);\n\n  if (!activeNode) {\n    return (\n      <div\n        data-slot=\"ai-conversation-tree-preview\"\n        className={cn(\n          \"p-4 text-sm text-muted-foreground text-center\",\n          className,\n        )}\n      >\n        Select a message to preview\n      </div>\n    );\n  }\n\n  const nodeConfig = {\n    user: { label: \"User\", className: \"text-blue-600 dark:text-blue-400\" },\n    assistant: {\n      label: \"Assistant\",\n      className: \"text-purple-600 dark:text-purple-400\",\n    },\n    system: { label: \"System\", className: \"text-gray-600 dark:text-gray-400\" },\n    tool: { label: \"Tool\", className: \"text-amber-600 dark:text-amber-400\" },\n  }[activeNode.type];\n\n  return (\n    <div\n      data-slot=\"ai-conversation-tree-preview\"\n      className={cn(\"p-4 border-t border-border\", className)}\n    >\n      <div className=\"flex items-center gap-2 mb-2\">\n        <span\n          className={cn(\"text-xs font-medium uppercase\", nodeConfig.className)}\n        >\n          {nodeConfig.label}\n        </span>\n        <span className=\"text-xs text-muted-foreground\">#{activeNode.id}</span>\n      </div>\n      <p className=\"text-sm whitespace-pre-wrap\">{activeNode.content}</p>\n    </div>\n  );\n}\n\nexport {\n  AiConversationTree,\n  AiConversationTreeHeader,\n  AiConversationTreeContent,\n  AiConversationTreeNode,\n  AiConversationTreePreview,\n};\nexport type { AiConversationTreeProps, ConversationNode, NodeType };\n",
      "type": "registry:component"
    }
  ],
  "docs": "A compound component for visualizing conversation branches. Ideal for chat UIs with edit/regenerate features. Use AiConversationTree as root with AiConversationTreeHeader, AiConversationTreeContent, and AiConversationTreePreview sub-components. Supports controlled activeNodeId and onNodeSelect callback.",
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}