{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "branch-tree",
  "title": "BranchTree",
  "description": "A drawn branch graph: nodes colored by compute state, curved edges to each parent, default and selected marked.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json"
  ],
  "files": [
    {
      "path": "src/components/branch-tree/branch-tree.tsx",
      "content": "\"use client\";\n\nimport { GitBranchIcon, SquareLock01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { useState } from \"react\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** Compute lifecycle, mirrored from ComputeStatus. Drives the node color. */\nexport type ComputeState = \"active\" | \"idle\" | \"scaling\" | \"suspended\";\n\nexport interface Branch {\n  id: string;\n  name: string;\n  /** The project's default branch: its node wears a primary ring. */\n  default?: boolean;\n  /** A protected branch: shows a lock. */\n  protected?: boolean;\n  /** The parent branch's id; the edge is drawn from it. */\n  parent?: string;\n  /** Compute state, painted onto the node dot. */\n  state?: ComputeState;\n  /** ISO timestamp of the branch's last update, shown as relative time. */\n  updatedAt?: string;\n}\n\ninterface TreeNode {\n  branch: Branch;\n  depth: number;\n  row: number;\n}\n\n/* ─────────────────────────────────────────────────────────\n * GEOMETRY\n *\n *  The graph is a real node-link drawing, not indent guides.\n *  Rows stack at a fixed height; a branch sits in the lane of\n *  its depth. Each child's edge leaves its parent's lane,\n *  runs down, and curves (a fixed-radius quarter turn) into\n *  the child's node. Node color is the compute state; the\n *  default branch and the selected branch wear a ring.\n * ───────────────────────────────────────────────────────── */\nconst ROW_H = 46;\nconst LANE_W = 22;\nconst PAD_X = 18;\nconst DOT_R = 3.5;\nconst CORNER = 12;\n\nconst laneX = (depth: number) => PAD_X + depth * LANE_W;\nconst rowY = (row: number) => row * ROW_H + ROW_H / 2;\n\n/** Staggered entrance: edges draw, nodes pop, rows fade, top to bottom. */\nconst STAGGER = 0.06;\nconst DRAW = 0.45;\nconst POP = 0.32;\n\nconst BRANCH_TREE_STYLES = `\n@keyframes neon-bt-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }\n@keyframes neon-bt-pop { from { opacity: 0; transform: scale(0.2); } to { opacity: 1; transform: scale(1); } }\n@keyframes neon-bt-row { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) {\n  [data-slot=\"branch-tree\"] * { animation: none !important; }\n}\n`;\n\n/** Depth-first flatten, assigning each branch a row and a depth lane. */\nconst buildNodes = (branches: Branch[]): TreeNode[] => {\n  const ids = new Set(branches.map((branch) => branch.id));\n  const parentOf = (branch: Branch) =>\n    branch.parent && ids.has(branch.parent) ? branch.parent : undefined;\n  const childrenOf = (id?: string) =>\n    branches.filter((branch) => parentOf(branch) === id);\n\n  const nodes: TreeNode[] = [];\n\n  const walk = (branch: Branch, depth: number) => {\n    nodes.push({ branch, depth, row: nodes.length });\n    for (const kid of childrenOf(branch.id)) {\n      walk(kid, depth + 1);\n    }\n  };\n\n  for (const root of childrenOf()) {\n    walk(root, 0);\n  }\n\n  return nodes;\n};\n\nconst DOT_STATE: Record<ComputeState, string> = {\n  active: \"fill-[var(--status-active)] neon-status-breathe\",\n  idle: \"fill-[var(--status-active)]\",\n  scaling: \"fill-[var(--status-scaling)] neon-status-breathe\",\n  suspended: \"fill-[var(--status-sleeping)]\",\n};\n\n/** Ring stroke matched to the dot color, so the selection halo reads as the node. */\nconst RING_STATE: Record<ComputeState, string> = {\n  active: \"stroke-[var(--status-active)]\",\n  idle: \"stroke-[var(--status-active)]\",\n  scaling: \"stroke-[var(--status-scaling)]\",\n  suspended: \"stroke-[var(--status-sleeping)]\",\n};\n\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\nconst relativeTime = (iso?: string): string | null => {\n  if (!iso) {\n    return null;\n  }\n\n  const diff = Date.now() - new Date(iso).getTime();\n  if (diff < MINUTE) {\n    return \"just now\";\n  }\n  if (diff < HOUR) {\n    return `${Math.round(diff / MINUTE)}m ago`;\n  }\n  if (diff < DAY) {\n    return `${Math.round(diff / HOUR)}h ago`;\n  }\n  return `${Math.round(diff / DAY)}d ago`;\n};\n\nconst edgePath = (parent: TreeNode, child: TreeNode) => {\n  const px = laneX(parent.depth);\n  const py = rowY(parent.row);\n  const cx = laneX(child.depth);\n  const cy = rowY(child.row);\n  return `M ${px} ${py} V ${cy - CORNER} Q ${px} ${cy} ${px + CORNER} ${cy} H ${cx}`;\n};\n\nconst Graph = ({\n  nodes,\n  byId,\n  selectedId,\n  width,\n  height,\n}: {\n  nodes: TreeNode[];\n  byId: Map<string, TreeNode>;\n  selectedId?: string;\n  width: number;\n  height: number;\n}) => (\n  <svg\n    aria-hidden=\"true\"\n    className=\"pointer-events-none absolute top-0 left-0 z-10 overflow-visible\"\n    height={height}\n    width={width}\n  >\n    {nodes.map((node) => {\n      const parent = node.branch.parent\n        ? byId.get(node.branch.parent)\n        : undefined;\n      if (!parent) {\n        return null;\n      }\n      return (\n        <path\n          className=\"stroke-border\"\n          d={edgePath(parent, node)}\n          fill=\"none\"\n          key={`edge-${node.branch.id}`}\n          pathLength={1}\n          strokeDasharray={1}\n          strokeLinecap=\"round\"\n          strokeWidth={1.5}\n          style={{\n            animation: `neon-bt-draw ${DRAW}s ease ${node.row * STAGGER}s both`,\n          }}\n        />\n      );\n    })}\n    {nodes.map((node) => {\n      const cx = laneX(node.depth);\n      const cy = rowY(node.row);\n      const selected = node.branch.id === selectedId;\n      const ringed = node.branch.default || selected;\n      const ringStroke = node.branch.state\n        ? RING_STATE[node.branch.state]\n        : \"stroke-muted-foreground/60\";\n      const popStyle = {\n        animation: `neon-bt-pop ${POP}s cubic-bezier(0.34, 1.56, 0.64, 1) ${\n          node.row * STAGGER + 0.14\n        }s both`,\n        transformBox: \"fill-box\" as const,\n        transformOrigin: \"center\",\n      };\n      return (\n        <g key={`node-${node.branch.id}`}>\n          {ringed ? (\n            <rect\n              className={selected ? ringStroke : \"stroke-primary/40\"}\n              fill=\"none\"\n              height={2 * (DOT_R + 2.5)}\n              rx={3}\n              strokeWidth={1.5}\n              style={popStyle}\n              width={2 * (DOT_R + 2.5)}\n              x={cx - DOT_R - 2.5}\n              y={cy - DOT_R - 2.5}\n            />\n          ) : null}\n          <rect\n            className={\n              node.branch.state\n                ? DOT_STATE[node.branch.state]\n                : \"fill-muted-foreground/60\"\n            }\n            height={2 * DOT_R}\n            rx={1}\n            style={popStyle}\n            width={2 * DOT_R}\n            x={cx - DOT_R}\n            y={cy - DOT_R}\n          />\n        </g>\n      );\n    })}\n  </svg>\n);\n\nconst Fence = () => (\n  <span aria-hidden=\"true\" className=\"h-2.5 w-px shrink-0 bg-border/60\" />\n);\n\nconst BranchRow = ({\n  node,\n  selected,\n  onSelect,\n}: {\n  node: TreeNode;\n  selected: boolean;\n  onSelect: () => void;\n}) => {\n  const time = relativeTime(node.branch.updatedAt);\n  return (\n    <button\n      aria-current={selected || undefined}\n      className={cn(\n        \"relative flex w-full items-center rounded-md pr-3 text-left transition-colors\",\n        !selected && \"hover:bg-muted/50\"\n      )}\n      onClick={onSelect}\n      style={{\n        animation: `neon-bt-row 0.4s ease ${node.row * STAGGER}s both`,\n        height: ROW_H,\n        paddingLeft: laneX(node.depth) + DOT_R + 14,\n      }}\n      type=\"button\"\n    >\n      <span className=\"flex min-w-0 flex-1 items-center gap-1.5\">\n        <span\n          className={cn(\n            \"min-w-0 truncate font-mono text-xs\",\n            selected ? \"text-foreground\" : \"text-foreground/85\"\n          )}\n        >\n          {node.branch.name}\n        </span>\n        {node.branch.default ? (\n          <span className=\"shrink-0 rounded-sm border border-primary/40 px-1 py-px font-mono text-[9px] text-primary\">\n            default\n          </span>\n        ) : null}\n        {node.branch.protected ? (\n          <HugeiconsIcon\n            aria-label=\"protected\"\n            className=\"size-3 shrink-0 text-muted-foreground/70\"\n            icon={SquareLock01Icon}\n            strokeWidth={2}\n          />\n        ) : null}\n      </span>\n\n      <span className=\"flex shrink-0 items-center gap-2 font-mono text-[10px] text-muted-foreground\">\n        {node.branch.state ? (\n          <span className=\"capitalize\">{node.branch.state}</span>\n        ) : null}\n        {node.branch.state && time ? <Fence /> : null}\n        {time ? <span className=\"text-muted-foreground/70\">{time}</span> : null}\n      </span>\n    </button>\n  );\n};\n\nexport type BranchTreeProps = Omit<\n  ComponentProps<\"div\">,\n  \"onSelect\" | \"value\" | \"defaultValue\"\n> & {\n  /** The branches to draw. Each references its parent by id. */\n  branches: Branch[];\n  /** Selected branch id (controlled). */\n  value?: string;\n  /** Initial selected branch id (uncontrolled). */\n  defaultValue?: string;\n  /** Notified when a branch node is chosen. */\n  onValueChange?: (id: string) => void;\n};\n\nexport const BranchTree = ({\n  branches,\n  value,\n  defaultValue,\n  onValueChange,\n  className,\n  ...props\n}: BranchTreeProps) => {\n  const [internal, setInternal] = useState(defaultValue);\n  const selectedId = value ?? internal;\n\n  const nodes = buildNodes(branches);\n  const byId = new Map(nodes.map((node) => [node.branch.id, node]));\n  const maxDepth = Math.max(0, ...nodes.map((node) => node.depth));\n  const selectedNode = selectedId ? byId.get(selectedId) : undefined;\n  const gutterWidth = laneX(maxDepth) + DOT_R + 6;\n  const treeHeight = nodes.length * ROW_H;\n\n  const choose = (id: string) => {\n    if (value === undefined) {\n      setInternal(id);\n    }\n    onValueChange?.(id);\n  };\n\n  return (\n    <div\n      className={cn(\"rounded-lg border border-border/60 bg-card\", className)}\n      data-slot=\"branch-tree\"\n      {...props}\n    >\n      <style>{BRANCH_TREE_STYLES}</style>\n      <header className=\"flex items-center gap-2 border-border/60 border-b px-3 py-2\">\n        <HugeiconsIcon\n          className=\"size-3.5 text-muted-foreground\"\n          icon={GitBranchIcon}\n          strokeWidth={2}\n        />\n        <span className=\"font-mono text-foreground text-xs\">Branches</span>\n        <span className=\"ml-auto font-mono text-[10px] text-muted-foreground tabular-nums\">\n          {branches.length}\n        </span>\n      </header>\n\n      <div className=\"neon-scroll-fade max-h-80 overflow-auto px-2 py-1.5\">\n        <div className=\"relative\" style={{ height: treeHeight }}>\n          {selectedNode ? (\n            <div\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-x-0 top-0 rounded-md bg-muted transition-transform duration-300 ease-out motion-reduce:transition-none\"\n              style={{\n                height: ROW_H,\n                transform: `translateY(${selectedNode.row * ROW_H}px)`,\n              }}\n            />\n          ) : null}\n          <Graph\n            byId={byId}\n            height={treeHeight}\n            nodes={nodes}\n            selectedId={selectedId}\n            width={gutterWidth}\n          />\n          {nodes.map((node) => (\n            <BranchRow\n              key={node.branch.id}\n              node={node}\n              onSelect={() => choose(node.branch.id)}\n              selected={node.branch.id === selectedId}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}