{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/tree",
  "title": "Tree",
  "description": "Expandable/collapsible tree view",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-symbols.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/tree.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useEffect, useMemo, useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { resolveTerminalSymbol } from \"@/lib/terminal-symbols\";\n\nexport interface TreeNode {\n  key: string;\n  label: string;\n  children?: TreeNode[];\n  icon?: string;\n  disabled?: boolean;\n}\n\nexport interface TreeProps extends InteractionProps {\n  nodes: TreeNode[];\n  onSelect?: (node: TreeNode) => void;\n  defaultExpanded?: string[];\n  expandedIcon?: string;\n  collapsedIcon?: string;\n  leafIcon?: string;\n  \"aria-label\"?: string;\n}\n\ninterface FlatNode {\n  node: TreeNode;\n  depth: number;\n  hasChildren: boolean;\n}\n\nconst flattenTree = (\n  nodes: TreeNode[],\n  expandedKeys: Set<string>,\n  depth = 0\n): FlatNode[] => {\n  const result: FlatNode[] = [];\n  for (const node of nodes) {\n    const hasChildren = Boolean(node.children && node.children.length > 0);\n    result.push({ depth, hasChildren, node });\n    if (hasChildren && expandedKeys.has(node.key)) {\n      const childFlat = flattenTree(\n        node.children ?? [],\n        expandedKeys,\n        depth + 1\n      );\n      result.push(...childFlat);\n    }\n  }\n  return result;\n};\n\nexport const Tree = ({\n  nodes,\n  onSelect,\n  defaultExpanded = [],\n  expandedIcon,\n  collapsedIcon,\n  leafIcon,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"Tree\",\n}: TreeProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const resolvedExpandedIcon =\n    expandedIcon ?? resolveTerminalSymbol(unicode, \"▼\", \"v\");\n  const resolvedCollapsedIcon =\n    collapsedIcon ?? resolveTerminalSymbol(unicode, \"▶\", \">\");\n  const resolvedLeafIcon = leafIcon ?? resolveTerminalSymbol(unicode, \"•\", \"*\");\n  const [expandedKeys, setExpandedKeys] = useState<Set<string>>(\n    new Set(defaultExpanded)\n  );\n  const [activeIndex, setActiveIndex] = useState(0);\n\n  const flatNodes = useMemo(\n    () => flattenTree(nodes, expandedKeys),\n    [nodes, expandedKeys]\n  );\n\n  const { isFocused } = useInteraction(\n    (_input, key) => {\n      const current = flatNodes[activeIndex];\n\n      if (key.upArrow) {\n        setActiveIndex((i) => Math.max(0, i - 1));\n      } else if (key.downArrow) {\n        setActiveIndex((i) => Math.min(flatNodes.length - 1, i + 1));\n      } else if (key.home) {\n        setActiveIndex(0);\n      } else if (key.end) {\n        setActiveIndex(Math.max(0, flatNodes.length - 1));\n      } else if (key.rightArrow || _input === \" \") {\n        if (current?.hasChildren && !expandedKeys.has(current.node.key)) {\n          setExpandedKeys((prev) => new Set([...prev, current.node.key]));\n        }\n      } else if (key.leftArrow) {\n        if (current?.hasChildren && expandedKeys.has(current.node.key)) {\n          setExpandedKeys((prev) => {\n            const next = new Set(prev);\n            next.delete(current.node.key);\n            return next;\n          });\n        }\n      } else if (key.return && current && !current.node.disabled) {\n        onSelect?.(current.node);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  useEffect(() => {\n    setActiveIndex((index) =>\n      Math.min(index, Math.max(0, flatNodes.length - 1))\n    );\n  }, [flatNodes.length]);\n\n  return (\n    <Box flexDirection=\"column\" aria-role=\"list\">\n      <Text aria-label={ariaLabel}>{\"\"}</Text>\n      {flatNodes.map(({ node, depth, hasChildren }, idx) => {\n        const isActive = idx === activeIndex;\n        const isExpanded = expandedKeys.has(node.key);\n\n        const prefix = \"  \".repeat(depth);\n        let indicator: string;\n        if (hasChildren) {\n          indicator = isExpanded ? resolvedExpandedIcon : resolvedCollapsedIcon;\n        } else {\n          indicator = node.icon ?? resolvedLeafIcon;\n        }\n\n        return (\n          <Box\n            key={node.key}\n            flexDirection=\"row\"\n            aria-role=\"listitem\"\n            aria-label={`${node.label}, level ${depth + 1}${\n              hasChildren ? `, ${isExpanded ? \"expanded\" : \"collapsed\"}` : \"\"\n            }${isActive && isFocused ? \", current\" : \"\"}`}\n            aria-state={{\n              disabled: node.disabled || undefined,\n              expanded: hasChildren ? isExpanded : undefined,\n              selected: isActive && isFocused,\n            }}\n          >\n            <Text>{prefix}</Text>\n            <Text\n              color={\n                isActive ? theme.colors.primary : theme.colors.mutedForeground\n              }\n            >\n              {isActive ? \"[\" : \"\"}\n              {indicator}\n              {isActive ? \"] \" : \" \"}\n            </Text>\n            <Text\n              bold={isActive}\n              color={isActive ? theme.colors.primary : theme.colors.foreground}\n            >\n              {node.label}\n            </Text>\n          </Box>\n        );\n      })}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}