# File manager

> A React file manager for the documents of a company or a record: folders and files in a grid or a list, a folder tree, uploads and bulk actions.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add file-manager`
- Docs: https://wingo-ui.com/components/file-manager

## Interaction

Click a file or a folder to select it, Cmd or Ctrl + click to add or remove one, Shift + click for a range, tick the checkbox that appears on hover, or click empty space to clear the selection; with anything selected the toolbar turns into the bulk bar (Move, Download, Delete, Select all). Double-click (or Enter) opens: a folder slides in, an image opens in the lightbox with the folder's other images, any other file goes to onOpen or its url in a new tab. Right-click or the ⋯ button opens the menu (Open, Rename, Move to, Download, your own entries, Delete); right-click on empty space offers New folder, Upload and Select all. Rename with F2, the menu or a slow second click on the name: the base name is selected, Enter saves, Escape cancels, and a rejected promise shows its message under the field. Drag files (the whole selection comes along) onto a folder, a crumb or a folder in the tree to move them; a folder never drops into itself, and the drag card shows a stop sign over a refused target. Drop files from the desktop anywhere on it to upload into the open folder, or onto a folder tile to upload there; pasting files uploads too, and the uploads panel in the bottom corner shows progress and rejected files, folds away and closes once nothing runs. Search looks through every folder, lists the results with their path and marks the matched letters. With the keyboard the files are one Tab stop: arrows move (Up and Down by a row of tiles), Home, End, Page Up and Page Down jump, letters jump to a name, Space toggles, Shift + arrows extend, Cmd or Ctrl + A selects all, Escape clears, Delete asks and removes, Backspace or Alt + Up goes to the parent folder, Shift + F10 opens the menu, and Cmd or Ctrl + Shift + N makes a folder from anywhere inside. On a phone it starts as a list under the folder's name as a large title with a back link: a tap opens, a long press starts selecting (checkboxes appear, taps add or remove, a floating bar offers Move, Download and Delete), hold and drag a selected file onto a folder to move it, and every menu, the Move dialog and the new folder dialog are bottom sheets. The layout follows the manager's own width, so in a narrow side panel it shows the compact list and two-column tiles even on a wide screen.

## Installation

```bash
npx wingo-ui@latest add file-manager
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/file-manager` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/file-manager.tsx`

npm dependencies: `npm i @dnd-kit/core motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Breadcrumb](https://wingo-ui.com/components/breadcrumb) ([Markdown](https://wingo-ui.com/components/breadcrumb.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Context Menu](https://wingo-ui.com/components/context-menu) ([Markdown](https://wingo-ui.com/components/context-menu.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [File Upload](https://wingo-ui.com/components/file-upload) ([Markdown](https://wingo-ui.com/components/file-upload.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Toolbar](https://wingo-ui.com/components/toolbar) ([Markdown](https://wingo-ui.com/components/toolbar.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [Tree View](https://wingo-ui.com/components/tree-view) ([Markdown](https://wingo-ui.com/components/tree-view.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [useSelection](https://wingo-ui.com/components/use-selection) ([Markdown](https://wingo-ui.com/components/use-selection.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [File types](https://wingo-ui.com/components/file-types) ([Markdown](https://wingo-ui.com/components/file-types.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { FileManager, type FileManagerItem, type FileManagerUpload } from "@/components/ui/file-manager"

export function Documents({ initial }: { initial: FileManagerItem[] }) {
  const [items, setItems] = useState(initial)
  const [uploads, setUploads] = useState<FileManagerUpload[]>([])

  return (
    <FileManager
      items={items}
      uploads={uploads}
      quota={{ used: 2.4 * 1024 ** 3, total: 10 * 1024 ** 3 }}
      locale="en-US"
      persistKey="crm:documents"
      labels={{ root: "Company files", search: "Search company files" }}
      onUpload={async (files, folderId) => {
        setUploads(files.map((file) => ({ id: file.name, name: file.name, size: file.size, progress: 0, status: "uploading", folderId })))
        const body = new FormData()
        files.forEach((file) => body.append("files", file))
        const saved: FileManagerItem[] = await (await fetch(`/api/files?folder=${folderId ?? ""}`, { method: "POST", body })).json()
        setItems((list) => [...list, ...saved])
        setUploads((list) => list.map((upload) => ({ ...upload, progress: 1, status: "done" })))
      }}
      onCreateFolder={async (name, parentId) => {
        const folder: FileManagerItem = await (await fetch("/api/folders", { method: "POST", body: JSON.stringify({ name, parentId }) })).json()
        setItems((list) => [...list, folder])
      }}
      onRename={async (item, name) => {
        await fetch(`/api/files/${item.id}`, { method: "PATCH", body: JSON.stringify({ name }) })
        setItems((list) => list.map((i) => (i.id === item.id ? { ...i, name } : i)))
      }}
      onMove={async (ids, parentId) => {
        await fetch("/api/files/move", { method: "POST", body: JSON.stringify({ ids, parentId }) })
        setItems((list) => list.map((i) => (ids.includes(i.id) ? { ...i, parentId } : i)))
      }}
      onDelete={async (ids) => {
        await fetch("/api/files", { method: "DELETE", body: JSON.stringify({ ids }) })
        setItems((list) => list.filter((i) => !ids.includes(i.id)))
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `FileManagerItem[]` | `"[]"` | The whole tree, flat: { id, kind: "folder" \| "file", name, parentId (null at the root), size? (bytes), type? (mime), url?, thumbnailUrl?, modifiedAt?, modifiedBy?: { name, avatar? }, itemCount?, shared?, starred?, locked? }. The component filters by parentId; a parentId loop never hangs. Locked items cannot be renamed, moved or deleted; Move and Delete on a selection that holds some skip them. Tiles use content-visibility past 60 items; page from the server beyond about 2.000 files in one folder. |
| `rootLabel?` | `string` | `""` | The root's name in the breadcrumb, the folder tree and search paths. Empty uses labels.root ("All files"). |
| `quota?` | `{ used: number; total: number }` |  | Bytes used and available: a thin bar with "2,4 GB of 10 GB" next to the breadcrumb, amber past 80% and red past 95%. |
| `uploads?` | `FileManagerUpload[]` | `"[]"` | The uploads you are running: { id, name, size, progress (0 to 1), status: "uploading" \| "done" \| "error", error?, folderId }. They show in a floating panel at the bottom corner (collapsible, dismissible once nothing is running), next to the files the picker or a drop rejected. |
| `emptyState?` | `ReactNode` |  | Replaces the empty folder state (by default an icon, a line of text and New folder / Upload when those handlers exist). |
| `labels?` | `Partial<FileManagerLabels>` |  | Every string, English by default (DEFAULT_FILE_MANAGER_LABELS): root, region, folders, search, newFolder, folderName, create, cancel, upload, dropHere(folder), move, moveTo, moveHere, moveDescription(count, name), rename, download, delete, deleteTitle(count, name), deleteDescription, open, preview, select(name), selectAll, clearSelection, selected(count, formatted), items(count, formatted), results(count, formatted), name, modified, size, owner, type, sort, ascending, descending, view, grid, list, actions(name), sheetTitle, uploads, uploading(count, formatted), uploadsDone(count, formatted), uploadsFailed(count, formatted), uploadDone, uploadFailed, rejectedType, rejectedSize(max), collapse, expand, dismiss, storage, quota(used, total), empty, emptyDescription, noResults, noResultsDescription(query), clearSearch, loading, errorTitle, retry, moved(count, folder), renamed(name), deleted(count), created(name), failed, locked, shared, starred, folder, modifiedOn(when), dragging(count, name), pickedUp(count, name), dropped, dragCancelled, kinds (names per file kind). |
| `defaultView?` | `"grid" \| "list"` | `"grid"` | The first layout while uncontrolled: tiles with previews, or rows with Modified, Size and Owner columns. Phones start with the list until someone picks a view; search results are always a list. |
| `view?` | `"grid" \| "list"` |  | The layout, controlled. Pair it with onViewChange. |
| `onViewChange?` | `(view: "grid" \| "list") => void` |  | Called when the view switch in the toolbar changes. |
| `tileSize?` | `"sm" \| "md" \| "lg"` | `"md"` | The smallest tile width in the grid, 120 / 160 / 200px; the grid fits as many columns as it can. Below 512px of width (phones, side panels) it shows two columns. |
| `variant?` | `"surface" \| "plain"` | `"surface"` | surface sits the manager in a raised panel; plain drops the panel, for a section card that already has one. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Selected tiles and rows, drop targets, checkboxes, the tree's current folder, progress bars and the Upload button of the empty state. |
| `color?` | `string` | `""` | Any CSS color in place of the tone. |
| `showTree?` | `boolean` | `true` | The folder tree beside the files once the manager itself is 768px wide (a container query, so it also hides in a narrow panel on a wide screen). Its folders are drop targets too. |
| `showBreadcrumb?` | `boolean` | `true` | The path from the root to the open folder; every crumb is a drop target. Phones show a back link to the parent. |
| `loading?` | `boolean` | `false` | Skeleton tiles or rows with the real shape while the first page loads. |
| `skeletonCount?` | `number` | `12` | How many skeletons show while loading. |
| `error?` | `ReactNode` | `""` | Replaces the files with an error state and its message; add onRetry for a Try again button. |
| `onRetry?` | `() => (void \| Promise<unknown>)` |  | The error state's Try again button. |
| `defaultSearch?` | `string` | `""` | A search to start with. Search looks through every folder, ignoring case and diacritics, and lists the results with their folder path. |
| `search?` | `string` |  | The search, controlled. Pair it with onSearchChange. |
| `onSearchChange?` | `(search: string) => void` |  | Called with the debounced query, and with "" when a folder is opened from the results. |
| `folderId?` | `string \| null` |  | The open folder, controlled (null is the root). If it disappears from items, the nearest folder that still exists opens. |
| `defaultFolderId?` | `string \| null` | `"null"` | The folder that opens first while uncontrolled (null is the root). The playground offers folders of the demo. |
| `onFolderChange?` | `(folderId: string \| null) => void` |  | Called when a folder opens: a double-click, a crumb, the tree, Backspace. |
| `sort?` | `FileManagerSort` |  | The sort, controlled: { by: "name" \| "modified" \| "size" \| "type", direction: "asc" \| "desc" }. Folders always come first. |
| `defaultSort?` | `FileManagerSort` | `"DEFAULT_FILE_MANAGER_SORT"` | The first sort while uncontrolled: { by: "name", direction: "asc" }. Picking Modified or Size in the menu starts with the newest or largest. |
| `onSortChange?` | `(sort: FileManagerSort) => void` |  | Called from the sort menu and from the column headers of the list. |
| `selectedKeys?` | `string[]` |  | The selected ids, controlled. Pair it with onSelectedKeysChange. |
| `defaultSelectedKeys?` | `string[]` | `"[]"` | The ids selected at first while uncontrolled; with any selected the toolbar is the bulk bar. |
| `onSelectedKeysChange?` | `(keys: string[]) => void` |  | Called when the selection changes. Opening another folder clears it. |
| `persistKey?` | `string` | `""` | Remembers the view and the sort in localStorage under this key ("<key>:view", "<key>:sort"). Empty remembers nothing. |
| `confirmDelete?` | `boolean` | `true` | Ask before deleting, in an alert dialog that names the file or the count. Off, Delete runs at once. |
| `previewImages?` | `boolean` | `true` | Images open in the built-in lightbox with the folder's other images. Off, they go to onOpen like any other file. |
| `accept?` | `string` | `""` | What the upload picker offers, like the input's accept ("image/*,.pdf"). Dropped or pasted files outside it are listed as rejected in the uploads panel instead of reaching onUpload. |
| `maxFileSize?` | `number` | `0` | The largest file in bytes; bigger ones are listed as rejected with the limit. 0 means no limit. |
| `uploadShortcut?` | `string` | `""` | A key combo that opens the upload picker from anywhere on the page ("mod+u" notation). Empty binds nothing. |
| `locale?` | `string` | `""` | Formats sizes ("2,4 MB"), dates, counts and the sort order. Empty uses the UIProvider locale, then the browser's. |
| `now?` | `Date \| number` |  | The reference time for "modified 5 min ago". Defaults to a live clock that ticks every minute; demos pin 24.09.2026 16:40. |
| `onOpen?` | `(item: FileManagerItem) => void` |  | Opens a file. Folders always open in place and images in the lightbox (while previewImages is on). Without it a file with a url opens in a new tab. |
| `onUpload?` | `(files: File[], folderId: string \| null) => (void \| Promise<unknown>)` |  | Turns on the Upload button, dropping files from the desktop and pasting them. folderId is the open folder, or the folder tile the files were dropped on. Report progress back through uploads. |
| `onCreateFolder?` | `(name: string, parentId: string \| null) => (void \| Promise<unknown>)` |  | Turns on New folder (a dialog, a sheet on phones). A rejected promise keeps the dialog open with its message, so you decide about duplicate names. |
| `onRename?` | `(item: FileManagerItem, name: string) => (void \| Promise<unknown>)` |  | Turns on renaming in place. The field stays busy while the promise runs; a rejection shows its message under it and keeps it open. |
| `onMove?` | `(ids: string[], targetFolderId: string \| null) => (void \| Promise<unknown>)` |  | Turns on dragging onto folders, crumbs and tree nodes, and Move to (a dialog with the folder tree, a sheet on phones). Moves into the current folder, into itself or into a subfolder are refused before it runs. |
| `onDelete?` | `(ids: string[]) => (void \| Promise<unknown>)` |  | Turns on Delete in the menus, the bulk bar and the Delete key. The confirmation stays loading while the promise runs. |
| `onDownload?` | `(items: FileManagerItem[]) => void` |  | Download for files and selections. Without it Download appears when every file has a url and saves them through links. |
| `getActions?` | `(item: FileManagerItem) => MenuEntry[]` |  | Extra menu entries per item (Share, Copy link, Star), between Download and Delete, in the W1 MenuEntry shape. |
| `classNames?` | `Partial<Record<"toolbar" \| "breadcrumb" \| "title" \| "quota" \| "body" \| "tree" \| "content" \| "header" \| "tile" \| "row" \| "select" \| "icon" \| "name" \| "meta" \| "actions" \| "drop" \| "uploads" \| "empty" \| "move" \| "newFolder", string>>` |  | Extra classes for the parts. title is the phone's folder title, move and newFolder go on the dialogs' bodies, uploads on the floating panel. |
| `className?` | `string` |  | Extra classes for the root, merged last. Other props (id, aria-*, data-*, handlers) spread onto it. |

## Parts

### buildFolderTree (function)

The folders as nested tree-view nodes with folder icons, sorted by name; a parentId loop is broken (a folder is never listed twice). Pure, so a server can call it. Signature: (items: FileManagerItem[], options?: { locale?: string }) => TreeNode[].

### getFolderPath (function)

The trail from the root to a folder as breadcrumb entries (the root's id is "file-manager-root"). Pure and cycle-safe. Signature: (items: FileManagerItem[], folderId: string | null, options?: { rootLabel?: string }) => BreadcrumbEntry[].

### DEFAULT_FILE_MANAGER_LABELS (constant)

The English strings, to spread under a translation. Type: FileManagerLabels.

### DEFAULT_FILE_MANAGER_SORT (constant)

{ by: "name", direction: "asc" }, the default sort. Type: FileManagerSort.

## Examples

- **Search as a list**: Invoices from every folder, largest first, each with its path.
- **A record's files**: One client's documents: re-rooted, no tree, small tiles in brand orange.
- **Phone selection**: Long press to select, then the floating bar for bulk actions.
- **Move dialog**: Pick a destination in the folder tree; drops into the same folder are refused.
- **Loading, empty, uploading**: Skeletons, an empty folder with its actions, and the uploads panel.
- **Read-only viewer**: No handlers: browse, search, preview and open only.

## See also

- [File List](https://wingo-ui.com/components/file-list) ([Markdown](https://wingo-ui.com/components/file-list.md))
- [File Upload](https://wingo-ui.com/components/file-upload) ([Markdown](https://wingo-ui.com/components/file-upload.md))
- [Tree View](https://wingo-ui.com/components/tree-view) ([Markdown](https://wingo-ui.com/components/tree-view.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Breadcrumb](https://wingo-ui.com/components/breadcrumb) ([Markdown](https://wingo-ui.com/components/breadcrumb.md))

---

Canonical: https://wingo-ui.com/components/file-manager
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
