# TreeView

## Overview

An interactive hierarchical tree component for displaying and navigating nested data structures — file systems, organizational charts, category trees, and recursive navigation menus. Supports keyboard navigation (Arrow keys, Home, End, Space), expand/collapse, and single-node selection.

The package also exports a **headless `useTreeView` hook** for building fully custom tree UIs while reusing all the expand/collapse, selection, keyboard navigation, and focus management logic.

---

## Exports

| Export              | Description                              |
| ------------------- | ---------------------------------------- |
| `TreeView`          | Ready-to-use hierarchical tree component |
| `useTreeView`       | Headless hook — all logic, no UI         |
| `TreeNode`          | TypeScript type for a single tree node   |
| `UseTreeViewProps`  | TypeScript props type for the hook       |
| `UseTreeViewReturn` | TypeScript return type for the hook      |

---

## When to Use

- File system browsers
- Organizational hierarchy viewers
- Category/tag tree navigation
- Nested menu items in the assistant sidebar variant

---

## Props

### `TreeView`

| Prop              | Type                       | Required | Description                                                                           |
| ----------------- | -------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `data`            | `TreeNode[]`               | **Yes**  | The tree data structure                                                               |
| `defaultExpanded` | `string[]`                 | No       | Node IDs to expand on initial render                                                  |
| `selectedNodeId`  | `string`                   | No       | Controlled selected node ID                                                           |
| `onNodeClick`     | `(node: TreeNode) => void` | No       | Called when any node is clicked                                                       |
| `onNodeSelect`    | `(node: TreeNode) => void` | No       | Called when a node is selected (leaf or branch)                                       |
| `ariaLabel`       | `string`                   | No       | Accessible label for the `role="tree"` container. Defaults to `"Navegação em árvore"` |
| `className`       | `string`                   | No       | Additional CSS classes                                                                |

### `TreeNode`

```typescript
interface TreeNode {
  id: string;
  label: string;
  icon?: ReactNode;
  children?: TreeNode[];
}
```

---

## Examples

### File System Browser

```tsx
import { TreeView } from 'xertica-ui/ui';
import { Folder, File } from 'lucide-react';

const tree = [
  {
    id: 'src',
    label: 'src',
    icon: <Folder className="size-4" />,
    children: [
      {
        id: 'components',
        label: 'components',
        icon: <Folder className="size-4" />,
        children: [
          { id: 'button', label: 'Button.tsx', icon: <File className="size-4" /> },
          { id: 'card', label: 'Card.tsx', icon: <File className="size-4" /> },
        ],
      },
      { id: 'app', label: 'App.tsx', icon: <File className="size-4" /> },
    ],
  },
];

<TreeView data={tree} onNodeSelect={node => console.log('Selected:', node.id)} />;
```

### Pre-expanded Nodes

```tsx
<TreeView
  data={tree}
  defaultExpanded={['src', 'components']}
  onNodeSelect={node => setSelectedFile(node.id)}
/>
```

---

## `useTreeView` Hook

A headless hook that manages expand/collapse state, selection, keyboard navigation, and focus refs for a tree structure. Use it when the `<TreeView>` component's visual design doesn't fit your needs.

### Props

| Prop              | Type                       | Default | Description                                                                                     |
| ----------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------- |
| `data`            | `TreeNode[]`               | —       | **Required.** The tree data structure                                                           |
| `defaultExpanded` | `string[]`                 | `[]`    | Node IDs to expand on initial render                                                            |
| `selectedNodeId`  | `string`                   | —       | Controlled selected node ID — when provided, the hook uses this value instead of internal state |
| `onNodeClick`     | `(node: TreeNode) => void` | —       | Called when any node is clicked                                                                 |
| `onNodeSelect`    | `(node: TreeNode) => void` | —       | Called when a node is selected                                                                  |

### Return Value

| Property              | Type                                                          | Description                                                                   |
| --------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `expanded`            | `Set<string>`                                                 | Set of currently expanded node IDs                                            |
| `effectiveSelectedId` | `string \| undefined`                                         | The currently selected node ID (controlled or internal)                       |
| `nodeRefs`            | `Map<string, HTMLButtonElement>`                              | Map of node ID → DOM button element refs                                      |
| `getNodeRef`          | `(nodeId: string) => (el: HTMLButtonElement \| null) => void` | Ref callback factory — attach to each node's button element                   |
| `toggleExpand`        | `(nodeId: string) => void`                                    | Toggle the expand/collapse state of a node                                    |
| `handleSelect`        | `(node: TreeNode) => void`                                    | Handle node selection (updates internal state and calls callbacks)            |
| `handleKeyDown`       | `(e: KeyboardEvent, node: TreeNode) => void`                  | Full keyboard navigation handler — attach to each node's `onKeyDown`          |
| `getVisibleNodes`     | `() => TreeNode[]`                                            | Returns the flat list of currently visible (non-collapsed) nodes in DOM order |

### Keyboard Navigation

The `handleKeyDown` handler implements the WAI-ARIA Tree Pattern 1.2:

| Key          | Action                                                                     |
| ------------ | -------------------------------------------------------------------------- |
| `ArrowDown`  | Move focus to the next visible node                                        |
| `ArrowUp`    | Move focus to the previous visible node                                    |
| `ArrowRight` | Expand a collapsed branch node; or move to first child if already expanded |
| `ArrowLeft`  | Collapse an expanded branch node; or move to parent if already collapsed   |
| `Home`       | Move focus to the first node in the tree                                   |
| `End`        | Move focus to the last visible node in the tree                            |
| `Space`      | **Branch nodes**: toggle expand/collapse. **Leaf nodes**: select           |
| `Enter`      | **All nodes**: toggle expand/collapse (if branch) **and** select           |

> **Space vs Enter (v2.1.9+)**: These keys have distinct behaviors. `Space` on a branch expands/collapses without selecting; on a leaf it selects. `Enter` always selects the node and also toggles expand if it has children. This follows WAI-ARIA Tree Pattern specification.

### Roving Tabindex (v2.1.9+)

The `<TreeView>` component uses a roving tabindex strategy: only one node is in the tab sequence at a time (`tabIndex={0}`), the rest have `tabIndex={-1}`. The focusable node is `effectiveSelectedId ?? data[0]?.id`. This means:

- Arrow keys move focus between nodes without changing tab order
- `Tab` exits the tree entirely — screen reader users navigate with Arrow keys inside the tree

### Custom Tree Example

```tsx
import { useTreeView, TreeNode } from 'xertica-ui/ui';
import { ChevronRight, ChevronDown, Folder, File } from 'lucide-react';
import { cn } from '@/shared/utils';

function CustomTree({ data }: { data: TreeNode[] }) {
  const { expanded, effectiveSelectedId, getNodeRef, toggleExpand, handleSelect, handleKeyDown } =
    useTreeView({
      data,
      defaultExpanded: [],
      onNodeSelect: node => console.log('Selected:', node.id),
    });

  const renderNode = (node: TreeNode, level: number): React.ReactNode => {
    const hasChildren = !!node.children?.length;
    const isExpanded = expanded.has(node.id);
    const isSelected = effectiveSelectedId === node.id;

    return (
      <div key={node.id}>
        <button
          ref={getNodeRef(node.id)}
          role="treeitem"
          aria-expanded={hasChildren ? isExpanded : undefined}
          aria-selected={isSelected}
          onClick={() => {
            if (hasChildren) toggleExpand(node.id);
            handleSelect(node);
          }}
          onKeyDown={e => handleKeyDown(e, node)}
          style={{ paddingLeft: `${level * 16}px` }}
          className={cn(
            'flex w-full items-center gap-2 rounded px-2 py-1 text-sm',
            isSelected ? 'bg-primary text-primary-foreground' : 'hover:bg-accent'
          )}
        >
          {hasChildren ? (
            isExpanded ? (
              <ChevronDown className="size-4 shrink-0" />
            ) : (
              <ChevronRight className="size-4 shrink-0" />
            )
          ) : (
            <span className="size-4 shrink-0" />
          )}
          {node.icon}
          <span>{node.label}</span>
        </button>

        {hasChildren && isExpanded && (
          <div role="group">{node.children!.map(child => renderNode(child, level + 1))}</div>
        )}
      </div>
    );
  };

  return (
    <div role="tree" className="w-full">
      {data.map(node => renderNode(node, 0))}
    </div>
  );
}
```

---

## AI Rules

- Each `TreeNode` must have a unique `id` — the component uses it to track expand/collapse state and selection.
- Leaf nodes (no `children`) do not show expand arrows.
- Use `lucide-react` icons with `className="size-4"` consistently across all tree nodes.
- When using `useTreeView`, attach `getNodeRef(node.id)` as the `ref` on each node's button — keyboard navigation (`handleKeyDown`) requires DOM refs to move focus.
- Attach `handleKeyDown` to `onKeyDown` on every node button — omitting it breaks keyboard accessibility.
- `getVisibleNodes()` returns only nodes that are currently visible (not inside a collapsed branch) — use it to compute `ArrowDown`/`ArrowUp` targets.
- For controlled selection, pass `selectedNodeId` — the hook will use it as `effectiveSelectedId` instead of internal state.
- `expanded` is a `Set<string>` — use `expanded.has(nodeId)` to check if a node is expanded.
- Pass `ariaLabel` to `<TreeView>` to provide a meaningful label for screen readers (e.g., `ariaLabel="File system"`, `ariaLabel="Category navigation"`). The default `"Navegação em árvore"` is in Portuguese — always override it for English-language apps.
- Do NOT manually set `tabIndex` on tree item buttons when using `<TreeView>` — roving tabindex is managed automatically since v2.1.9. When using `useTreeView`, implement roving tabindex: `tabIndex={node.id === focusableId ? 0 : -1}` where `focusableId = effectiveSelectedId ?? data[0]?.id`.

---

## Related Components

- [`Sidebar`](./sidebar.md) — Uses TreeView-style navigation for nested menu items
