Project folder

A folder card whose contents fan out on hover and expand into a full-screen grid.

Right-click the folder, or a file once it fans out

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/project-folder.json
Usage
Hand it a title and an array of previews — the fan geometry, the overlay, the focus trap and the reduced-motion fallbacks are all internal.
TSImport
import { ProjectFolder } from "@/components/ui/project-folder";
TSExample
"use client";

import { ProjectFolder } from "@/components/ui/project-folder";

// Hand it file URLs: images render as cover thumbnails, PDFs as their first
// page (react-pdf, loaded on demand — the host must send CORS headers),
// everything else as a type tile (INDD, AI…). Each one cross-fades in from
// its type tile once it paints. Pass `content` instead for a custom tile.
// The demo reads real rows from `files.files` via /api/previous-submissions.
const previews = files.map((f) => ({ id: f.url, src: f.url, name: f.name }));

export function Example() {
  return (
    <ProjectFolder
      title="Merch Apparel"
      updatedAt={submission.submittedAt}
      previews={previews}
      onExpandedChange={(expanded) => {
        // Fires when the overlay opens and again when it finishes closing.
        console.log(expanded ? "opened" : "closed");
      }}
    />
  );
}
Composition
Anatomy of the folder card and the overlay it portals into.
ProjectFolder                    (single component — pass everything as props)
├── back wall                    (tilts away on hover/focus)
├── preview fan                  (up to 5 previews, splayed from centre)
│   └── previews[n]              (src → image thumb or type tile; content overrides)
├── front wall                   (title + count + updatedLabel/updatedAt)
├── context menu                 (optional — right-click, t-dropdown)
└── portal → dialog              (rendered into document.body on click)
    ├── backdrop                 (click to dismiss)
    ├── header                   (title, count, close button)
    └── grid                     (2 / 3 / 5 cols — each preview morphs in
                                  from its fanned position via layoutId)
Default
Hover or focus the card to fan the previews out of the folder; each file rises as the pointer crosses it and shows its name and type in a tooltip tilted to match it, and with `onPreviewClick` a click opens it directly. Click the card itself to morph the previews into the overlay, a masonry of every file in its own shape. Escape or a backdrop click retraces the arc back in.
Custom labels and count
`updatedLabel` swaps the word before the relative time ("Submitted 3d ago"), `itemLabel` renames the count noun, and `count` reports the real total when the folder holds more than the five previews it can fan.
Context menu
Pass `contextMenu` to open a menu on right-click, and `previewContextMenu` to give each file its own — right-click a file once the fan is out, or its card in the overlay. Both take the same items as `ContextMenu` and `DropdownMenu` — headings, icons, shortcuts, separators, disabled and destructive rows.

Right-click the folder, or a file once it fans out

File details
Pass `renderPreviewDetail` and a click in the overlay opens the file in place: it grows to the top, the rest fade down into a strip, and your panel reveals beside it. Click the open file or press Escape to go back to the grid.
Loading
`loading` swaps the labels for boneyard skeleton bones while the folder's data is in flight and holds the card shut — only the parts that come from the remote call; the card shell stays. Thumbnails, also remote, have their own load state: each one pulses as its type tile, then cross-fades in once the image or PDF page paints.
Empty and disabled
With no previews there is nothing to expand — the card stays flat, and `disabled` dims it and drops it out of the tab order.
API Reference
Props for the folder and for a single preview.

ProjectFolder

PropTypeDefaultDescription
titlestring—Folder name on the front wall and in the overlay header.
titleHrefstring—Makes the open folder's title a link to what the folder is of (its project), opened in a new tab so the folder stays open.
updatedAtDate | string | number—When the folder last changed. Rendered relative on the meta row — "Updated 48d ago". Omitted, it reads "Updated recently".
updatedLabelstring"Updated"The word in front of the relative time — "Edited", "Submitted", "Uploaded"…
descriptionstring—Replaces the whole meta line (label and time) with your own text.
loadingbooleanfalseSet while the folder's data is being fetched. The title, count and updated line give way to boneyard skeleton bones (captured from the real labels, shipped inline) while the card shell stays drawn; hover, expand and the context menu are blocked. When it flips false the bones fade out and the count rolls up to the real total.
contextMenuContextMenuItemSpec[]—Right-click menu. Same item shape as `ContextMenu` / `DropdownMenu`: `{ label, icon?, shortcut?, onClick?, variant?, disabled? }`, `"separator"`, or `{ heading }`. Opens with the SDK's t-dropdown transition.
onContextMenu() => void—Fires on right-click, before the menu opens.
previewContextMenuContextMenuItemSpec[] | (preview, index) => ContextMenuItemSpec[]—Right-click menu for a single file, on a fanned-out file and on its card in the overlay grid. An array applies to every file; a function varies it per file. It nests inside the folder's menu — a file opens only its own — and an empty array falls through to the folder's. Tucked-in files aren't targets until the fan is out, and the fan holds open while a file's menu is up.
onPreviewClick(preview: ProjectFolderPreview, index: number) => void—Makes each file clickable. Fires from a fanned-out file — the click doesn't also expand the folder — and from the overlay grid, where every file is a real button for keyboard and screen-reader users. Navigate, open a viewer, anything.
renderPreviewDetail(preview: ProjectFolderPreview, index: number) => ReactNode—Opens a file inside the overlay instead of handing it off. The clicked file grows to the top on the transitions-dev card-resize curve, the others fade down into a strip under it (click one to switch), and what this returns slides in beside it with the transitions-dev panel reveal. With it, overlay clicks select; the fan still fires `onPreviewClick`. Clicking the open file or Escape goes back to the grid.
renderPreviewOpen(preview: ProjectFolderPreview, index: number) => ReactNode—What the open file shows once it has grown into place, instead of its thumbnail — a viewer, such as a PDF's pages. The file grows as its thumbnail (the same tween every file makes), then this takes over the slot. Return null to keep the thumbnail. Needs `renderPreviewDetail`.
selectedPreviewIdstring | null—The file open in the overlay, by id — controlled. Looked up in every preview, not only the five the grid shows. Omit it and the folder keeps the selection itself.
onSelectedPreviewChange(preview: ProjectFolderPreview | null) => void—A file was opened, or `null` when the view goes back to the grid. Not called when the overlay closes; `onExpandedChange` reports that.
previewsProjectFolderPreview[][]The folder's files. The first 5 fan out of the closed folder; the open folder shows every one (a file with no `src` is its type tile). Pass `count` when the folder holds more than you pass.
countnumberpreviews.lengthNumber shown in the count label.
itemLabelstring"file"Singular noun for the count label — pluralised with "s".
openboolean—Controls the fanned-out (hover/focus) state. Uncontrolled by default.
defaultOpenbooleanfalseInitial fan state when uncontrolled.
onOpenChange(open: boolean) => void—Fires when the fan opens or collapses.
expandedboolean—Controls the full-screen overlay. Uncontrolled by default.
defaultExpandedbooleanfalseInitial overlay state when uncontrolled.
onExpandedChange(expanded: boolean) => void—Fires when the overlay opens or closes.
onClick() => void—Extra handler run alongside opening the overlay.
disabledbooleanfalseDims the card and blocks hover, focus, and expansion.
ariaLabelstring—Accessible name for the folder button.
classNamestring—Override the card's size or layout classes.

ProjectFolderPreview

PropTypeDefaultDescription
idstring—Stable key — also drives the shared `layoutId` that morphs the tile from fan to grid.
srcstring—File URL. Images (png, jpg, webp, gif, avif, svg) render as a cover thumbnail; `.pdf` renders page 1 via react-pdf, loaded on first use with range requests so large files only fetch what page 1 needs (the host must send CORS headers — otherwise, or on any error, it falls back to the tile); anything else renders as a tile showing the file type and name.
namestringlast segment of srcFile name — alt text, tile label, the hover tooltip over the fanned file, and where the type is read from.
contentReactNode—Custom tile for both the fan and the overlay card — wins over `src`. Use it for video posters or PDF first pages.
aspectnumber—width ÷ height. The shape the file is drawn in once the folder is open — a masonry of real shapes, the strip at one height, the open file as large as fits the row and 60% of the screen's height — so nothing is cropped. Leave it out and the folder reads it off the thumbnail once it loads (4:5 until then).