{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "branch-picker",
  "title": "BranchPicker",
  "description": "A searchable branch selector with default/protected status and an inline create-branch affordance.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/input.json",
    "https://ui.neon.com/r/popover.json",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/tooltip.json"
  ],
  "files": [
    {
      "path": "src/components/branch-picker/branch-picker.tsx",
      "content": "/* oxlint-disable jsx-a11y/prefer-tag-over-role -- a custom ARIA listbox; a native select cannot filter, create, or draw the tree. */\n\"use client\";\n\nimport {\n  ArrowDown01Icon,\n  ArrowLeft01Icon,\n  GitBranchIcon,\n  PlusSignIcon,\n  Search01Icon,\n  SquareLock01Icon,\n  Tick02Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ComponentProps, FormEvent, KeyboardEvent } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Branch {\n  id: string;\n  name: string;\n  /** The project's default branch. */\n  default?: boolean;\n  /** A protected branch. */\n  protected?: boolean;\n  /** The parent branch's id; drives the drawn hierarchy. */\n  parent?: string;\n}\n\ninterface TreeNode {\n  branch: Branch;\n  depth: number;\n}\n\n/* ─────────────────────────────────────────────────────────\n * GEOMETRY\n *\n *  The list draws a real branch graph, not indent guides:\n *  a branch sits in the lane of its depth, and a curved edge\n *  runs from its parent's node into it. Tuned tight for the\n *  32px popover rows. While searching, the list flattens to\n *  one lane and drops the edges.\n * ───────────────────────────────────────────────────────── */\nconst ROW_H = 32;\nconst LANE_W = 18;\nconst PAD_X = 12;\nconst DOT_R = 3;\nconst CORNER = 8;\n\nconst laneX = (depth: number) => PAD_X + depth * LANE_W;\nconst rowY = (row: number) => row * ROW_H + ROW_H / 2;\n\n/** Depth-first flatten, assigning each branch a depth lane. */\nconst buildTree = (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 });\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 edgePath = (px: number, py: number, cx: number, cy: number) =>\n  `M ${px} ${py} V ${cy - CORNER} Q ${px} ${cy} ${px + CORNER} ${cy} H ${cx}`;\n\nconst Graph = ({\n  nodes,\n  byId,\n  selectedId,\n  drawEdges,\n  width,\n  height,\n}: {\n  nodes: TreeNode[];\n  byId: Map<string, { depth: number; row: number }>;\n  selectedId?: string;\n  drawEdges: boolean;\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    {drawEdges\n      ? nodes.map((node, index) => {\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(\n                laneX(parent.depth),\n                rowY(parent.row),\n                laneX(node.depth),\n                rowY(index)\n              )}\n              fill=\"none\"\n              key={`edge-${node.branch.id}`}\n              strokeLinecap=\"round\"\n              strokeWidth={1.5}\n            />\n          );\n        })\n      : null}\n    {nodes.map((node, index) => {\n      const cx = laneX(node.depth);\n      const cy = rowY(index);\n      const selected = node.branch.id === selectedId;\n      const ringed = node.branch.default || selected;\n      return (\n        <g key={`node-${node.branch.id}`}>\n          {ringed ? (\n            <rect\n              className={selected ? \"stroke-primary\" : \"stroke-primary/40\"}\n              fill=\"none\"\n              height={2 * (DOT_R + 2)}\n              rx={2.5}\n              strokeWidth={1.5}\n              width={2 * (DOT_R + 2)}\n              x={cx - DOT_R - 2}\n              y={cy - DOT_R - 2}\n            />\n          ) : null}\n          <rect\n            className={selected ? \"fill-primary\" : \"fill-muted-foreground/60\"}\n            height={2 * DOT_R}\n            rx={1}\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 BranchBadge = ({ children }: { children: string }) => (\n  <span className=\"shrink-0 rounded-sm border border-primary/40 px-1 py-px font-mono text-[9px] text-primary\">\n    {children}\n  </span>\n);\n\nconst BranchOption = ({\n  node,\n  selected,\n  highlighted,\n  canBranch,\n  onSelect,\n  onBranchFrom,\n  onHighlight,\n}: {\n  node: TreeNode;\n  selected: boolean;\n  highlighted: boolean;\n  canBranch: boolean;\n  onSelect: () => void;\n  onBranchFrom: () => void;\n  onHighlight: () => void;\n}) => (\n  <div\n    aria-selected={selected}\n    className={cn(\n      \"group/row relative flex h-8 items-center rounded-md pr-1 transition-colors\",\n      highlighted && \"bg-muted\"\n    )}\n    data-highlighted={highlighted || undefined}\n    onMouseMove={onHighlight}\n    role=\"option\"\n    tabIndex={-1}\n  >\n    <button\n      className={cn(\n        \"flex h-full min-w-0 flex-1 items-center rounded-md text-left outline-none\",\n        highlighted ? \"text-foreground\" : \"text-foreground/80\"\n      )}\n      onClick={onSelect}\n      style={{ paddingLeft: laneX(node.depth) + DOT_R + 10 }}\n      type=\"button\"\n    >\n      <span className=\"flex min-w-0 flex-1 items-center gap-1.5\">\n        <span className=\"min-w-0 truncate font-mono text-xs\">\n          {node.branch.name}\n        </span>\n        {node.branch.default ? <BranchBadge>default</BranchBadge> : 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    </button>\n\n    <div className=\"flex shrink-0 items-center gap-0.5 pl-1\">\n      {selected ? (\n        <HugeiconsIcon\n          className={cn(\n            \"size-3.5 text-primary\",\n            canBranch && \"group-hover/row:hidden\"\n          )}\n          icon={Tick02Icon}\n          strokeWidth={2}\n        />\n      ) : null}\n      {canBranch ? (\n        <Tooltip>\n          <TooltipTrigger\n            render={\n              <button\n                aria-label={`New branch from ${node.branch.name}`}\n                className=\"flex size-6 scale-75 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 outline-none transition-all duration-150 ease-out hover:bg-muted-foreground/10 hover:text-foreground focus-visible:scale-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50 group-hover/row:scale-100 group-hover/row:opacity-100 motion-reduce:transition-none motion-reduce:group-hover/row:scale-100\"\n                onClick={onBranchFrom}\n                type=\"button\"\n              >\n                <HugeiconsIcon\n                  className=\"size-3.5\"\n                  icon={PlusSignIcon}\n                  strokeWidth={2}\n                />\n              </button>\n            }\n          />\n          <TooltipContent>New branch from {node.branch.name}</TooltipContent>\n        </Tooltip>\n      ) : null}\n    </div>\n  </div>\n);\n\nconst normalize = (value: string) => value.trim().toLowerCase();\n\nexport type BranchPickerProps = Omit<\n  ComponentProps<\"button\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n  /** The branches to choose from. */\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 is chosen. */\n  onValueChange?: (id: string) => void;\n  /** Create a branch off `fromId`. Hides the per-branch affordance when omitted. */\n  onCreateBranch?: (name: string, fromId: string) => void;\n  /** Trigger text when nothing is selected. */\n  placeholder?: string;\n};\n\nexport const BranchPicker = ({\n  branches,\n  value,\n  defaultValue,\n  onValueChange,\n  onCreateBranch,\n  placeholder = \"Select branch\",\n  className,\n  ...props\n}: BranchPickerProps) => {\n  const [internal, setInternal] = useState(defaultValue);\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [highlight, setHighlight] = useState(0);\n  const [createFrom, setCreateFrom] = useState<Branch>();\n  const [newName, setNewName] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n  const createRef = useRef<HTMLInputElement>(null);\n\n  const selectedId = value ?? internal;\n  const selected = branches.find((branch) => branch.id === selectedId);\n\n  const searching = query.trim().length > 0;\n  const nodes: TreeNode[] = searching\n    ? branches\n        .filter((branch) => normalize(branch.name).includes(normalize(query)))\n        .map((branch) => ({ branch, depth: 0 }))\n    : buildTree(branches);\n\n  const byId = new Map(\n    nodes.map((node, index) => [\n      node.branch.id,\n      { depth: node.depth, row: index },\n    ])\n  );\n  const maxDepth = Math.max(0, ...nodes.map((node) => node.depth));\n  const gutterWidth = laneX(maxDepth) + DOT_R + 6;\n\n  useEffect(() => {\n    if (!open) {\n      return;\n    }\n\n    const id = window.setTimeout(() => inputRef.current?.focus(), 0);\n    return () => window.clearTimeout(id);\n  }, [open]);\n\n  useEffect(() => {\n    if (!createFrom) {\n      return;\n    }\n\n    const id = window.setTimeout(() => createRef.current?.focus(), 0);\n    return () => window.clearTimeout(id);\n  }, [createFrom]);\n\n  const handleOpenChange = (next: boolean) => {\n    setOpen(next);\n\n    if (next) {\n      setQuery(\"\");\n      setHighlight(0);\n      setCreateFrom(undefined);\n      setNewName(\"\");\n    }\n  };\n\n  const choose = (id: string) => {\n    if (value === undefined) {\n      setInternal(id);\n    }\n    onValueChange?.(id);\n    setOpen(false);\n  };\n\n  const submitCreate = (event: FormEvent) => {\n    event.preventDefault();\n\n    if (createFrom && newName.trim()) {\n      onCreateBranch?.(newName.trim(), createFrom.id);\n      setOpen(false);\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      setHighlight((current) => Math.min(current + 1, nodes.length - 1));\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      setHighlight((current) => Math.max(current - 1, 0));\n    } else if (event.key === \"Enter\") {\n      const node = nodes[highlight];\n      if (node) {\n        event.preventDefault();\n        choose(node.branch.id);\n      }\n    }\n  };\n\n  return (\n    <Popover onOpenChange={handleOpenChange} open={open}>\n      <PopoverTrigger\n        render={\n          <Button\n            aria-expanded={open}\n            aria-haspopup=\"listbox\"\n            className={cn(\"w-56 justify-between font-mono\", className)}\n            variant=\"outline\"\n            {...props}\n          >\n            <span className=\"flex min-w-0 items-center gap-2\">\n              <HugeiconsIcon\n                className=\"size-3.5 shrink-0 text-muted-foreground\"\n                icon={GitBranchIcon}\n                strokeWidth={2}\n              />\n              <span\n                className={cn(\n                  \"truncate text-xs\",\n                  selected ? \"text-foreground\" : \"text-muted-foreground\"\n                )}\n              >\n                {selected?.name ?? placeholder}\n              </span>\n            </span>\n            <HugeiconsIcon\n              className=\"size-4 shrink-0 text-muted-foreground\"\n              icon={ArrowDown01Icon}\n              strokeWidth={2}\n            />\n          </Button>\n        }\n      />\n      <PopoverContent align=\"start\" className=\"w-72 overflow-hidden p-0\">\n        <TooltipProvider>\n          {createFrom ? (\n            <form\n              className=\"fade-in-0 slide-in-from-right-3 animate-in p-2 duration-200 motion-reduce:animate-none\"\n              onSubmit={submitCreate}\n            >\n              <button\n                className=\"mb-2 flex items-center gap-1 font-mono text-[10px] text-muted-foreground outline-none transition-colors hover:text-foreground\"\n                onClick={() => {\n                  setCreateFrom(undefined);\n                  setNewName(\"\");\n                }}\n                type=\"button\"\n              >\n                <HugeiconsIcon\n                  className=\"size-3\"\n                  icon={ArrowLeft01Icon}\n                  strokeWidth={2}\n                />\n                New branch from{\" \"}\n                <span className=\"text-foreground\">{createFrom.name}</span>\n              </button>\n              <Input\n                className=\"h-8 font-mono text-xs focus-visible:border-primary/50 focus-visible:ring-[3px] focus-visible:ring-primary/15\"\n                onChange={(event) => setNewName(event.target.value)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Escape\") {\n                    setCreateFrom(undefined);\n                    setNewName(\"\");\n                  }\n                }}\n                placeholder=\"branch-name\"\n                ref={createRef}\n                value={newName}\n              />\n              <div className=\"mt-2 flex justify-end gap-2\">\n                <Button\n                  onClick={() => {\n                    setCreateFrom(undefined);\n                    setNewName(\"\");\n                  }}\n                  size=\"xs\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  Cancel\n                </Button>\n                <Button\n                  disabled={newName.trim().length === 0}\n                  size=\"xs\"\n                  type=\"submit\"\n                >\n                  Create branch\n                </Button>\n              </div>\n            </form>\n          ) : (\n            <>\n              <div className=\"flex items-center gap-2 border-border/60 border-b px-2.5\">\n                <HugeiconsIcon\n                  className=\"size-3.5 shrink-0 text-muted-foreground/60\"\n                  icon={Search01Icon}\n                  strokeWidth={2}\n                />\n                <Input\n                  className=\"h-9 rounded-none border-0 bg-transparent px-0 font-mono text-xs shadow-none focus-visible:border-0 focus-visible:ring-0 dark:bg-transparent\"\n                  onChange={(event) => {\n                    setQuery(event.target.value);\n                    setHighlight(0);\n                  }}\n                  onKeyDown={handleKeyDown}\n                  placeholder=\"Search branches\"\n                  ref={inputRef}\n                  value={query}\n                />\n              </div>\n\n              <div\n                className=\"neon-scroll-fade max-h-72 overflow-auto p-1\"\n                role=\"listbox\"\n              >\n                <div className=\"relative\">\n                  {nodes.length > 0 ? (\n                    <Graph\n                      byId={byId}\n                      drawEdges={!searching}\n                      height={nodes.length * ROW_H}\n                      nodes={nodes}\n                      selectedId={selectedId}\n                      width={gutterWidth}\n                    />\n                  ) : null}\n                  {nodes.map((node, index) => (\n                    <BranchOption\n                      canBranch={Boolean(onCreateBranch)}\n                      highlighted={highlight === index}\n                      key={node.branch.id}\n                      node={node}\n                      onBranchFrom={() => {\n                        setCreateFrom(node.branch);\n                        setNewName(\"\");\n                      }}\n                      onHighlight={() => setHighlight(index)}\n                      onSelect={() => choose(node.branch.id)}\n                      selected={node.branch.id === selectedId}\n                    />\n                  ))}\n\n                  {nodes.length === 0 ? (\n                    <p className=\"px-2 py-6 text-center text-muted-foreground text-xs\">\n                      No branches found.\n                    </p>\n                  ) : null}\n                </div>\n              </div>\n            </>\n          )}\n        </TooltipProvider>\n      </PopoverContent>\n    </Popover>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}