# Tree

A hierarchy of items that open and close, such as files and folders.

Source: [tree.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/tree.tsx)

```tsx
import { FileCodeIcon, FileImageIcon, FileJsonIcon, FileTextIcon, FolderIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";

export default function TreeFileTree() {
  return (
    <Tree
      aria-label="Files"
      defaultExpanded={["src", "src/components"]}
      defaultValue="src/components/button.tsx"
      className="w-full max-w-64"
    >
      <TreeItem value="src" label="src" icon={<FolderIcon />}>
        <TreeItem value="src/components" label="components" icon={<FolderIcon />}>
          <TreeItem value="src/components/button.tsx" label="button.tsx" icon={<FileCodeIcon />} />
          <TreeItem value="src/components/card.tsx" label="card.tsx" icon={<FileCodeIcon />} />
          <TreeItem value="src/components/dialog.tsx" label="dialog.tsx" icon={<FileCodeIcon />} />
        </TreeItem>
        <TreeItem value="src/hooks" label="hooks" icon={<FolderIcon />}>
          <TreeItem
            value="src/hooks/use-media-query.ts"
            label="use-media-query.ts"
            icon={<FileCodeIcon />}
          />
        </TreeItem>
        <TreeItem value="src/app.tsx" label="app.tsx" icon={<FileCodeIcon />} />
        <TreeItem value="src/main.tsx" label="main.tsx" icon={<FileCodeIcon />} />
      </TreeItem>
      <TreeItem value="public" label="public" icon={<FolderIcon />}>
        <TreeItem value="public/favicon.svg" label="favicon.svg" icon={<FileImageIcon />} />
      </TreeItem>
      <TreeItem value="package.json" label="package.json" icon={<FileJsonIcon />} />
      <TreeItem value="README.md" label="README.md" icon={<FileTextIcon />} />
    </Tree>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/tree
```

## Usage

```tsx
import { FileIcon, FolderIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";

export function Files() {
  return (
    <Tree aria-label="Files" defaultExpanded={["src"]}>
      <TreeItem value="src" label="src" icon={<FolderIcon />}>
        <TreeItem value="src/app.tsx" label="app.tsx" icon={<FileIcon />} />
      </TreeItem>
      <TreeItem value="README.md" label="README.md" icon={<FileIcon />} />
    </Tree>
  );
}
```

Give `Tree` an `aria-label`, and each `TreeItem` a `value` that's unique within the tree. An item
with `TreeItem`s inside it is a folder that opens and closes, and `children={[]}` makes an empty
folder. Clicking an item selects it, and clicking a folder also opens or closes it. Clicking only
the caret opens or closes it without selecting it.

The tree is a single stop in the tab order, which lands on the selected item, or on the folder that
holds it while that folder is closed. Inside it, the up and down arrows move between items, right
opens a folder or moves into it, and left closes it or moves to its parent. Home and End jump to
the first and last items, typing jumps to the next item whose label starts with what's typed, and
`*` opens every folder beside the current one. Enter or Space selects an item, and Enter also opens
or closes a folder.

## Composition

- `Tree`
  - `TreeItem`
    - `TreeItem`

## Examples

### From data

Render a nested array recursively, using each item's path as its `value`. Items can come from your
own components, as long as each renders a `TreeItem`. A closed folder keeps its items in the page,
hidden, so the tree always knows where the selected item is.

```tsx
import { FileIcon, FolderIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";

type Entry = { name: string; children?: Entry[] };

const files: Entry[] = [
  {
    name: "docs",
    children: [
      { name: "getting-started.md" },
      { name: "guides", children: [{ name: "deploying.md" }, { name: "theming.md" }] },
    ],
  },
  { name: "scripts", children: [{ name: "build.mjs" }, { name: "release.mjs" }] },
  { name: "LICENSE" },
];

function FileNode({ entry, path }: { entry: Entry; path: string }) {
  return (
    <TreeItem value={path} label={entry.name} icon={entry.children ? <FolderIcon /> : <FileIcon />}>
      {entry.children?.map((child) => (
        <FileNode key={child.name} entry={child} path={`${path}/${child.name}`} />
      ))}
    </TreeItem>
  );
}

export default function TreeFromData() {
  return (
    <Tree aria-label="Files" defaultExpanded={["docs"]} className="w-full max-w-64">
      {files.map((entry) => (
        <FileNode key={entry.name} entry={entry} path={entry.name} />
      ))}
    </Tree>
  );
}
```

### Controlled

Pass `expanded` and `onExpandedChange` to control which folders are open, and `value` and
`onValueChange` to control which item is selected.

```tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Tree, TreeItem } from "@/components/ui/tree";

const folders = ["design", "design/components", "engineering"];

export default function TreeControlled() {
  const [expanded, setExpanded] = React.useState(["design"]);
  const [value, setValue] = React.useState("design/components/buttons");

  return (
    <div className="flex w-full max-w-64 flex-col gap-3">
      <div className="flex gap-2">
        <Button variant="secondary" size="sm" onClick={() => setExpanded(folders)}>
          Expand all
        </Button>
        <Button variant="secondary" size="sm" onClick={() => setExpanded([])}>
          Collapse all
        </Button>
      </div>
      <Tree
        aria-label="Projects"
        expanded={expanded}
        onExpandedChange={setExpanded}
        value={value}
        onValueChange={setValue}
      >
        <TreeItem value="design" label="Design">
          <TreeItem value="design/components" label="Components">
            <TreeItem value="design/components/buttons" label="Buttons" />
            <TreeItem value="design/components/inputs" label="Inputs" />
          </TreeItem>
          <TreeItem value="design/icons" label="Icons" />
        </TreeItem>
        <TreeItem value="engineering" label="Engineering">
          <TreeItem value="engineering/api" label="API" />
          <TreeItem value="engineering/web" label="Web" />
        </TreeItem>
      </Tree>
      <p className="text-sm text-muted-foreground">
        Selected: <span className="text-foreground">{value}</span>
      </p>
    </div>
  );
}
```

### Without guides

Lines run down each open folder's items, and the line beside the selected item darkens. Set
`guides={false}` to leave just the indentation.

```tsx
import { Tree, TreeItem } from "@/components/ui/tree";

export default function TreeWithoutGuides() {
  return (
    <Tree
      aria-label="Settings"
      guides={false}
      defaultExpanded={["workspace"]}
      defaultValue="workspace/members"
      className="w-full max-w-64"
    >
      <TreeItem value="account" label="Account">
        <TreeItem value="account/profile" label="Profile" />
        <TreeItem value="account/security" label="Security" />
      </TreeItem>
      <TreeItem value="workspace" label="Workspace">
        <TreeItem value="workspace/general" label="General" />
        <TreeItem value="workspace/members" label="Members" />
        <TreeItem value="workspace/billing" label="Billing" />
      </TreeItem>
      <TreeItem value="integrations" label="Integrations" />
    </Tree>
  );
}
```

### With counts

`end` places text at the end of an item, such as a count. Keep it to text and badges, since the
row is a single control and nothing inside it can take focus.

```tsx
import { ArchiveIcon, FilePenIcon, InboxIcon, SendIcon, StarIcon, TagIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";

export default function TreeWithCounts() {
  return (
    <Tree
      aria-label="Mailboxes"
      defaultExpanded={["inbox"]}
      defaultValue="inbox"
      className="w-full max-w-64"
    >
      <TreeItem value="inbox" label="Inbox" icon={<InboxIcon />} end="12">
        <TreeItem value="inbox/receipts" label="Receipts" icon={<TagIcon />} end="3" />
        <TreeItem value="inbox/travel" label="Travel" icon={<TagIcon />} />
      </TreeItem>
      <TreeItem value="starred" label="Starred" icon={<StarIcon />} />
      <TreeItem value="drafts" label="Drafts" icon={<FilePenIcon />} end="1" />
      <TreeItem value="sent" label="Sent" icon={<SendIcon />} />
      <TreeItem value="archive" label="Archive" icon={<ArchiveIcon />} />
    </Tree>
  );
}
```

### Long labels

A text label that doesn't fit ends in an ellipsis, and hovering it shows the whole label in a
tooltip. For file names, pass a [Truncate](https://ui.jarv.is/components/truncate.md) with `position="middle"` as the
`label` instead, so they're cut in the middle and their extensions stay in sight.

```tsx
import { FileIcon, FolderIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";
import { Truncate } from "@/components/ui/truncate";

export default function TreeLongLabels() {
  return (
    <Tree aria-label="Exports" defaultExpanded={["exports"]} className="w-full max-w-56">
      <TreeItem
        value="exports"
        label="Exports from the October board meeting"
        icon={<FolderIcon />}
      >
        <TreeItem
          value="exports/board-update"
          label={<Truncate position="middle">q3-board-update-final-v4-with-appendix.pdf</Truncate>}
          icon={<FileIcon />}
        />
        <TreeItem
          value="exports/interviews"
          label={<Truncate position="middle">customer-interview-transcripts-batch-2.docx</Truncate>}
          icon={<FileIcon />}
        />
        <TreeItem
          value="exports/hero"
          label={<Truncate position="middle">homepage-hero-illustration-dark@2x.png</Truncate>}
          icon={<FileIcon />}
        />
        <TreeItem value="exports/readme" label="README.md" icon={<FileIcon />} />
      </TreeItem>
    </Tree>
  );
}
```

### Disabled

A disabled item can still be reached with the arrow keys, but it can't be selected, and a disabled
folder can't be opened.

```tsx
import { FileTextIcon, FolderIcon, LockIcon } from "lucide-react";

import { Tree, TreeItem } from "@/components/ui/tree";

export default function TreeDisabled() {
  return (
    <Tree aria-label="Documents" defaultExpanded={["reports"]} className="w-full max-w-64">
      <TreeItem value="reports" label="Reports" icon={<FolderIcon />}>
        <TreeItem value="reports/q1" label="Q1 summary" icon={<FileTextIcon />} />
        <TreeItem value="reports/q2" label="Q2 summary" icon={<FileTextIcon />} disabled />
      </TreeItem>
      <TreeItem value="legal" label="Legal" icon={<LockIcon />} disabled>
        <TreeItem value="legal/contracts" label="Contracts" icon={<FileTextIcon />} />
      </TreeItem>
    </Tree>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `Tree` | `<ul>` |
| `TreeItem` | `<li>` |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### Tree

| Prop                | Type                             | Description                                                                             |
| ------------------- | -------------------------------- | --------------------------------------------------------------------------------------- |
| `value?`            | `string \| null`                 | The value of the selected item.                                                         |
| `defaultValue?`     | `string \| null`                 | The value of the item selected at first, when `value` isn't controlled. Default: `null` |
| `onValueChange?`    | `((value: string) => void)`      | Called with an item's value when it's selected by a click, Enter or Space.              |
| `expanded?`         | `string[]`                       | The values of the open items.                                                           |
| `defaultExpanded?`  | `string[]`                       | The values of the items open at first, when `expanded` isn't controlled. Default: `[]`  |
| `onExpandedChange?` | `((expanded: string[]) => void)` | Called with the values of the open items when one opens or closes.                      |
| `guides?`           | `boolean`                        | Draws a line down each open item's children. Default: `true`                            |

### TreeItem

| Prop        | Type              | Description                                                                                                                              |
| ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `value`     | `string`          | Identifies the item for selection and for opening it. Unique within the tree.                                                            |
| `label`     | `React.ReactNode` | The item's name, shown in its row and read out as its name. Text that doesn't fit ends in an ellipsis, with all of it in a tooltip.      |
| `icon?`     | `React.ReactNode` | An icon before the label.                                                                                                                |
| `end?`      | `React.ReactNode` | Content at the end of the row, such as a count. Keep it to text and badges: the row is one control, so nothing inside it can take focus. |
| `disabled?` | `boolean`         | Keeps the item from being selected, or opened if it's a folder. It can still be reached with the arrow keys. Default: `false`            |
