{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/tree-view/tree-view.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ChevronRight, File, Folder, FolderOpen } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { TreeViewContext, type TreeViewContextValue } from './context'\nimport type { TreeViewItem } from './types'\n\nexport interface TreeViewProps {\n  items: TreeViewItem[]\n  className?: string\n  showIcons?: boolean\n  showCheckboxes?: boolean\n  defaultExpanded?: boolean\n  selectedId?: string | null\n  onSelect?: (item: TreeViewItem) => void\n  onToggle?: (item: TreeViewItem) => void\n  onSelectedIdChange?: (id: string | null) => void\n}\n\nfunction collectExpandable(items: TreeViewItem[], acc: Set<string>) {\n  for (const it of items) {\n    if (it.children?.length) {\n      acc.add(it.id)\n      collectExpandable(it.children, acc)\n    }\n  }\n}\n\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(\n  (\n    {\n      items,\n      className,\n      showIcons = true,\n      showCheckboxes = false,\n      defaultExpanded = false,\n      selectedId: selectedIdProp = null,\n      onSelect,\n      onToggle,\n      onSelectedIdChange,\n    },\n    ref,\n  ) => {\n    const [expandedIds, setExpandedIds] = React.useState<Set<string>>(() => {\n      if (!defaultExpanded) return new Set()\n      const next = new Set<string>()\n      collectExpandable(items, next)\n      return next\n    })\n    const [selectedId, setSelectedId] = React.useState<string | null>(selectedIdProp)\n\n    // Mirror the Vue `watch(() => props.selectedId)` — keep internal state in\n    // sync when the controlled prop changes.\n    React.useEffect(() => {\n      setSelectedId(selectedIdProp)\n    }, [selectedIdProp])\n\n    const toggle = React.useCallback(\n      (item: TreeViewItem) => {\n        if (item.disabled) return\n        setExpandedIds((prev) => {\n          const next = new Set(prev)\n          if (next.has(item.id)) next.delete(item.id)\n          else next.add(item.id)\n          return next\n        })\n        onToggle?.(item)\n      },\n      [onToggle],\n    )\n\n    const select = React.useCallback(\n      (item: TreeViewItem) => {\n        if (item.disabled) return\n        setSelectedId(item.id)\n        onSelect?.(item)\n        onSelectedIdChange?.(item.id)\n      },\n      [onSelect, onSelectedIdChange],\n    )\n\n    const ctx = React.useMemo<TreeViewContextValue>(\n      () => ({ expandedIds, selectedId, showIcons, showCheckboxes, toggle, select }),\n      [expandedIds, selectedId, showIcons, showCheckboxes, toggle, select],\n    )\n\n    return (\n      <TreeViewContext.Provider value={ctx}>\n        <div ref={ref} className={cn('text-sm', className)} role=\"tree\">\n          {items.map((item, i) => (\n            <TreeViewNode key={item.id} item={item} depth={0} isLast={i === items.length - 1} />\n          ))}\n        </div>\n      </TreeViewContext.Provider>\n    )\n  },\n)\nTreeView.displayName = 'TreeView'\n\nexport interface TreeViewNodeProps {\n  item: TreeViewItem\n  depth: number\n  isLast?: boolean\n}\n\n// 20px per level. Connector lives in parent's gutter (depth - 1).\nconst INDENT = 20\n\nfunction TreeViewNode({ item, depth, isLast = false }: TreeViewNodeProps) {\n  const ctx = React.useContext(TreeViewContext)\n  if (!ctx) throw new Error('TreeViewNode must be used inside <TreeView>')\n\n  const isExpanded = ctx.expandedIds.has(item.id)\n  const isSelected = ctx.selectedId === item.id\n  const hasChildren = !!(item.children && item.children.length)\n\n  let Icon: typeof File | null = null\n  if (ctx.showIcons) {\n    if (item.icon) Icon = item.icon\n    else if (!hasChildren) Icon = File\n    else Icon = isExpanded ? FolderOpen : Folder\n  }\n\n  const rowPadLeft = `${depth * INDENT + 4}px`\n  const connectorLeft = `${(depth - 1) * INDENT + 10}px`\n\n  const handleToggle = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    ctx.toggle(item)\n  }\n\n  const handleSelect = () => {\n    if (item.disabled) return\n    ctx.select(item)\n  }\n\n  return (\n    <div role=\"treeitem\" aria-expanded={hasChildren ? isExpanded : undefined} className=\"relative\">\n      {/* Discord-style elbow + trunk for non-root nodes. The elbow points\n          from the parent's chevron column down to this row's center; the\n          trunk continues to the next sibling at this depth (omitted on the\n          last sibling). */}\n      {depth > 0 && (\n        <span\n          aria-hidden=\"true\"\n          className=\"border-border pointer-events-none absolute top-0 h-4 w-3 rounded-bl-md border-b border-l\"\n          style={{ left: connectorLeft }}\n        />\n      )}\n      {depth > 0 && !isLast && (\n        <span\n          aria-hidden=\"true\"\n          className=\"bg-border pointer-events-none absolute top-4 bottom-0 w-px\"\n          style={{ left: connectorLeft }}\n        />\n      )}\n\n      {/* Row */}\n      <div\n        className={cn(\n          'group relative flex h-8 cursor-pointer items-center gap-1.5 rounded-md pr-2 text-sm transition-colors',\n          'hover:bg-accent hover:text-accent-foreground',\n          'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',\n          item.disabled && 'cursor-not-allowed opacity-50',\n          isSelected && 'bg-accent text-accent-foreground font-medium',\n        )}\n        style={{ paddingLeft: rowPadLeft }}\n        tabIndex={item.disabled ? -1 : 0}\n        role=\"button\"\n        onClick={handleSelect}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault()\n            handleSelect()\n          }\n        }}\n      >\n        {/* Chevron (or 16px spacer for leaves so labels align) */}\n        {hasChildren ? (\n          <button\n            type=\"button\"\n            className={cn(\n              'focus-visible:ring-ring flex size-4 shrink-0 items-center justify-center rounded transition-transform duration-150 focus-visible:ring-2 focus-visible:outline-none',\n              'hover:bg-foreground/10',\n              isExpanded && 'rotate-90',\n            )}\n            aria-label={isExpanded ? 'Collapse' : 'Expand'}\n            onClick={handleToggle}\n          >\n            <ChevronRight className=\"text-muted-foreground size-3.5\" aria-hidden=\"true\" />\n          </button>\n        ) : (\n          <span className=\"size-4 shrink-0\" />\n        )}\n\n        {/* Checkbox */}\n        {ctx.showCheckboxes && (\n          <input\n            type=\"checkbox\"\n            checked={!!item.selected}\n            disabled={item.disabled}\n            className=\"border-input bg-background text-primary focus:ring-ring focus-visible:ring-ring size-3.5 shrink-0 rounded focus:ring-1 focus-visible:ring-2 focus-visible:outline-none\"\n            onChange={handleSelect}\n            onClick={(e) => e.stopPropagation()}\n          />\n        )}\n\n        {/* Icon */}\n        {Icon && (\n          <Icon className={cn('size-4 shrink-0', hasChildren ? 'text-primary' : 'text-muted-foreground')} />\n        )}\n\n        {/* Label */}\n        <span className=\"flex-1 truncate\">{item.label}</span>\n      </div>\n\n      {/* Children */}\n      {hasChildren && isExpanded && (\n        <div role=\"group\">\n          {item.children!.map((child, j) => (\n            <TreeViewNode\n              key={child.id}\n              item={child}\n              depth={depth + 1}\n              isLast={j === (item.children?.length ?? 0) - 1}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\nTreeViewNode.displayName = 'TreeViewNode'\n\nexport { TreeView, TreeViewNode }\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-view/tree-view.tsx"
    },
    {
      "path": "packages/registry-react/components/tree-view/context.ts",
      "content": "import * as React from 'react'\nimport type { TreeViewItem } from './types'\n\nexport interface TreeViewContextValue {\n  expandedIds: Set<string>\n  selectedId: string | null\n  showIcons: boolean\n  showCheckboxes: boolean\n  toggle: (item: TreeViewItem) => void\n  select: (item: TreeViewItem) => void\n}\n\n// The Vue source threads the tree state from <TreeView> down to each\n// <TreeViewNode> via provide/inject. React's equivalent is context — the Root\n// sets it, every recursing node reads it. `null` default lets a node throw if\n// rendered outside a <TreeView>.\nexport const TreeViewContext = React.createContext<TreeViewContextValue | null>(null)\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-view/context.ts"
    },
    {
      "path": "packages/registry-react/components/tree-view/types.ts",
      "content": "import type { LucideIcon } from 'lucide-react'\n\nexport interface TreeViewItem {\n  id: string\n  label: string\n  icon?: LucideIcon\n  children?: TreeViewItem[]\n  disabled?: boolean\n  selected?: boolean\n  expanded?: boolean\n  [key: string]: unknown\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-view/types.ts"
    },
    {
      "path": "packages/registry-react/components/tree-view/index.ts",
      "content": "export { TreeView, TreeViewNode, type TreeViewProps, type TreeViewNodeProps } from './tree-view'\nexport { type TreeViewItem } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/tree-view/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Indented tree of expandable nodes — file browsers, taxonomy editors, nested settings. Discord-style elbow connectors, lazy-load branches, and full keyboard navigation.",
  "categories": [
    "data-display"
  ]
}