File tree

Displays a hierarchy of files and folders with curved branch connectors, an accent line that draws to the selection, and keyboard navigation.

Brand
logo-primary.svg
logo-stacked.ai
brand-guidelines.pdf
Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/file-tree.json
Usage
Import the master file and pass a nested `items` array. Selection and expansion are uncontrolled until you pass `value` or `expandedIds`.
TSImport
import { FileTree } from "@/components/ui/file-tree";
TSExample
import { FileTree } from "@/components/ui/file-tree";

// A round of work as it lands in Dropbox — brand kit, layered artwork,
// motion project files, and the flattened deliverables.
const items = [
  {
    name: "Brand",
    children: [
      { name: "logo-primary.svg" },
      { name: "logo-stacked.ai" },
      { name: "brand-guidelines.pdf" },
      {
        name: "Fonts",
        children: [
          { name: "Albra-Regular.otf" },
          { name: "Inter-Variable.ttf" },
        ],
      },
    ],
  },
  {
    name: "Design",
    children: [
      { name: "easter-key-art.psd" },
      { name: "title-slide.png" },
      { name: "series-mockup.jpg" },
    ],
  },
  {
    name: "Motion",
    children: [{ name: "bumper.aep" }, { name: "bumper-final.mov" }],
  },
  { name: "walk-in-loop.wav" },
  { name: "easter-2026-delivery.zip" },
];

export function Example() {
  return (
    <FileTree
      items={items}
      defaultExpandedIds={["Design"]}
      onSelect={(node, path) => openAsset(path.join("/"))}
    />
  );
}
Composition
Anatomy of the FileTree primitive.
FileTree                         (single component — pass everything as props)
├── items={FileTreeNode[]}       (nested; `children` marks a folder)
│   ├── name                     ("key-art.psd" — drives the auto icon)
│   ├── id?                      (defaults to the slash path, "Design/key-art.psd")
│   ├── children?                (presence ⇒ folder · type:"folder" for empties)
│   ├── icon? / iconClassName?   (override the extension icon)
│   ├── badge?                   (trailing slot — round count, size, status)
│   ├── hasChildren?             (an S3 prefix — opens, children fetched then)
│   ├── data?                    (your own payload, handed back on onSelect)
│   └── disabled?
├── value / defaultValue         (selected node id — the gliding highlight)
├── expandedIds / defaultExpandedIds / defaultExpandAll
├── contextMenu={[…] | (node, path) => […]}   (right-click menu)
├── loadChildren={(node, path) => Promise<FileTreeNode[]>}  (lazy folders)
├── filter="query"               (hides non-matches, forces hit branches open)
├── size="sm | default" · indent={16} · showLines · colorIcons
└── maxHeight={240}              (turns the tree into its own scroll-fade scroller)
Default
Pass a nested `items` array. Icons come from the file extension — Adobe formats get the application's own mark, everything else a tinted Phosphor glyph — folders open on click, and the selection glides between rows.
Brand
logo-primary.svg
logo-stacked.ai
brand-guidelines.pdf
Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip
Filter
`filter` hides non-matching assets, keeps folders whose descendants match, and forces those branches open for as long as the query lasts — type `psd` to pull every layered file out of a deep round.
Design
easter-key-art.psd
Badges, custom icons, disabled
Each node takes a trailing `badge` node, an `icon` override, and `disabled` — enough for approval state on a round of concepts, with superseded versions locked. Empty folders need `type: "folder"`.
Round 23
key-art-v3.psdApproved
key-art-v2.psdRevisions
key-art-v1.psd
brand-guidelines.pdf
Controlled selection
Pass `value` + `onValueChange` to own the selection. `onSelect` also hands you the node and its ancestor path.
Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip

Selected: Design/easter-key-art.psd

Right-click menu
`contextMenu` takes the same item specs as the SDK's ContextMenu, except every `onSelect` is handed the node that was right-clicked and its ancestor path. Pass an array for one menu, or a function to vary it per node.
Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip

Right-click any row.

S3 — flat keys
`fileTreeFromKeys` turns a `ListObjectsV2` page into a tree: folders from the `/` segments, folders sorted before files, and every node's id set to its full object key.
design
easter-key-art.psd176 MB
title-slide.png2.3 MB
delivery.zip1.1 GB

Built from 9 flat keys. Each node’s id is its full object key.

S3 — one listing per prefix
For a bucket too big to walk, `loadChildren` fetches a folder the first time it opens — a spinner row while it's in flight, the error inline if it rejects, and the result cached for the life of the tree.
Listing bucket…

One request per prefix. Open restricted/ to see a failed listing.

Scrolling
`maxHeight` makes the tree its own scroller with the SDK's `scroll-fade` mask. Keep the border on a wrapper so the fade dissolves content, not the card edge.
Design
Sermon series
easter-key-art.psd
easter-key-art-v2.psd
title-slide.png
lower-third.png
stage-screen.jpg
Social
ig-story-01.jpg
ig-story-02.jpg
ig-carousel.psd
fb-cover.png
Print
invite-card.indd
invite-card.pdf
Motion
bumper.aep
recap-edit.prproj
bumper-final.mov
lower-thirds.mogrt
countdown-5min.mp4
Photography
IMG_4417.dng
IMG_4418.dng
team-portrait.tiff
run-of-show.pptx
Sizes, guides, and color
`size="sm"` tightens rows for dense sidebars, `showLines={false}` drops the branch connectors and the rail, and `colorIcons={false}` mutes the per-type tints.

sm

Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip

no guides

Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip

monochrome

Design
easter-key-art.psd
title-slide.png
series-mockup.jpg
walk-in-loop.wav
easter-2026-delivery.zip
File type colors
Icons are chosen and colored by extension so a folder of artwork reads at a glance. Adobe formats render the application's own mark; every other tint pairs a light `-600` with a dark `-400`. Turn the whole thing off with `colorIcons={false}`.

The Adobe marks are inlined from the public-domain SVGs on Wikimedia Commons (PD-textlogo) so the CLI install stays a single file. They remain Adobe trademarks and identify the file format only — pass colorIcons={false} to render neutral glyphs instead.

Adobe formats carry the application's own mark:

psd · psb                  Photoshop
ai · eps                   Illustrator
indd · idml                InDesign
aep · mogrt                After Effects
prproj                     Premiere Pro
pdf                        Acrobat

Everything else is a tinted Phosphor glyph:

afphoto                    sky        (layered artwork)
afdesign                   orange     (vector)
fig · sketch · xd · ase    fuchsia    (design tools, palettes)
drp · blend · c4d          violet     (other motion projects)
mp4 · mov · m4v · webm     rose       (rendered video)
mp3 · wav · aif            cyan       (audio)
png · jpg · tif · dng · …  teal       (raster + raw photography)
svg                        amber      (vector export)
ppt · pptx · key           orange     (decks)
doc · docx                 blue
xls · xlsx · csv           emerald
otf · ttf · woff           neutral    (type)
zip · tar · gz · rar       neutral    (archives)
md · txt                   neutral
folders                    neutral    (so file types carry the color)
Keyboard
The tree is a roving-tabindex `role="tree"` — one tab stop, arrows move within it.
↑ / ↓        move to the previous / next visible row
→            open a closed folder, then step into it
←            close an open folder, else jump to its parent
Home / End   first / last visible row
Enter, Space select the row (and toggle it when it's a folder)
API Reference
Props on FileTree and on each node.

FileTree

PropTypeDefaultDescription
itemsFileTreeNode[]—The tree. A node is a folder when it has `children` (or `type: "folder"`).
valuestring | null—Controlled selection (node id). Leave undefined to let the tree own it.
defaultValuestring | nullnullUncontrolled initial selection.
onValueChange(id: string) => void—Fires with the newly selected node id.
onSelect(node: FileTreeNode, path: string[]) => void—Fires with the node itself plus its ancestor names, root first — `["Design", "easter-key-art.psd"]`.
expandedIdsstring[]—Controlled expansion. Pair with `onExpandedChange`.
defaultExpandedIdsstring[][]Folders open on first render.
onExpandedChange(ids: string[]) => void—Fires with the full expanded set after every toggle.
onExpandChange(id: string, expanded: boolean) => void—Fires with just the folder that toggled.
defaultExpandAllbooleanfalseStart with every folder open. Ignored when `defaultExpandedIds` is set.
contextMenuFileTreeMenuItem[] | ((node, path) => FileTreeMenuItem[])—Right-click menu. An array gives every row the same menu; a function varies it per node — return `[]` to suppress it. Right-clicking moves the keyboard cursor without firing `onSelect`.
onContextMenu(node: FileTreeNode, path: string[]) => void—Fires when a row is right-clicked, before the menu opens.
onMove(node: FileTreeNode, target: FileTreeNode | null, path: string[]) => void—Drag a row onto another row to move it inside that one, or onto the tree's empty space to move it to the top level (`target` null). Native drag and drop; omit it and nothing drags. The tree only reports the move — rewrite `items` yourself.
canDrop(node: FileTreeNode, target: FileTreeNode | null) => boolean—Whether `node` may land on `target` (null = the top level). Dropping onto itself, into its own subtree, or a top-level row onto the top level is already refused.
rowClassNamestring—Class added to every row. Each row also carries its position in the visible order as `--stagger-index`, which is what lets a caller reveal a listing one row at a time without knowing how long it is.
rowIndexCapnumber12Rows past this keep the last one's delay, so a long listing doesn't leave its tail waiting seconds for a turn.
columnsFileTreeColumn[]—Columns to the right of every row, under a header naming them — the tree as a table. `{ id, header, width?, align?, cell(node, path) }`; the cell is a function of the node, so nothing has to be copied onto a row to appear in a column. Header type and size match data grid v3's.
nameHeaderReactNode"Name"Header above the name column.
showHeaderboolean—Draw the header row. Defaults to on whenever there are columns.
onNodeHover(node: FileTreeNode, path: string[]) => void—Fires when the pointer enters a row or the keyboard cursor lands on it — for warming what a click will need (a preview image, a metadata read) before the click happens.
loadChildren(node, path) => Promise<FileTreeNode[]>—Fetches a folder's children the first time it opens — one `ListObjectsV2` per prefix instead of walking a whole bucket. Called for folders with no `children` array and for any node marked `hasChildren`. A spinner row shows while in flight, a rejection renders inline, and results are cached for the life of the tree (remount with `key` to drop it).
loadingMessageReactNode"Loading…"Row text while a lazy folder is loading.
filterstring""Substring match over node names. Non-matching files drop out, folders survive when a descendant matches, and matching branches force open.
colorIconsbooleantrueColor icons by type. Adobe formats (psd, ai, indd, aep, mogrt, prproj, pdf) render the application's own mark; everything else is a tinted Phosphor glyph — video rose, audio cyan, raster teal, decks orange. `false` renders every icon as a muted Phosphor glyph.
showLinesbooleantrueCurved branch connectors down each open folder, plus the rail and its selection marker down the left edge. `false` leaves a plain indent.
indentnumber16Indent per level, in px.
size"sm" | "default""default"Row height and type scale (h-7/text-xs vs h-8/text-sm).
maxHeightnumber | string—Turns the tree into its own scroller and applies the `scroll-fade` mask.
emptyMessageReactNode"No files"Shown when `items` is empty or the filter matches nothing.
ariaLabelstring"Files"Accessible name on the `role="tree"` container.
classNamestring—Extends the outer wrapper.

FileTreeNode

PropTypeDefaultDescription
namestring—Displayed label. Its extension picks the Phosphor duotone icon — psd, ai, indd, fig, aep, mogrt, mov, wav, otf, dng and the usual document types are all mapped.
idstringslash pathStable id used for selection and expansion — defaults to `"Design/easter-key-art.psd"`.
childrenFileTreeNode[]—Presence marks the node as a folder.
type"file" | "folder"—Force the kind — use `"folder"` for an empty directory.
iconComponentType<{ className?: string }>—Override the extension-derived icon (and its tint). Bind Phosphor `weight` in a wrapper.
iconClassNamestring—Tint or resize just this node's icon.
badgeReactNode—Trailing slot — a round count, an approval Badge, a status dot.
hasChildrenboolean—Marks a folder whose children aren't loaded yet — it opens like any other folder and calls `loadChildren` on first expand. `fileTreeFromListing` sets it on every S3 common prefix.
dataunknown—Your own payload — an S3 object's size, ETag, storage class, version id. FileTree never reads it; `onSelect` hands it back.
disabledboolean—Dims the row and blocks selection.

FileTreeMenuItem

PropTypeDefaultDescription
"separator"string—A divider — pass the bare string in the array.
headingReactNode—A group label row: `{ heading: "Danger zone" }`.
labelReactNode—The item's text.
iconReactNode—Leading icon — typically a Phosphor icon element.
shortcutReactNode—Right-aligned hint, e.g. "⌘C". Display only — bind the key yourself.
variant"default" | "destructive""default"`destructive` tints the item red.
disabledboolean—Dims and blocks the item.
onSelect(node: FileTreeNode, path: string[]) => void—Fires with the right-clicked node and its ancestor names, root first.

Exported helpers

PropTypeDefaultDescription
fileTreeFromKeys(objects, options?)FileTreeNode[]—Flat object keys → nested tree. `options`: `root` (prefix to strip), `delimiter` (default `"/"`), `formatMeta` (renders into each file's `badge`), `sort` (folders first, then A–Z — default `true`). Node ids are full keys; a key ending in the delimiter is treated as a folder marker.
fileTreeFromListing(listing, options?)FileTreeNode[]—One delimited `ListObjectsV2` response → one level. `CommonPrefixes` become folders marked `hasChildren`; `Contents` become files. Pair with `loadChildren`.
fileTreeFormatSize(bytes)string—`2411724` → `"2.3 MB"`. Handy inside `formatMeta`.
fileTreeIconFor(name)ComponentType<{ className?: string }>—The Phosphor glyph FileTree would pick for a filename.
fileTreeBrandIconFor(name)ComponentType | undefined—The full-color Adobe mark for a filename, when one exists.
fileTreeColorFor(name)string—The tint FileTree would pick, as Tailwind classes.