# TreeNav

Hierarchical tree navigation for file browsers, folder structures, and nested category lists. Supports expand/collapse, selection state, file/folder icons with extension-based coloring, and controlled/uncontrolled expanded node management.

Pairs with `@particle-academy/fancy-code`'s `CodeEditor` for IDE-style layouts.

## Import

```tsx
import { TreeNav } from "@particle-academy/react-fancy";
import type { TreeNodeData } from "@particle-academy/react-fancy";
```

## Basic Usage

```tsx
const files: TreeNodeData[] = [
  {
    id: "src", label: "src", type: "folder", children: [
      { id: "app.tsx", label: "App.tsx", ext: "tsx" },
      { id: "main.ts", label: "main.ts", ext: "ts" },
      {
        id: "components", label: "components", type: "folder", children: [
          { id: "button.tsx", label: "Button.tsx", ext: "tsx" },
          { id: "modal.tsx", label: "Modal.tsx", ext: "tsx" },
        ],
      },
    ],
  },
  { id: "pkg", label: "package.json", ext: "json" },
  { id: "readme", label: "README.md", ext: "md" },
];

<TreeNav
  nodes={files}
  selectedId={selectedFile}
  onSelect={(id, node) => setSelectedFile(id)}
  defaultExpandedIds={["src"]}
/>
```

## Props

### TreeNav (root)

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| nodes | `TreeNodeData[]` | - | Tree data (required) |
| selectedId | `string` | - | Currently selected node ID |
| onSelect | `(id: string, node: TreeNodeData) => void` | - | Selection callback |
| onNodeContextMenu | `(e: React.MouseEvent, node: TreeNodeData) => void` | - | Right-click callback per node |
| draggable | `boolean` | `false` | Enable drag-and-drop reordering of tree nodes |
| onNodeMove | `(sourceId: string, targetId: string, position: DropPosition) => void` | - | Callback when a node is moved via drag-and-drop within the tree |
| acceptExternalDrops | `boolean` | `false` | Accept drops from outside the tree — OS files, items dragged from other components, etc. |
| onExternalDrop | `(event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void` | - | Fires on external drop. Read `event.dataTransfer.files` for OS file drops or `event.dataTransfer.getData(type)` for custom MIME payloads. |
| expandedIds | `string[]` | - | Controlled expanded node IDs |
| defaultExpandedIds | `string[]` | - | Initial expanded nodes (uncontrolled) |
| onExpandedChange | `(ids: string[]) => void` | - | Callback when expanded state changes |
| defaultExpandAll | `boolean` | `false` | Expand all folders on mount |
| indentSize | `number` | `16` | Indent per nesting level in px |
| showIcons | `boolean` | `true` | Show file/folder icons |
| className | `string` | - | Additional CSS classes |

### TreeNodeData

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| id | `string` | - | Unique identifier (required) |
| label | `string` | - | Display text (required) |
| type | `"file" \| "folder"` | - | Node type (auto-detected from children) |
| ext | `string` | - | File extension for icon coloring (e.g., `"ts"`, `"php"`) |
| children | `TreeNodeData[]` | - | Nested child nodes |
| icon | `ReactNode` | - | Custom icon override |
| disabled | `boolean` | - | Disable the node |

## Context Hook

Access tree state from custom components inside the tree:

```tsx
import { useTreeNav } from "@particle-academy/react-fancy";

function CustomNode() {
  const { selectedId, toggle, expandedIds } = useTreeNav();
  // ...
}
```

| Property | Type | Description |
|----------|------|-------------|
| selectedId | `string \| undefined` | Currently selected node ID |
| onSelect | `(id, node) => void` | Selection callback |
| expandedIds | `string[]` | Currently expanded node IDs |
| toggle | `(id: string) => void` | Toggle a node's expanded state |
| indentSize | `number` | Current indent size |
| showIcons | `boolean` | Whether icons are shown |

## Expand All

```tsx
<TreeNav nodes={files} defaultExpandAll />
```

## Controlled Expanded State

```tsx
const [expanded, setExpanded] = useState(["src", "components"]);

<TreeNav
  nodes={files}
  expandedIds={expanded}
  onExpandedChange={setExpanded}
/>
```

## No Icons

```tsx
<TreeNav nodes={files} showIcons={false} />
```

## Custom Indent

```tsx
<TreeNav nodes={files} indentSize={24} />
```

## File Icon Colors

Icons are automatically colored by file extension:

| Extension | Color |
|-----------|-------|
| `.ts`, `.tsx` | Blue (#3178c6) |
| `.js`, `.jsx` | Yellow (#f7df1e) |
| `.php` | Purple (#777bb4) |
| `.html`, `.htm` | Orange (#e34c26) |
| `.css` | Blue (#264de4) |
| `.json` | Gray (#a1a1aa) |
| `.md` | Gray (#71717a) |
| `.yaml`, `.yml` | Red (#cb171e) |

Override with the `icon` field on any node:

```tsx
{ id: "special", label: "config", icon: <GearIcon /> }
```

## Drag and Drop

Enable drag-and-drop reordering with the `draggable` prop. The tree itself is never mutated — your `onNodeMove` callback receives the source, target, and position, and you update state accordingly.

```tsx
import { TreeNav } from "@particle-academy/react-fancy";
import type { TreeNodeData, DropPosition } from "@particle-academy/react-fancy";

const [files, setFiles] = useState<TreeNodeData[]>(initialFiles);

function handleNodeMove(sourceId: string, targetId: string, position: DropPosition) {
  // position is "before", "after", or "inside" (folders only)
  setFiles((prev) => moveNode(prev, sourceId, targetId, position));
}

<TreeNav
  nodes={files}
  draggable
  onNodeMove={handleNodeMove}
/>
```

**Drop positions:**
- `"before"` — insert above the target node (thin blue line indicator)
- `"after"` — insert below the target node (thin blue line indicator)
- `"inside"` — drop into a folder (blue highlight on the folder)

**Built-in safety:**
- Cannot drop a node onto itself
- Cannot drop a node into its own descendants
- Folders auto-expand after 500ms when dragging over them
- Disabled nodes cannot be dragged

## External Drops (files, cross-component)

Set `acceptExternalDrops` and supply `onExternalDrop` to accept payloads from outside the tree — OS files dragged from the desktop, items dragged from a Kanban card, anything that ships a `dataTransfer`. The same `before` / `after` / `inside` indicators apply, and folders still auto-expand.

```tsx
<TreeNav
  nodes={files}
  acceptExternalDrops
  onExternalDrop={(event, target, position) => {
    // OS file drop?
    if (event.dataTransfer.files.length > 0) {
      uploadFiles(event.dataTransfer.files, target.id);
      return;
    }
    // Custom payload from another component?
    const payload = event.dataTransfer.getData("application/x-card-id");
    if (payload) attachCardToNode(payload, target.id, position);
  }}
/>
```

`acceptExternalDrops` and `draggable` are independent — enable either, both, or neither. With both on, internal drag-reorder and external drops coexist; the handler distinguishes via the absence of an internal drag source.

## Context Menu

Use `onNodeContextMenu` with the `ContextMenu` component to add right-click menus per node:

```tsx
import { TreeNav, ContextMenu } from "@particle-academy/react-fancy";

const [ctxNode, setCtxNode] = useState<TreeNodeData | null>(null);

<ContextMenu>
  <ContextMenu.Trigger>
    <TreeNav
      nodes={files}
      selectedId={selectedFile}
      onSelect={handleSelect}
      onNodeContextMenu={(e, node) => setCtxNode(node)}
    />
  </ContextMenu.Trigger>
  <ContextMenu.Content>
    {ctxNode?.type === "folder" ? (
      <>
        <ContextMenu.Item>New File</ContextMenu.Item>
        <ContextMenu.Item>New Folder</ContextMenu.Item>
      </>
    ) : (
      <>
        <ContextMenu.Item onClick={() => openFile(ctxNode)}>
          Open File
        </ContextMenu.Item>
        <ContextMenu.Item onClick={() => copyName(ctxNode)}>
          Copy File Name
        </ContextMenu.Item>
        <ContextMenu.Separator />
        <ContextMenu.Item danger>Delete</ContextMenu.Item>
      </>
    )}
  </ContextMenu.Content>
</ContextMenu>
```

The `onNodeContextMenu` callback fires with the mouse event and the node data. Wrap TreeNav in `ContextMenu.Trigger` to let the ContextMenu handle positioning and open/close — the callback just tracks which node was right-clicked so you can render different menu items for files vs folders.

## IDE Layout Example

Pair with `@particle-academy/fancy-code` for a full IDE:

```tsx
import { TreeNav } from "@particle-academy/react-fancy";
import { CodeEditor } from "@particle-academy/fancy-code";

<div className="flex" style={{ height: 600 }}>
  <div className="w-56 shrink-0 overflow-y-auto border-r p-2">
    <TreeNav
      nodes={fileTree}
      selectedId={activeFile}
      onSelect={(id, node) => openFile(id, node)}
      defaultExpandedIds={["src"]}
      indentSize={12}
    />
  </div>
  <div className="flex-1">
    <CodeEditor value={code} onChange={setCode} language={lang}>
      <CodeEditor.Toolbar />
      <CodeEditor.Panel />
      <CodeEditor.StatusBar />
    </CodeEditor>
  </div>
</div>
```

## Data Attributes

| Attribute | Element |
|-----------|---------|
| `data-react-fancy-tree-nav` | Root nav element |
| `data-react-fancy-tree-node` | Each tree node wrapper |
| `data-react-fancy-tree-node-children` | Children container of an expanded node |
