import * as React from 'react'; import { ChevronRight, ChevronDown } from 'lucide-react'; import { cn } from '../../shared/utils'; import { useTreeView, type TreeNode } from './use-tree-view'; interface TreeViewProps extends React.HTMLAttributes { data: TreeNode[]; /** Called when a node is clicked or activated via keyboard. */ onNodeClick?: (node: TreeNode) => void; /** Called when a node becomes the selected item. Prefer this over `onNodeClick` for selection logic. */ onNodeSelect?: (node: TreeNode) => void; defaultExpanded?: string[]; /** Controlled selected node ID. When provided, selection state is managed externally. */ selectedNodeId?: string; /** Accessible label for the tree widget. */ ariaLabel?: string; } /** * Hierarchical tree navigation component. * * @description * Renders a nested tree structure for browsing hierarchical data such as file * systems, category trees, or organizational charts. Nodes can be expanded * and collapsed independently. Supports custom icons per node, controlled * selection, and full WAI-ARIA keyboard navigation. * * Headless logic is available via `useTreeView` for custom tree UIs. * * @ai-rules * 1. Each `TreeNode` must have a unique `id`. * 2. Leaf nodes (no `children`) do not render expand arrows. * 3. Use `onNodeSelect` for selection and `onNodeClick` for any click side-effect. * 4. Pass `selectedNodeId` to control selection externally. * 5. Keyboard: Arrow Up/Down navigate siblings, Arrow Right expands / enters child, Arrow Left collapses / goes to parent, Home/End jump to first/last visible node. */ const TreeView = React.forwardRef( ( { className, data, onNodeClick, onNodeSelect, defaultExpanded = [], selectedNodeId, ariaLabel, ...props }, ref ) => { const { expanded, effectiveSelectedId, getNodeRef, toggleExpand, handleSelect, handleKeyDown } = useTreeView({ data, defaultExpanded, selectedNodeId, onNodeClick, onNodeSelect }); const focusableId = effectiveSelectedId ?? data[0]?.id; return (
{data.map(node => ( ))}
); } ); TreeView.displayName = 'TreeView'; interface TreeNodeComponentProps { node: TreeNode; level: number; expanded: Set; selectedId: string | undefined; focusableId: string | undefined; onToggle: (nodeId: string) => void; onSelect: (node: TreeNode) => void; onKeyDown: (e: React.KeyboardEvent, node: TreeNode) => void; getNodeRef: (nodeId: string) => (el: HTMLButtonElement | null) => void; } const TreeNodeComponent: React.FC = ({ node, level, expanded, selectedId, focusableId, onToggle, onSelect, onKeyDown, getNodeRef, }) => { const hasChildren = !!node.children?.length; const isExpanded = expanded.has(node.id); const isSelected = node.id === selectedId; const handleClick = () => { if (hasChildren) onToggle(node.id); onSelect(node); }; return (
{hasChildren && isExpanded && (
{node.children!.map(child => ( ))}
)}
); }; export { TreeView }; export type { TreeViewProps }; export type { TreeNode } from './use-tree-view';