App

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.

Free, MIT licenseOpen in Browse
"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>
  )
}

Installation

pnpm dlx shadcn add @01orbital/file-tree
npx shadcn add @01orbital/file-tree
yarn shadcn add @01orbital/file-tree
bunx --bun shadcn add @01orbital/file-tree

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/blocks/app/file-tree/index.tsx, as source you own and edit.

Usage

import FileTree from "@/components/blocks/app/file-tree"

export default function FileTreeDemo() {
  return (
    <>
      <FileTree />
    </>
  )
}

Dependencies

npm: @phosphor-icons/react. The CLI installs them for you.

Registry pieces it installs with it:

  • button
  • card
  • collapsible