# File Tree

> An Explorer card showing a nested project file tree with collapsible folders, rotating carets and the selected file's path in the header. Folder and file rows are ghost buttons with Phosphor icons.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/file-tree).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/file-tree
- Screenshot: https://shadcn.exchange/shot/01orbital/file-tree/light.png
- Registry item: https://01orbital.dev/r/file-tree.json
- Categories: app
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/file-tree.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@phosphor-icons/react`
- Registry items: `@01orbital/button`, `card`, `@01orbital/collapsible`

## Files

### `blocks/app/file-tree/index.tsx`

```tsx
"use client"

import * as React from "react"
import { CaretRightIcon, FileIcon, FolderIcon } from "@phosphor-icons/react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Explorer",
} as const

type FileTreeItem = { name: string } | { name: string; items: FileTreeItem[] }

// Sample data.
const FILE_TREE: FileTreeItem[] = [
  {
    name: "components",
    items: [
      {
        name: "ui",
        items: [
          { name: "button.tsx" },
          { name: "card.tsx" },
          { name: "dialog.tsx" },
          { name: "input.tsx" },
          { name: "select.tsx" },
          { name: "table.tsx" },
        ],
      },
      { name: "login-form.tsx" },
      { name: "register-form.tsx" },
    ],
  },
  {
    name: "lib",
    items: [{ name: "utils.ts" }, { name: "cn.ts" }, { name: "api.ts" }],
  },
  {
    name: "hooks",
    items: [
      { name: "use-media-query.ts" },
      { name: "use-debounce.ts" },
      { name: "use-local-storage.ts" },
    ],
  },
  {
    name: "types",
    items: [{ name: "index.d.ts" }, { name: "api.d.ts" }],
  },
  {
    name: "public",
    items: [{ name: "favicon.ico" }, { name: "logo.svg" }],
  },
  { name: "app.tsx" },
  { name: "layout.tsx" },
  { name: "globals.css" },
  { name: "package.json" },
  { name: "tsconfig.json" },
  { name: "README.md" },
  { name: ".gitignore" },
]

export default function FileTree() {
  const [selected, setSelected] = React.useState("components/ui/button.tsx")

  const renderItem = (item: FileTreeItem, parent: string) => {
    const path = parent ? `${parent}/${item.name}` : item.name
    if ("items" in item) {
      return (
        <Collapsible
          key={path}
          defaultOpen={item.name === "components" || item.name === "ui"}
        >
          <CollapsibleTrigger
            render={
              <Button
                variant="ghost"
                size="sm"
                className="group w-full justify-start"
              />
            }
          >
            <CaretRightIcon
              className="transition-transform group-data-panel-open:rotate-90"
              data-icon="inline-start"
            />
            <FolderIcon data-icon="inline-start" />
            {item.name}
          </CollapsibleTrigger>
          <CollapsibleContent className="mt-1 ml-5">
            <section className="flex flex-col gap-1">
              {item.items.map((child) => renderItem(child, path))}
            </section>
          </CollapsibleContent>
        </Collapsible>
      )
    }
    return (
      <Button
        key={path}
        variant={selected === path ? "secondary" : "ghost"}
        size="sm"
        aria-current={selected === path ? "true" : undefined}
        onClick={() => setSelected(path)}
        className="w-full justify-start"
      >
        <FileIcon data-icon="inline-start" />
        {item.name}
      </Button>
    )
  }

  return (
    <Card className="mx-auto w-full max-w-64" size="sm">
      <CardHeader>
        <CardTitle>{COPY.title}</CardTitle>
        <CardDescription>{selected}</CardDescription>
      </CardHeader>
      <CardContent>
        <section className="flex flex-col gap-1">
          {FILE_TREE.map((item) => renderItem(item, ""))}
        </section>
      </CardContent>
    </Card>
  )
}
```
