{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "type": "registry:component",
  "title": "Tree View",
  "description": "Hierarchical tree component for nested data with controlled state, single/multi-select, and keyboard navigation.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/tree-view/tree-view.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Selection mode for {@link TreeView}.\n *\n * @public\n */\nexport type TreeViewSelectionMode = \"multiple\" | \"single\";\n\n/**\n * A node in the tree.\n *\n * @public\n */\nexport type TreeNode = {\n  /** When `true`, the node renders dimmed and ignores clicks. */\n  disabled?: boolean;\n  /** Optional leading icon. */\n  icon?: ReactNode;\n  /** Stable identifier. */\n  id: string;\n  /** Visible label. */\n  label: ReactNode;\n  /** Child nodes; the node renders as a branch when present. */\n  nodes?: TreeNode[];\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type TreeViewLabels = {\n  /** Aria-label for the tree. Defaults to `\"Tree\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Tree\",\n} as const satisfies Required<TreeViewLabels>;\n\n/**\n * Props for {@link TreeView}.\n *\n * @public\n */\nexport type TreeViewProps = {\n  /** Default expanded ids (uncontrolled). */\n  defaultExpanded?: string[];\n  /** Default selected ids (uncontrolled). */\n  defaultSelected?: string[];\n  /** Controlled expanded ids. */\n  expanded?: string[];\n  /** Localizable strings. */\n  labels?: TreeViewLabels;\n  /** Tree data. */\n  nodes: TreeNode[];\n  /** Fires when expanded ids change. */\n  onExpandedChange?: (next: string[]) => void;\n  /** Fires when selection changes. */\n  onSelect?: (next: string[]) => void;\n  /** Controlled selected ids. */\n  selected?: string[];\n  /** Selection mode. Defaults to `\"single\"`. */\n  selectionMode?: TreeViewSelectionMode;\n} & Omit<ComponentPropsWithoutRef<\"ul\">, \"onSelect\">;\n\ntype FlatNode = {\n  depth: number;\n  hasChildren: boolean;\n  node: TreeNode;\n  parentId?: string;\n};\n\ntype FlattenArguments = {\n  depth?: number;\n  expanded: ReadonlySet<string>;\n  nodes: TreeNode[];\n  parentId?: string;\n};\n\nfunction flattenVisible(arguments_: FlattenArguments): FlatNode[] {\n  const { depth = 0, expanded, nodes, parentId } = arguments_;\n  return nodes.flatMap((node) => {\n    const hasChildren = (node.nodes?.length ?? 0) > 0;\n    const head: FlatNode = { depth, hasChildren, node, parentId };\n    if (!hasChildren || !expanded.has(node.id)) return [head];\n    return [\n      head,\n      ...flattenVisible({\n        depth: depth + 1,\n        expanded,\n        nodes: node.nodes ?? [],\n        parentId: node.id,\n      }),\n    ];\n  });\n}\n\nfunction useControlled<T>(\n  controlled: T | undefined,\n  defaultValue: T,\n): readonly [T, (next: T) => void, boolean] {\n  const [internal, setInternal] = useState<T>(defaultValue);\n  const isControlled = controlled !== undefined;\n  const value = isControlled ? controlled : internal;\n  const setValue = useCallback(\n    (next: T) => {\n      if (!isControlled) setInternal(next);\n    },\n    [isControlled],\n  );\n  return [value, setValue, isControlled];\n}\n\nfunction toggleSet(set: ReadonlySet<string>, id: string): string[] {\n  const next = new Set(set);\n  if (next.has(id)) next.delete(id);\n  else next.add(id);\n  return [...next];\n}\n\ntype TreeRowProps = {\n  active: boolean;\n  expanded: boolean;\n  flat: FlatNode;\n  onActivate: (id: string) => void;\n  onExpand: (id: string) => void;\n  onSelect: (id: string) => void;\n  selected: boolean;\n};\n\nfunction TreeRow({\n  active,\n  expanded,\n  flat,\n  onActivate,\n  onExpand,\n  onSelect,\n  selected,\n}: TreeRowProps): ReactNode {\n  const { depth, hasChildren, node } = flat;\n  const activate = (): void => {\n    if (node.disabled) return;\n    onActivate(node.id);\n    if (hasChildren) onExpand(node.id);\n    else onSelect(node.id);\n  };\n  const handleKeyDown = (event: ReactKeyboardEvent<HTMLLIElement>): void => {\n    if (event.key !== \"Enter\" && event.key !== \" \") return;\n    event.preventDefault();\n    activate();\n  };\n  return (\n    <li\n      aria-disabled={node.disabled || undefined}\n      aria-expanded={hasChildren ? expanded : undefined}\n      aria-selected={selected || undefined}\n      className={cn(\n        \"flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-sm focus:outline-none\",\n        active ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent/60\",\n        selected ? \"ring-1 ring-primary\" : \"\",\n        node.disabled ? \"cursor-not-allowed opacity-50\" : \"\",\n      )}\n      data-active={active ? \"true\" : undefined}\n      data-depth={depth}\n      data-node-id={node.id}\n      data-selected={selected ? \"true\" : undefined}\n      onClick={activate}\n      onKeyDown={handleKeyDown}\n      role=\"treeitem\"\n      style={{ paddingLeft: `${(depth * 16 + 8).toString()}px` }}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"inline-flex size-4 shrink-0 items-center justify-center text-xs text-muted-foreground transition-transform\",\n          hasChildren ? \"\" : \"opacity-0\",\n          expanded ? \"rotate-90\" : \"\",\n        )}\n      >\n        ▸\n      </span>\n      {node.icon ? (\n        <span aria-hidden=\"true\" className=\"inline-flex size-4 shrink-0\">\n          {node.icon}\n        </span>\n      ) : null}\n      <span className=\"truncate\">{node.label}</span>\n    </li>\n  );\n}\n\nfunction nextActiveId(\n  flat: FlatNode[],\n  delta: number,\n  current?: string,\n): string | undefined {\n  if (flat.length === 0) return undefined;\n  if (!current) return flat[0]?.node.id;\n  const index = flat.findIndex((entry) => entry.node.id === current);\n  if (index < 0) return flat[0]?.node.id;\n  const nextIndex = Math.min(Math.max(index + delta, 0), flat.length - 1);\n  return flat[nextIndex]?.node.id;\n}\n\nfunction findFlat(flat: FlatNode[], id?: string): FlatNode | undefined {\n  if (!id) return undefined;\n  return flat.find((entry) => entry.node.id === id);\n}\n\nfunction useTreeState(arguments_: {\n  defaultExpanded?: string[];\n  defaultSelected?: string[];\n  expanded?: string[];\n  onExpandedChange?: (next: string[]) => void;\n  onSelect?: (next: string[]) => void;\n  selected?: string[];\n  selectionMode: TreeViewSelectionMode;\n}): {\n  applyExpand: (id: string) => void;\n  applySelect: (id: string) => void;\n  expandedSet: ReadonlySet<string>;\n  selectedSet: ReadonlySet<string>;\n} {\n  const {\n    defaultExpanded = [],\n    defaultSelected = [],\n    expanded,\n    onExpandedChange,\n    onSelect,\n    selected,\n    selectionMode,\n  } = arguments_;\n  const [expandedState, setExpandedState] = useControlled<string[]>(\n    expanded,\n    defaultExpanded,\n  );\n  const [selectedState, setSelectedState] = useControlled<string[]>(\n    selected,\n    defaultSelected,\n  );\n  const expandedSet = useMemo(() => new Set(expandedState), [expandedState]);\n  const selectedSet = useMemo(() => new Set(selectedState), [selectedState]);\n\n  const applyExpand = useCallback(\n    (id: string) => {\n      const next = toggleSet(expandedSet, id);\n      setExpandedState(next);\n      onExpandedChange?.(next);\n    },\n    [expandedSet, onExpandedChange, setExpandedState],\n  );\n\n  const applySelect = useCallback(\n    (id: string) => {\n      const next =\n        selectionMode === \"single\" ? [id] : toggleSet(selectedSet, id);\n      setSelectedState(next);\n      onSelect?.(next);\n    },\n    [onSelect, selectedSet, selectionMode, setSelectedState],\n  );\n\n  return { applyExpand, applySelect, expandedSet, selectedSet };\n}\n\nfunction useKeyboardHandler(arguments_: {\n  activeId?: string;\n  applyExpand: (id: string) => void;\n  applySelect: (id: string) => void;\n  expandedSet: ReadonlySet<string>;\n  flat: FlatNode[];\n  setActiveId: (id?: string) => void;\n}): (event: ReactKeyboardEvent<HTMLUListElement>) => void {\n  const { activeId, applyExpand, applySelect, expandedSet, flat, setActiveId } =\n    arguments_;\n  return useCallback(\n    (event) => {\n      const current = findFlat(flat, activeId);\n      if (event.key === \"ArrowDown\") {\n        event.preventDefault();\n        setActiveId(nextActiveId(flat, 1, activeId));\n        return;\n      }\n      if (event.key === \"ArrowUp\") {\n        event.preventDefault();\n        setActiveId(nextActiveId(flat, -1, activeId));\n        return;\n      }\n      if (event.key === \"ArrowRight\" && current?.hasChildren) {\n        event.preventDefault();\n        if (!expandedSet.has(current.node.id)) applyExpand(current.node.id);\n        return;\n      }\n      if (event.key === \"ArrowLeft\" && current) {\n        event.preventDefault();\n        if (current.hasChildren && expandedSet.has(current.node.id)) {\n          applyExpand(current.node.id);\n        } else if (current.parentId) {\n          setActiveId(current.parentId);\n        }\n        return;\n      }\n      if (event.key === \"Enter\" || event.key === \" \") {\n        if (!current || current.node.disabled) return;\n        event.preventDefault();\n        if (current.hasChildren) applyExpand(current.node.id);\n        else applySelect(current.node.id);\n      }\n    },\n    [activeId, applyExpand, applySelect, expandedSet, flat, setActiveId],\n  );\n}\n\ntype TreeRowsProps = {\n  activeId?: string;\n  applyExpand: (id: string) => void;\n  applySelect: (id: string) => void;\n  expandedSet: ReadonlySet<string>;\n  flat: FlatNode[];\n  selectedSet: ReadonlySet<string>;\n  setActiveId: (id?: string) => void;\n};\n\nfunction TreeRows({\n  activeId,\n  applyExpand,\n  applySelect,\n  expandedSet,\n  flat,\n  selectedSet,\n  setActiveId,\n}: TreeRowsProps): ReactNode {\n  return (\n    <>\n      {flat.map((entry) => (\n        <TreeRow\n          active={entry.node.id === activeId}\n          expanded={expandedSet.has(entry.node.id)}\n          flat={entry}\n          key={entry.node.id}\n          onActivate={setActiveId}\n          onExpand={applyExpand}\n          onSelect={applySelect}\n          selected={selectedSet.has(entry.node.id)}\n        />\n      ))}\n    </>\n  );\n}\n\n/**\n * Hierarchical tree component for nested data (file systems, categories,\n * org charts). Pass {@link TreeNode} data via `nodes`. Supports controlled\n * and uncontrolled expand/select state, single or multi-select, and full\n * keyboard navigation (arrows expand/collapse and traverse, enter/space\n * activates).\n *\n * @example\n * ```tsx\n * <TreeView\n *   nodes={[\n *     { id: \"src\", label: \"src/\", nodes: [\n *       { id: \"components\", label: \"components/\" },\n *       { id: \"utils\", label: \"utils/\" },\n *     ]},\n *   ]}\n *   defaultExpanded={[\"src\"]}\n *   onSelect={(ids) => console.info(ids)}\n * />\n * ```\n *\n * @public\n */\nexport const TreeView = forwardRef<HTMLUListElement, TreeViewProps>(\n  (props, ref) => {\n    const {\n      className,\n      defaultExpanded,\n      defaultSelected,\n      expanded,\n      labels,\n      nodes,\n      onExpandedChange,\n      onSelect,\n      selected,\n      selectionMode = \"single\",\n      ...rest\n    } = props;\n    const resolvedLabels = useMemo(\n      () => ({ ...DEFAULT_LABELS, ...labels }),\n      [labels],\n    );\n\n    const { applyExpand, applySelect, expandedSet, selectedSet } = useTreeState(\n      {\n        defaultExpanded,\n        defaultSelected,\n        expanded,\n        onExpandedChange,\n        onSelect,\n        selected,\n        selectionMode,\n      },\n    );\n\n    const flat = useMemo(\n      () => flattenVisible({ expanded: expandedSet, nodes }),\n      [expandedSet, nodes],\n    );\n\n    const [activeId, setActiveId] = useState<string | undefined>(\n      () => flat[0]?.node.id,\n    );\n\n    const handleKeyDown = useKeyboardHandler({\n      activeId,\n      applyExpand,\n      applySelect,\n      expandedSet,\n      flat,\n      setActiveId,\n    });\n\n    return (\n      <ul\n        aria-label={resolvedLabels.region}\n        aria-multiselectable={selectionMode === \"multiple\" || undefined}\n        className={cn(\n          \"flex flex-col gap-0.5 rounded-2xl border bg-background p-2 text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        onKeyDown={handleKeyDown}\n        ref={ref}\n        role=\"tree\"\n        tabIndex={0}\n        {...rest}\n      >\n        <TreeRows\n          activeId={activeId}\n          applyExpand={applyExpand}\n          applySelect={applySelect}\n          expandedSet={expandedSet}\n          flat={flat}\n          selectedSet={selectedSet}\n          setActiveId={setActiveId}\n        />\n      </ul>\n    );\n  },\n);\nTreeView.displayName = \"TreeView\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
