File Tree
Expandable and collapsible tree view for file structures.
Installation
CLI
npx shadcn@latest add "https://ui.xaclabs.dev/r/file-tree.json"Usage
import { FileTree } from "@/registry/components/file-tree"View component source
file-tree.tsx
tsx"use client"
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface FileTreeItem {
name: string
type: "file" | "folder"
children?: FileTreeItem[]
icon?: React.ComponentType<{ className?: string }>
highlight?: boolean
}
export interface FileTreeProps {
items: FileTreeItem[]
onFileClick?: (name: string, path: string) => void
className?: string
defaultExpanded?: boolean
}
interface FileTreeNodeProps {
item: FileTreeItem
depth: number
path: string
onFileClick?: (name: string, path: string) => void
defaultExpanded: boolean
}
function FileTreeNode({
item,
depth,
path,
onFileClick,
defaultExpanded,
}: FileTreeNodeProps) {
const [expanded, setExpanded] = React.useState(defaultExpanded)
const fullPath = path ? `${path}/${item.name}` : item.name
const isFolder = item.type === "folder"
const Icon = item.icon ?? (isFolder ? (expanded ? FolderOpen : Folder) : File)
const handleClick = () => {
if (isFolder) {
setExpanded((prev) => !prev)
} else {
onFileClick?.(item.name, fullPath)
}
}
return (
<li>
<button
type="button"
onClick={handleClick}
className={cn(
"flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
item.highlight && "bg-accent/50 font-medium",
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
>
{isFolder && (
<motion.span
animate={{ rotate: expanded ? 90 : 0 }}
transition={{ duration: 0.15 }}
className="flex shrink-0"
>
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
</motion.span>
)}
{!isFolder && <span className="w-3.5 shrink-0" />}
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{item.name}</span>
</button>
<AnimatePresence initial={false}>
{isFolder && expanded && item.children && (
<motion.ul
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
{item.children.map((child) => (
<FileTreeNode
key={child.name}
item={child}
depth={depth + 1}
path={fullPath}
onFileClick={onFileClick}
defaultExpanded={defaultExpanded}
/>
))}
</motion.ul>
)}
</AnimatePresence>
</li>
)
}
export function FileTree({
items,
onFileClick,
className,
defaultExpanded = false,
}: FileTreeProps) {
return (
<div
role="tree"
className={cn(
"rounded-lg border bg-card p-2 text-card-foreground",
className,
)}
>
{items.map((item) => (
<FileTreeNode
key={item.name}
item={item}
depth={0}
path=""
onFileClick={onFileClick}
defaultExpanded={defaultExpanded}
/>
))}
</div>
)
}