# Browse files

Compose file explorer with storage permissions, loading and failure states.

Import: @slices.sh/file-storage/headless

Public parts: 72

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/file-storage@^0.2.7
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/file-storage.md

## Anatomy

### Composition

```text
Files.Container bucket="files" ifExists="ask"
  Files.Root provided internally by Container
    Files.Offline
    Files.Uploader
      Files.CanUpload
        Files.DropArea
          Files.Receiving
    Files.NoUploads
    Files.Announcer
    Files.Preview
      Files.PreviewImage
      Files.PreviewFrame
      Files.PreviewPending
      Files.NoPreview
      Files.ClosePreview
    Files.MutationFailed
    Files.Unavailable
    Files.MissingConfiguration
    Files.Children
      Files.Tree
        Files.ChildEntries
          Files.TreeToggle
      Files.WhenChildren of="/Brand assets" state="ready"
      Files.WhenChildren of="/Brand assets" state="unknown"
      Files.WhenChildren of="/Brand assets" state="loading"
      Files.WhenChildren of="/Brand assets" state="failed"
      Files.WhenChildren of="/Brand assets" state="empty"
    Files.Views
      Files.ViewArea
        Files.WhenView value="icons"
        Files.WhenView value="list"
          Files.Listed
            Files.Entries
              Files.Entry
                Files.EntryLocation
                Files.Pick
                Files.EntryKind
                Files.EntryDate
                Files.EntryItems
                Files.File
                Files.Pending
                  Files.PendingRing
                  Files.PendingCancel
                Files.PendingFailed
                  Files.PendingRetry
                  Files.PendingDismiss
                Files.Open
                Files.Folder
                  Files.FolderName
                Files.Thumbnail
                Files.EntryName
                Files.EntrySize
                Files.EntryModified
                Files.Download
        Files.WhenView value="columns"
          Files.Columns
            Files.Column
              Files.CurrentColumn
              Files.ColumnName
            Files.Cascade
              Files.CascadeEnd
        Files.WhenView value="gallery"
          Files.Gallery
            Files.GalleryActive
            Files.GallerySettled
            Files.GalleryStep
            Files.GalleryPending
      Files.ViewOption value="list"
    Files.Navigation
      Files.Up
      Files.Back
      Files.Forward
    Files.Window
    Files.Breadcrumb
      Files.Crumb
    Files.LoadMore
    Files.Loading
    Files.Empty
    Files.Failed
```

## Minimal composition

```tsx
"use client"

import "@/services"
import { Files } from "@slices.sh/file-storage/headless"

export function Example() {
  return (
    <Files.Root>
      <Files.Breadcrumb>
        <Files.Crumb />
      </Files.Breadcrumb>
      <Files.Up>Parent folder</Files.Up>
      <Files.Entries>
        <Files.Entry>
          <Files.Open>
            <Files.Thumbnail />
            <Files.EntryName />
          </Files.Open>
        </Files.Entry>
      </Files.Entries>
      <Files.Empty>No files here.</Files.Empty>
      <Files.Loading>Loading…</Files.Loading>
    </Files.Root>
  )
}

```

Configure a file-storage provider. Mutating actions follow the storage rules and current user permissions.

## Tree, columns & gallery

```tsx
"use client"

import "@/services"
import { Files } from "@slices.sh/file-storage/headless"

export function Example() {
  return (
    <Files.Root>
      <Files.Tree>
        <Files.Entries>
          <Files.Entry>
            <Files.TreeToggle />
            <Files.EntryName />
            <Files.Children>
              <Files.ChildEntries>
                <Files.EntryName />
              </Files.ChildEntries>
            </Files.Children>
          </Files.Entry>
        </Files.Entries>
      </Files.Tree>
    </Files.Root>
  )
}

```

Configure a file-storage provider. Mutating actions follow the storage rules and current user permissions.

## Complete API reference

### Files.Root

scope

The browser's root, and Files.Root with Files.Children in one, the seam that reads what the config's client can do.

Import: @slices.sh/file-storage/headless

```tsx
FilesRootProps

export type FilesRootProps = RootProps & {
  /** The file-storage config, a slug from the registry (`services/file-storage/<config>.config.ts`). The nearest root's, else `"default"`, when left out. */
  config?: string
  /**
   * Left out or `true`: required, and a missing service throws. `"optional"`: renders its
   * children and every part inside renders nothing except `Files.Unavailable`, and a Composer
   * inside finds its sink switched off. `false`: off. A capability name such as `"trash"`
   * requires the service with it.
   */
  enabled?: Enabled
  bucket?: string
  /** Where the browser opens. Give `path` and `onPath` instead to keep it in the address. */
  initialPath?: string
  path?: string
  onPath?(path: string): void
  multiple?: boolean
  /** Opening another folder ends a search, as in Finder, instead of carrying it into the folder. */
  searchLeavesWithFolder?: boolean
  /**
   * What moving an entry into a folder that has one of that name does. Left out, it is
   * refused. `"ask"` raises a question for `Files.Clash` to put to the person: replace, keep
   * both or skip.
   */
  ifExists?: IfExists | "ask"
}
```

Source: @slices.sh/file-storage/src/headless/files-root.tsx

### Files.Navigation

content · Element: ul

Opt-in roving primary controls; secondary buttons retain their normal Tab behavior.

Import: @slices.sh/file-storage/headless

Files.Navigation requires Files.Root.

```tsx
PartProps<"ul"> & { layout?: "linear" | "grid"; windowed?: boolean }
```

Source: @slices.sh/file-storage/src/headless/navigation.tsx

### Files.Back

action · Element: button

The folder this panel was looking at before.

Disabled rather than hidden, unlike `Files.Up`: back and forward sit in a row of controls
whose places people learn, and one that comes and goes moves the other one under the
pointer. Up has no such pair — at the root there is genuinely nothing there.

Import: @slices.sh/file-storage/headless

Files.Back must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/back.tsx

### Files.Forward

action · Element: button

The folder this panel came back from, until opening another one replaces it.

Import: @slices.sh/file-storage/headless

Files.Forward must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/forward.tsx

### Files.Up

action · Element: button

One folder up. Not rendered at the root, where there is no up.

Import: @slices.sh/file-storage/headless

Files.Up must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/up.tsx

### Files.Breadcrumb

scope

Every folder from the root down to where the browser is, each in its own scope.

Import: @slices.sh/file-storage/headless

Files.Breadcrumb must be inside <Files.Root>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/breadcrumb.tsx

### Files.Crumb

action · Element: button

One step of the trail. The last one is where you already are, so it is disabled.

Import: @slices.sh/file-storage/headless

Files.Crumb must be inside <Files.Breadcrumb>.

Files.Crumb must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/crumb.tsx

### Files.Window

content · Element: ul

Window linear rows while preserving native Tab order and the last focused entry.

Import: @slices.sh/file-storage/headless

Files.Window requires Files.Root.

```tsx
PartProps<"ul"> & { windowed?: boolean; maxHeight?: CSSProperties["maxHeight"] }
```

Source: @slices.sh/file-storage/src/headless/window.tsx

### Files.ViewArea

content · Element: div

Which view is drawn (icons, list, columns, gallery), the area it measures, a pressed button per view, and a gate per view.

Import: @slices.sh/file-storage/headless

Files.ViewArea requires Files.Views.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/view-area.tsx

### Files.Container

scope

The one place a file explorer talks to the file service.

It maps the client onto what the screen below draws: the folder that is open and its pages
(`Files.Root`), the folders read besides it, from the store's listings or from
`loadChildren`, and the address of every file and every picture, which the parts ask the
client for and the server signs. What the screen may do is read here too, from what the
client has: a part that needs `upload` asks `Files.CanUpload`, so a service with none gets a
browser with no upload button and no drop, and not a button that fails when pressed.

The screen never calls the service itself: pattern code calls no hook, and every part under
here reaches the service through this container's contexts. → docs/rules/patterns-read-no-state.md

Import: @slices.sh/file-storage/headless

```tsx
ContainerProps

type ContainerProps = RootProps &
  Omit<FilesRootProps, "children"> & {
    /** Where a folder's children come from when it is not the store's own listing. */
    loadChildren?: LoadChildren
  }
```

Source: @slices.sh/file-storage/src/headless/container.tsx

### Files.Views

scope

Which view is drawn (icons, list, columns, gallery), the area it measures, a pressed button per view, and a gate per view.

Import: @slices.sh/file-storage/headless

Files.Views requires Files.Root.

```tsx
RootProps & { view?: FileView; defaultView?: FileView; onViewChange?(view: FileView): void }
```

Source: @slices.sh/file-storage/src/headless/views.tsx

### Files.WhenView

gate

Which view is drawn (icons, list, columns, gallery), the area it measures, a pressed button per view, and a gate per view.

Import: @slices.sh/file-storage/headless

Files.WhenView requires Files.Views.

```tsx
GateProps & { value: FileView | readonly FileView[] }
```

Source: @slices.sh/file-storage/src/headless/when-view.tsx

### Files.Entries

scope

What the browser is showing: the directory's children, or what the search found.

Under a folder that is open, in a tree, its entries follow it with how far in they are.

One part for both, because a screen that drew them separately would be two lists whose
rows, selection and actions drifted apart — and a search that showed a different kind of
row than the folder it searched is the thing people report as a bug.

Where the screen draws uploads in place, each one that is on its way is a row among these, in
the order of the rest, with `Files.Pending` to say how far it has got. A row that is not
there yet cannot be opened, picked, renamed or moved: its parts say so themselves.

Import: @slices.sh/file-storage/headless

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/entries.tsx

### Files.Listed

gate

The folder is being listed, or has been: where its headings and its rows go.

Everything a listing is made of belongs inside this — the column headings, the skeletons that
wait for the rows, the rows, the empty state that stands in for them, the page after this
one — so that they share one place in the layout. A folder with nothing in it then keeps its
headings over a body that says so, rather than a sentence above headings with nothing under
them.

Nothing when the read failed, where `Files.Failed` says why and headings over no rows would
say nothing, nor where no backend is installed, where `Files.Unavailable` is the answer.

Import: @slices.sh/file-storage/headless

Files.Listed must be inside <Files.Root>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/listed.tsx

### Files.Entry

content · Element: li

The gate around everything a listed folder draws, the repeater of rows on screen, the row, the next page, roving keys over the rows, a windowed list for long folders and the live announcement of what the folder now holds.

Import: @slices.sh/file-storage/headless

Files.Entry must be inside <Files.Entries>.

Files.Entry must be inside <Files.Root>.

```tsx
PartProps<"li", EntryState>

type EntryState = { kind: "file" | "directory"; selected: boolean }
```

Source: @slices.sh/file-storage/src/headless/entry.tsx

### Files.File

gate

This row is a file: what a folder has no use for, a picture of its contents or a size.
The other half of `Files.Folder`.

Import: @slices.sh/file-storage/headless

Files.File must be inside <Files.Entries>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/file-only.tsx

### Files.EntryName

content · Element: span

Opening a folder or selecting a file, the file manager's pick (a click picks, a double click opens), gates for a folder row and a file row, the thumbnail of a picture, the facts a row says, the folder an entry sits in for a search result, and a signed download link. Files.Folder is also a sink: a Composer inside it posts to that folder.

Import: @slices.sh/file-storage/headless

File.EntryName must be inside <File.Entries>.

```tsx
PartProps<"span">
```

Source: @slices.sh/file-storage/src/headless/entry-name.tsx

### Files.EntryKind

content · Element: span

What an entry is, in the words the screen has for it.

The part says the facts (the kind, the extension and the declared type) and its function
child turns them into "PDF Document" or "Folder", because those are words and the words are
the screen's. Without one it says the kind, which is the one thing that is not a guess.

Import: @slices.sh/file-storage/headless

Files.EntryKind must be inside <Files.Entries>.

```tsx
PartProps<"span", EntryKindState>

type EntryKindState = {
  kind: FileKind
  /** Lowercase, without the dot; empty for a name with none. */
  extension: string
  /** What the file declares itself to be; `null` for a folder or for a file that says nothing. */
  mime: string | null
  folder: boolean
}
```

Source: @slices.sh/file-storage/src/headless/entry-kind.tsx

### Files.EntryModified

content · Element: time

When it last changed, as a `<time>` carrying the machine-readable value.

The text is the date in the reader's own locale, computed on the client — which is why
`dateTime` matters: it is the one form a screen reader and a test can both rely on while
the visible text depends on where you are.

Import: @slices.sh/file-storage/headless

File.EntryModified must be inside <File.Entries>.

```tsx
PartProps<"time">
```

Source: @slices.sh/file-storage/src/headless/entry-modified.tsx

### Files.Folder

gate

This row is a folder.

What a row that is not a file needs in place of what only a file has: `Files.Download`
renders nothing for a folder, so a chip whose name is its download link shows the folder's
name through this instead.

Import: @slices.sh/file-storage/headless

Files.Folder must be inside <Files.Entries>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/folder.tsx

### Files.FolderName

content · Element: span

The name of the folder that is open, as a toolbar says it. At the root that is the bucket's
own name, which is not a word anybody chose, so the function child is told it is the root
and says what the screen calls it.

Import: @slices.sh/file-storage/headless

Files.FolderName must be inside <Files.Root>.

```tsx
PartProps<"span", { name: string; root: boolean }>
```

Source: @slices.sh/file-storage/src/headless/folder-name.tsx

### Files.EntryItems

content · Element: span

How many entries a folder holds, once somebody has looked: `null` until then, which is not
zero. A row that said "0 items" for a folder nobody had opened would be calling it empty
before it had looked. → docs/rules/capability-honesty.md

Only for a folder, and only where `Files.Children` is, which is what reads one. The count is
what was read, so while the pages of a long folder are still arriving it is what has.

Import: @slices.sh/file-storage/headless

Files.EntryItems must be inside <Files.Entries>.

```tsx
PartProps<"span", { count: number | null }>
```

Source: @slices.sh/file-storage/src/headless/entry-items.tsx

### Files.ViewOption

action · Element: button

Which view is drawn (icons, list, columns, gallery), the area it measures, a pressed button per view, and a gate per view.

Import: @slices.sh/file-storage/headless

Files.ViewOption requires Files.Views.

```tsx
PartProps<"button"> & { value: FileView }
```

Source: @slices.sh/file-storage/src/headless/view-option.tsx

### Files.Open

action · Element: button

The control that opens a row: a folder to browse into, a file to select.

One part for both, because a folder and a file are opened the same way by everybody who
has used a file browser, and only the row knows which it is.

Import: @slices.sh/file-storage/headless

Files.Open must be inside <Files.Entries>.

Files.Open must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/open.tsx

### Files.Pick

action · Element: button

The control of a row as a file manager has it: a click picks it, and opening is a double
click or Enter.

Apart from `Files.Open`, where one click opens a folder: here a folder is picked like a
file, which is what lets its neighbours stay on screen while it is looked at, and it is
browsed into on the second press. Opening a file is whatever the screen has wired to open
one (`FilePreview.Root` opens the viewer); with nothing wired, the file stays picked.

Shift extends from the last row touched and Ctrl or Command adds one, over the rows on
screen; without either the row is the whole selection. A tree's rows and a column's go
through the same keys, since both are drawn from `Files.Entries`.

An upload that has not arrived is a row that is not there to pick: the control is disabled,
and is no stop of the keys, until the file is.

`data-state` says what else the row is to the screen it is in: `trail`, the folder whose
contents are the next column of a Miller view, and `stage`, the entry on the stage of a
gallery, which is the picked one or else the first.

Import: @slices.sh/file-storage/headless

Files.Pick must be inside <Files.Entries>.

Files.Pick must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/pick.tsx

### Files.Download

content · Element: a

Saves the file it is in, by a URL the server signed for saving rather than for showing.

An anchor and not a button, so the browser's own "save link as" and middle-click work.
Until the server has said where the bytes are there is no `href`, which makes this not a
link yet rather than a link to nowhere — `data-pending` is how a skin says so instead of
offering something that would do nothing.

`download` names the file: an object store's key is a hash often enough that saving one
without this produces a folder full of them.

The file's name is in `state` as well, because a list of these reads as "Download, link"
once per row otherwise, with nothing saying which row. What to do with it — a name, or a
sentence around one — is the pattern's. → docs/rules/words-in-copy-looks-in-skin.md

Import: @slices.sh/file-storage/headless

Files.Download must be inside <Files.Entries> or <Files.Preview>.

```tsx
PartProps<"a", { name: string }>
```

Source: @slices.sh/file-storage/src/headless/download-entry.tsx

### Files.Thumbnail

content · Element: img

The picture a row is of, at the size the skin draws it.

Nothing at all for anything that is not an image, and nothing until the server has said
where the bytes are: a grid of photographs drawn as identical grey glyphs is unusable, and
a grid of broken-image icons is worse. The icon beside it is the skin's business — this
part's answer to "no picture" is to render none. → docs/rules/capability-honesty.md

An upload on its way is the picture on this device, which is there from the moment the file
was chosen; the store has nothing to ask for yet.

Which is also the answer when the bytes do not arrive. A signed URL is good for minutes,
and a folder left open for longer would otherwise turn every picture in it into a broken
one; the glyph underneath is still the truth about what the file is. Remembered against
the URL that failed, so the next one is tried.

Import: @slices.sh/file-storage/headless

Files.Thumbnail must be inside <Files.Entries>.

```tsx
PartProps<"img">
```

Source: @slices.sh/file-storage/src/headless/thumbnail.tsx

### Files.EntrySize

content · Element: span

A file's size. A directory has none of its own, so it renders nothing rather than "0 B",
unless it is a folder on its way, which is as large as the files going into it.

Import: @slices.sh/file-storage/headless

File.EntrySize must be inside <File.Entries>.

```tsx
PartProps<"span">
```

Source: @slices.sh/file-storage/src/headless/entry-size.tsx

### Files.EntryLocation

content · Element: span

The folder an entry sits in, from the one the browser has open: `Invoices/2026` for a file
found by a search two levels down. Nothing for an entry that is in the open folder itself,
because a column that repeated the folder in every row would say nothing.

What a recursive search needs beside the name: a list of `report.pdf` from five folders is
five files nobody can tell apart.

Import: @slices.sh/file-storage/headless

Files.EntryLocation must be inside <Files.Entries>.

Files.EntryLocation must be inside <Files.Root>.

```tsx
PartProps<"span">
```

Source: @slices.sh/file-storage/src/headless/entry-location.tsx

### Files.EntryDate

content · Element: time

One of an entry's two dates as a `<time>`, with the date itself for a function child to say
in the screen's words: `Jan 8, 2026 at 12:24 PM` is a choice of format and of "at".

Nothing when the store did not say, rather than the epoch: a date that is a guess is worse
than none. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Files.EntryDate must be inside <Files.Entries>.

```tsx
PartProps<"time", { date: Date }> & { of: "createdAt" | "updatedAt" }
```

Source: @slices.sh/file-storage/src/headless/entry-date.tsx

### Files.Preview

scope

The one file that is selected, in its own scope.

A scope rather than a gate, so everything already written for a row works inside it:
`Files.EntryName` is the preview's title and `Files.EntrySize` is its subtitle, with no
second set of parts that could disagree with the first about what a name is.

Exactly one, and a file: two selected files are not a preview, and a folder has nothing to
show. The record comes from the listing on screen rather than being fetched again, so a
selection left behind by a folder that has since changed simply stops matching and this
closes itself.

Escape puts it away. This is not a dialog — nothing is trapped in it, the list behind it
stays live, and no focus is moved — but on a phone it is drawn over that list, and the key
everybody presses to dismiss something drawn over something else is this one. `useEscape`
owns the listener: the thing being dismissed is rarely what has the focus.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.Preview must be inside <Files.Root>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/preview.tsx

### Files.PreviewImage

content · Element: img

The picture itself, once the server has said where it is.

Nothing until then, and nothing at all for a file that is not an image: an `<img>` with no
`src` is a broken-image icon, which tells somebody the file is damaged when it is only the
URL that has not arrived. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Capability: previews

Files.PreviewImage must be inside <Files.Preview>.

```tsx
PartProps<"img">
```

Source: @slices.sh/file-storage/src/headless/preview-image.tsx

### Files.ClosePreview

action · Element: button

Puts the preview away, which is the same thing as selecting nothing.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.ClosePreview must be inside <Files.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/file-storage/src/headless/close-preview.tsx

### Files.PreviewFrame

content · Element: iframe

A PDF, a text file, a video: the things a browser opens for itself.

`sandbox` with nothing in it, because the bytes in a bucket are whatever somebody
uploaded. An SVG or an HTML file is a document with script in it, and a frame without this
would be running that script on this origin, against this session.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.PreviewFrame must be inside <Files.Preview>.

```tsx
PartProps<"iframe">
```

Source: @slices.sh/file-storage/src/headless/preview-frame.tsx

### Files.PreviewPending

gate

This file can be drawn, and where its bytes are has not been said yet.

On a store that signs its URLs that is a round trip, and until it answers the panel is a
title, a size and a hole where the file goes — which reads as a preview that failed. This
is where the shape of what is coming belongs. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Capability: previews

Files.PreviewPending must be inside <Files.Preview>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/preview-pending.tsx

### Files.NoPreview

gate

This file is not something a browser can draw.

A spreadsheet, a deck, an archive: real files that simply have no viewer here. Saying so
and offering `Files.Download` beside it is the honest screen; a blank frame is the one
people report as broken. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Capability: previews

Files.NoPreview must be inside <Files.Preview>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/no-preview.tsx

### Files.Uploader

scope

An upload queue that belongs to the folder around it.

`Upload.Root` on its own has to be told where to put things. This one asks: files land in
the directory that is on screen, and when one arrives the listing reads again — so a file
you dropped is in the list a moment later rather than after a reload nobody thought to do.

Both of those are wiring between two hooks, which is why they are here and not in a
pattern: pattern code calls no hook, and a screen that passed `path` and `onUploaded`
itself would have to hold the browser's state to do it.

`inPlace` is the screen that has no list of uploads of its own: each one is a row of the
folder it is going to, as `Files.Entries` draws them, and a file the person cannot yet open
is a row they cannot open.

Inside a `Files.Container` the service has the last word: where it has no upload, the queue
is not mounted at all, and `Files.CanUpload` is what keeps every part that needs the queue
from drawing. The server's log says so once, since a missing button is otherwise a mystery.

Import: @slices.sh/file-storage/headless

Capability: upload

Files.Uploader must be inside <Files.Root>.

```tsx
UploaderProps

type UploaderProps = RootProps & {
  bucket?: string
  /** Bytes. A larger file is refused before anything is sent. */
  maxSize?: number
  maxFiles?:number
  /** An `accept` attribute: `"image/*,.pdf"`. Offered to the picker and checked in the queue. */
  accept?: string
  /** Resumable parts for large files where the backend has them. On unless turned off. */
  multipart?: boolean
  includeHidden?: boolean
  /**
   * Draw each upload among the rows of the folder it is going to (`Files.Entries`), where it
   * will land, until the file is there. Off, the screen draws the queue itself.
   */
  inPlace?: boolean
  /** What a name that is taken does. `"ask"` holds the file for `Files.Clash` to put to the person. Left out, it replaces. */
  ifExists?: IfExists | "ask"
  onSend?(message: { text: string; files: FileRecord[] }): void
}
```

Source: @slices.sh/file-storage/src/headless/uploader.tsx

### Files.CanUpload

gate

The service takes files: where an upload button, a drop target and the rows of what is on
its way go.

Nothing at all where it does not, and nothing outside a `Files.Container`, which is the one
thing that can say. A control for something the backend cannot do is a promise it breaks.
→ docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/can-upload.tsx

### Files.DropArea

content · Element: div

A place files are dropped where the service takes them, and an ordinary element where it
does not.

`Upload.DropZone` is the first half and is an error outside an upload queue, which is right
for a message box that is nothing without one. A folder is something without it: the same
box, drawn the same, that takes no drag.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
PartProps<"div", DropAreaState>

type DropAreaState = { over: boolean; refusing: boolean }
```

Source: @slices.sh/file-storage/src/headless/drop-area.tsx

### Files.Receiving

content · Element: div

How far the files going into this folder have got, on the row of the folder: what files
dropped from the desktop on a folder that is not open say, since nothing else on screen is
theirs until it is.

Nothing for a file, for a folder nothing is going into, and for a row that is itself an
upload on its way, which has `Files.PendingRing`. The folder's name is its accessible name,
and `files` and `percent` are in the state for the sentence a pattern says about them.
Without a byte counted yet it is an unknown, which a skin draws as a stationary arc.

Import: @slices.sh/file-storage/headless

Capability: upload

Files.Receiving must be inside <Files.Entries>.

```tsx
PartProps<"div", { files: number; percent: number | null }>
```

Source: @slices.sh/file-storage/src/headless/receiving.tsx

### Files.Pending

gate

This row is an upload that has not arrived: where what is said about its journey goes.

Nothing for a stored entry, which is what makes it the one place a row says "42%" or "3 of 12
files" without a pattern asking whether it is looking at an upload. The words are the
pattern's; this says how far it has got. → docs/rules/states-gate-themselves.md

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
GateProps<PendingSaid>

type PendingSaid = {
  name: string
  /** A folder that was dropped, which adds up the files going into it. */
  folder: boolean
  state: UploadState
  /** How many files are behind the row, how many have landed and how many were refused. */
  files: number
  done: number
  failed: number
  /** Of the bytes of everything behind the row, as a whole number; `null` until one has been counted. */
  percent: number | null
}
```

Source: @slices.sh/file-storage/src/headless/pending.tsx

### Files.PendingRing

content · Element: div

How far an upload on its way has got, as the progress of the row it is drawn in.

Nothing once it has stopped, whether it arrived or was refused: a full ring that stays full
reads as still working. Without a byte counted yet it is an unknown, which a skin draws as a
stationary arc rather than a bar at nought. For a folder that was dropped it is every file
behind it, by bytes. The name of what it is for is its accessible name, since a folder
with five of these on it is five "40 percent"s otherwise.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/pending-ring.tsx

### Files.PendingCancel

action · Element: button

Stops an upload on its way, every file of it for a folder that was dropped. The row goes
with it: a file nobody is sending is not a row of the folder.

Not drawn once nothing of it can be stopped, as `Upload.Cancel`: a file that has been sent
and is being put together on the other side can no longer be called back. The name is in
`state`, so that a folder of twenty rows is not twenty buttons called "Stop".

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
PartProps<"button", { name: string }>
```

Source: @slices.sh/file-storage/src/headless/pending-cancel.tsx

### Files.PendingFailed

gate

An upload on its way was refused, with the reason: the row stays where the file would have
been, and says why it is not there.

For a folder that was dropped it is the first file that was refused, with how many were.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
GateProps<{ error: FileErrorSlug; name: string; failed: number }>
```

Source: @slices.sh/file-storage/src/headless/pending-failed.tsx

### Files.PendingRetry

action · Element: button

Sends again what was refused: the file, or the files of a folder that were. Drawn once there
is nothing left on its way, so a folder that is still sending is not sent twice.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
PartProps<"button", { name: string }>
```

Source: @slices.sh/file-storage/src/headless/pending-retry.tsx

### Files.PendingDismiss

action · Element: button

Lets go of a row that was refused, for somebody who does not mean to try again. What was not
sent is not in the folder, so the row has nothing left to be.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
PartProps<"button", { name: string }>
```

Source: @slices.sh/file-storage/src/headless/pending-dismiss.tsx

### Files.NoUploads

gate

Nothing is on its way: where a folder's "this is empty" goes, so it is not said while a file
is landing in it.

Where the uploads are drawn in place, what counts is what is drawn in this folder: a file
the listing has caught up with, that was stopped or that is going into another folder is not
on its way here, and a folder somebody emptied after filling it is empty again. One that was
refused still is a row of the folder it was meant for, with a reason.

`Upload.Empty` is this for a queue that must be there. Where the service takes no uploads
there is no queue, and a folder with nothing in it is empty, so this holds too.

Import: @slices.sh/file-storage/headless

Capability: upload

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/no-uploads.tsx

### Files.LoadMore

action · Element: button

The rest of a folder.

Rendered only where the store said there is another page, so a folder that fits shows
nothing and a folder of ten thousand never pretends to have ended. `data-pending` while
the next page is on its way; a second press does nothing.

`loading` is in `state` too, so the button can say it is fetching rather than going quiet
and disabled — which reads as a button that did not work.

Import: @slices.sh/file-storage/headless

Files.LoadMore must be inside <Files.Root>.

```tsx
PartProps<"button", { loading: boolean }>
```

Source: @slices.sh/file-storage/src/headless/load-more.tsx

### Files.Loading

gate

The first read. A reload keeps the rows on screen rather than blanking them.

Nothing at all where no backend is installed: `useDirectory` never asks, so its read stays
`loading` for good, and three skeletons pulsing under "no storage backend is installed"
is a screen promising rows that are never coming. `Files.Unavailable` is the whole answer
there. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Files.Loading must be inside <Files.Root>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/loading.tsx

### Files.Empty

gate

Nothing to show. `searched` tells the two apart, because the remedies differ: "drop
something here" against "try another word", and `query` is the word that was tried.

Import: @slices.sh/file-storage/headless

Files.Empty must be inside <Files.Root>.

```tsx
GateProps<{ searched: boolean; query: string }>
```

Source: @slices.sh/file-storage/src/headless/empty.tsx

### Files.Failed

gate

The read was refused, and the child is told which refusal it was.

Import: @slices.sh/file-storage/headless

Files.Failed must be inside <Files.Root>.

```tsx
GateProps<{ error: FileErrorSlug }>
```

Source: @slices.sh/file-storage/src/headless/failed.tsx

### Files.MutationFailed

gate

The last change to this folder was refused, with the reason.

Making a folder, moving an entry into one and deleting one can all be refused, and until
this there was nowhere for any of those answers to go: the row simply stayed where it was,
which reads as a screen that ignored the press. → docs/rules/states.md

Import: @slices.sh/file-storage/headless

Files.MutationFailed must be inside <Files.Root>.

```tsx
GateProps<{ error: FileErrorSlug }>
```

Source: @slices.sh/file-storage/src/headless/mutation-failed.tsx

### Files.Offline

gate

The first read, nothing to show (with the search that found nothing), the read refused, a change refused, the service missing under enabled="optional", a production server that answers config-missing, and the browser offline.

Import: @slices.sh/file-storage/headless

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/offline.tsx

### Files.Unavailable

gate

No storage backend is installed at all.

Distinct from a refused read, because nothing the person does will help: the app is
missing an implementation, and the only useful sentence names the command that adds one.

Import: @slices.sh/file-storage/headless

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/unavailable.tsx

### Files.MissingConfiguration

gate

The server this screen keeps its files on answered that it lacks configuration.

The family's own name for the shared gate, with the service already chosen: a screen that
draws files should not have to know which string the file routes answer with, and another
service's trouble (a transcription key) is not its notice. Not `Unavailable`, which is no
backend installed at all: that has a command to run, this has a log to read.
→ docs/rules/missing-configuration-is-said.md

Import: @slices.sh/file-storage/headless

```tsx
GateProps<{ scopes: readonly string[] }>
```

Source: @slices.sh/file-storage/src/headless/missing-configuration.tsx

### Files.Announcer

content · Element: div

What the listing is showing, for somebody who cannot see it change.

Opening a folder, searching, and asking for another page all replace the rows under a
heading that never moves: there is no navigation, no focus change and nothing said, so a
person using a screen reader presses a button and is told nothing at all.

Counts rather than events, and `aria-atomic`, so a folder of two hundred is one sentence.
The words are the pattern's — "empty", "nothing matched" and "twelve items" are three
different things to say. → docs/rules/words-in-copy-looks-in-skin.md

A live region announces what appears *inside* it, so the element is here from the first
render either way — and empty while no backend is installed, because `useDirectory` never
asks and its read stays `loading` for good. A region saying "Loading…" for the rest of the
session would be the one untrue thing on that screen. → docs/rules/capability-honesty.md

Import: @slices.sh/file-storage/headless

Files.Announcer must be inside <Files.Root>.

```tsx
PartProps<"div", FolderSaid>

type FolderSaid = { loading: boolean; failed: boolean; count: number; searched: boolean; folder: string }
```

Source: @slices.sh/file-storage/src/headless/folder-announcer.tsx

### Files.Tree

scope

Folders that open in place: the list as a tree, where a folder's row has its entries under
it and an arrow to show them.

Which are open is held here, above the views, so switching to icons and back finds the
branches as they were left. The rows are a tree only in the list view: the strip of a
gallery and a column are lists of their own folder, and read the same entries unchanged.

An open folder is asked for from `Files.Children`, which is why it has to be inside one:
the read starts when the folder opens, not when its row is listed.

Import: @slices.sh/file-storage/headless

Files.Tree must be inside <Files.Children>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/tree.tsx

### Files.Children

scope

The folders this screen reads besides the open one, each when something on screen asks to
see it: the branches of a tree, the columns of a Miller view, the folder behind a tile.

`loadChildren` is where they come from when it is not the store: called with a path and the
cursor of the page it wants, answering `{ entries, cursor }` as a listing does, and read
to its last page. The store's own listing is the default, so a screen on a bucket passes
nothing.

They are read again whenever the open folder is. A rename, a delete or a file landing in it
all read it again, and a branch left as it was would go on drawing the file that is gone.
What is on screen stays until the new reading arrives.

Import: @slices.sh/file-storage/headless

```tsx
RootProps & { loadChildren?: LoadChildren }
```

Source: @slices.sh/file-storage/src/headless/children.tsx

### Files.ChildEntries

scope

The entries of a folder, each in its row's scope: asking for the folder when this is drawn,
and narrowed and ordered as the open folder is.

Mounting it is the request, which is why a branch that is closed draws none of it and reads
nothing: the folder is read when somebody opens it, not when its row is listed.

Import: @slices.sh/file-storage/headless

Files.ChildEntries must be inside <Files.Children>.

```tsx
RootProps & { of?: string }
```

Source: @slices.sh/file-storage/src/headless/child-entries.tsx

### Files.WhenChildren

gate

Where a folder's reading is: not asked (`unknown`), asked and nothing back yet (`loading`),
refused (`failed`), read and holding nothing (`empty`), or holding something (`ready`).

`unknown` is its own answer and never `empty`: a folder nobody has read is not a folder with
nothing in it, and a tree that drew the first as the second would tell people a branch was
bare before it had looked. → docs/rules/capability-honesty.md

`loading` is only until the first page: after it the rows that came are drawn, and the pages
still to come are not a reason to hide them.

Import: @slices.sh/file-storage/headless

Files.WhenChildren must be inside <Files.Children>.

```tsx
GateProps & { of?: string; state: ChildrenState }

type ChildrenState = "unknown" | "loading" | "failed" | "empty" | "ready"
```

Source: @slices.sh/file-storage/src/headless/when-children.tsx

### Files.TreeToggle

action · Element: button

The arrow that opens a folder's row in place, and shuts it again.

Nothing for a file: it has nothing under it, and a row that offered an arrow it could not
keep would be saying it had contents. Nothing either where the screen is not a tree, in a
column or a strip, where the folder's entries are not drawn under it. The arrow keys do the
same from the keyboard, which is why this one is not a stop of its own.
→ docs/rules/capability-honesty.md

A folder that is still arriving has the arrow, disabled: its contents are not there to show,
and the room the arrow takes is what keeps its name in line with the names beside it.

`data-state` is `open` or `closed`, and `aria-expanded` says it as well. Children get the
folder's name, so a button can be named for the row it belongs to.

Import: @slices.sh/file-storage/headless

Files.TreeToggle must be inside <Files.Entries>.

Files.TreeToggle must be inside <Files.Tree>.

```tsx
PartProps<"button", { open: boolean; name: string }>
```

Source: @slices.sh/file-storage/src/headless/tree-toggle.tsx

### Files.Columns

content · Element: div

Miller columns: the trail of folders picked on the way down, a column per folder with its name, the column of the open folder, and the last column where the preview goes.

Import: @slices.sh/file-storage/headless

Files.Columns requires Files.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/columns.tsx

### Files.Column

content · Element: section

Miller columns: the trail of folders picked on the way down, a column per folder with its name, the column of the open folder, and the last column where the preview goes.

Import: @slices.sh/file-storage/headless

Files.Column requires Files.Columns.

```tsx
PartProps<"section">
```

Source: @slices.sh/file-storage/src/headless/column.tsx

### Files.Cascade

content · Element: div

Miller columns: the open folder, and a column after it for each folder picked on the way
down, each drawn from the children it is given.

Each column is a folder of its own, drawn as the open one is, so it has its rows, its
skeletons and its empty state as the list does, and the keys of `Files.Navigation` inside
it. The folders after the first are read through `Files.Children`, which this has to be inside. What picking a folder does is show it in the next column and leave the open folder
where it is, which is what a Miller view is: double clicking it goes into it.

`Files.CascadeEnd` is where the last column ends, and what is drawn after it, a preview
of the picked file, goes in it.

Import: @slices.sh/file-storage/headless

Files.Cascade requires Files.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/file-storage/src/headless/cascade.tsx

### Files.CurrentColumn

gate

Miller columns: the trail of folders picked on the way down, a column per folder with its name, the column of the open folder, and the last column where the preview goes.

Import: @slices.sh/file-storage/headless

Files.CurrentColumn requires Files.Columns.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/current-column.tsx

### Files.ColumnName

content · Element: h3

Miller columns: the trail of folders picked on the way down, a column per folder with its name, the column of the open folder, and the last column where the preview goes.

Import: @slices.sh/file-storage/headless

Files.ColumnName requires Files.Columns.

```tsx
PartProps<"h3", { name: string; path: string }>
```

Source: @slices.sh/file-storage/src/headless/column-name.tsx

### Files.CascadeEnd

gate

The last column of the trail, and nothing in the others: where what follows the columns goes.

Import: @slices.sh/file-storage/headless

Files.CascadeEnd requires Files.Cascade.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/cascade-end.tsx

### Files.Gallery

scope

One entry large and the rest in a strip: which is the large one, and which the stage has
stopped on.

The strip is `Files.Entries`, so it is the listing the filters and the sort already
narrowed; the stage is `Files.GalleryActive`, and a preview worth waiting for goes in
`Files.GallerySettled`.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.Gallery must be inside <Files.Root>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/gallery.tsx

### Files.GalleryActive

scope

The entry on the stage, in its own scope, so `Files.EntryName` and the rest are about it.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.GalleryActive must be inside <Files.Gallery>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/gallery-active.tsx

### Files.GallerySettled

scope

What a gallery has stopped on, in its scope: where a viewer that is slow to start goes.

Nothing while the active entry is still moving, because a stage that mounted a viewer for
every file an arrow key crossed would spend the whole scrub parsing documents nobody read.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.GallerySettled must be inside <Files.Gallery>.

```tsx
RootProps
```

Source: @slices.sh/file-storage/src/headless/gallery-settled.tsx

### Files.GalleryStep

action · Element: button

Moves the stage to the entry before or after it. Disabled at either end: a strip does not wrap.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.GalleryStep must be inside <Files.Gallery>.

Files.GalleryStep must be inside <Files.Root>.

```tsx
PartProps<"button"> & { by: -1 | 1 }
```

Source: @slices.sh/file-storage/src/headless/gallery-step.tsx

### Files.GalleryPending

gate

The stage has an entry and has not stopped on it: where a spinner goes while somebody
scrubs past files, so a viewer is only started for the one they land on.

Import: @slices.sh/file-storage/headless

Capability: previews

Files.GalleryPending must be inside <Files.Gallery>.

```tsx
GateProps
```

Source: @slices.sh/file-storage/src/headless/gallery-pending.tsx
