277

File System (Finder)

A macOS Finder-style file browser for flat object-store manifests with icon, list, column, and gallery views.

File System renders a flat manifest of files and folders — the shape you get back from S3, R2, or any key-prefix object store — as a macOS Finder-style browser. It derives the folder hierarchy from object keys, switches between icon, list, column, and gallery views, and treats thumbnails, URLs, and signing as external concerns.

Documents
6 items

Installation

pnpm dlx shadcn@latest add @extend/file-system

Data Model

The component accepts a normalized flat manifest instead of a nested tree. Files are addressable objects, folders are prefixes, URLs are optional, and signing happens outside the component.

const items: FileSystemItem[] = [
  {
    kind: "folder",
    path: "invoices/2026/",
    hasChildren: true,
  },
  {
    kind: "file",
    key: "invoices/2026/jan.pdf",
    path: "invoices/2026/jan.pdf",
    contentType: "application/pdf",
    size: 482193,
    createdAt: "2026-01-04T10:02:00.000Z",
    updatedAt: "2026-06-09T18:21:00.000Z",
    etag: '"abc123"',
    previewImageUrl: "/thumbnails/invoices/2026/jan.png",
  },
]

Explicit folder entries are optional — missing prefixes are inferred from file paths. Keeping kind: "folder" support matters for paginated or lazy traversal, where S3 CommonPrefixes describe folders whose contents have not been listed yet.

S3 / R2 Mapping

A ListObjectsV2 response maps directly onto the manifest:

S3 / R2 fieldManifest field
Contents[].Keyfile.key and file.path
Contents[].Sizefile.size
Contents[].LastModifiedfile.updatedAt
Contents[].ETagfile.etag
CommonPrefixes[].Prefixfolder.path

Thumbnails

Like File Thumbnail, the component never parses documents and has no renderer dependencies. Generate preview images with whichever stack you already use — react-pdf, @extend-ai/react-docx, @extend-ai/react-xlsx, or a server-side pipeline — then pass the result through previewImageUrl. Files without a preview fall back to a generic document tile, or to the node returned by renderFilePreview.

One caveat for DOCX: @extend-ai/react-docx versions before 0.7.0 rasterize pages in a way that taints the canvas, blocking toDataURL export in the browser. On 0.7.0+ the demo generates DOCX thumbnails client-side like the PDFs; on older versions, render DOCX previews live via renderFilePreview or generate thumbnail images server-side.

Lazy Loading

For large buckets, pass hasChildren: true on folder entries and provide loadChildren. The component fetches a folder's contents the first time it is opened, following nextCursor until the listing is exhausted.

<FileSystem
  items={rootItems}
  loadChildren={async ({ path, cursor }) =>
    fetch(`/api/files/list?prefix=${path}&cursor=${cursor ?? ""}`).then((r) =>
      r.json()
    )
  }
  getFileUrl={async (file) =>
    `/api/files/sign?key=${encodeURIComponent(file.key ?? file.path)}`
  }
/>

Views

  • Icons — a thumbnail grid with macOS-style folder glyphs.
  • List — a hierarchical tree rendered with @pierre/trees, with modified date and size as a trailing column.
  • Columns — Miller columns with a preview-and-metadata pane for the selected file.
  • Gallery — a large preview with an information sidebar and a filmstrip of the current folder.

Double-clicking a folder navigates into it. Double-clicking a file (in any view, including the list tree) opens the built-in viewer dialog — PDF, DOCX, and XLSX files open in their respective viewers, and images open in an image dialog; other file types open their resolved URL in a new tab. Pass onFileOpen to replace this behavior entirely. The registry item depends on the PDF Viewer, DOCX Viewer, and Excel Viewer, so installing @extend/file-system brings the full preview experience.

The list view tree is derived from file paths, so lazy folders (hasChildren without loaded entries) appear there after their contents have been fetched — open them from the icon or column views first, or list them eagerly.

Keyboard Navigation

  • Grid — arrow keys move the selection in all four directions, following the rendered grid; Enter opens.
  • List — full keyboard support from @pierre/trees (arrows, type-ahead, expand/collapse).
  • Columns — up/down move within a column, left selects the parent folder, right steps into the selected folder; Enter opens.
  • Gallery — left/right move through the filmstrip; Enter opens.

Finder-style shortcuts work everywhere in the component: ⌘↑ goes to the enclosing folder, ⌘↓ opens the selection, and ⌘[ / ⌘] navigate back and forward through history (Ctrl works in place of ⌘).

When the component is narrower than 480px, the toolbar swaps the tabs view switcher for a compact select.

The gallery view embeds the full viewers — PDF, DOCX, and XLSX files render in their toolbar-less viewer (the XLSX sheet switcher stays), and images scale to fill the stage.

Multi-Page Thumbnails

Pass previewImageUrls (an array of page thumbnails — page counts come from whichever renderer generates them, e.g. react-pdf, @extend-ai/react-docx, or @extend-ai/react-xlsx). Large thumbnails in the column preview show hover pager buttons to step through pages. previewAspectRatio on a file controls its thumbnail shape — e.g. natural ratios for images, landscape for spreadsheets.

For long documents, provide only the first page(s) eagerly plus previewPageCount, and implement loadPreviewImageUrl(file, pageIndex) — the pager renders the remaining pages on demand (the demo renders PDF pages lazily through a cached pdfjs document).

API Reference

FileSystem

PropTypeDefaultRequired
itemsFileSystemItem[]-Yes
classNamestring-No
titlestring"Files"No
defaultView"icons" | "list" | "columns" | "gallery""icons"No
viewFileSystemView-No
onViewChange(view: FileSystemView) => void-No
defaultPathstring""No
onSelectionChange(item: FileSystemItem | null) => void-No
onFileOpen(file: FileSystemFileItem, url: string | null) => void-No
getFileUrl(file: FileSystemFileItem) => string | Promise<string>-No
loadChildren(args: FileSystemLoadChildrenArgs) => Promise<...>-No
loadPreviewImageUrl(file: FileSystemFileItem, pageIndex: number) => Promise<string | null>-No
renderFilePreview(file: FileSystemFileItem) => React.ReactNode-No

FileSystemFolderItem

PropTypeDefaultRequired
kind"folder"-Yes
pathstring-Yes
namestring-No
parentPathstring-No
hasChildrenboolean-No
createdAtstring-No
updatedAtstring-No

FileSystemFileItem

PropTypeDefaultRequired
kind"file"-Yes
pathstring-Yes
keystringpathNo
namestring-No
parentPathstring-No
contentTypestring-No
sizenumber-No
createdAtstring-No
updatedAtstring-No
etagstring-No
urlstring-No
previewImageUrlstring | null-No
previewImageUrlsstring[] | null-No
previewPageCountnumber-No
previewAspectRationumber-No
metadataRecord<string, string>-No