# Organize files

Compose file actions & trash with storage permissions, loading and failure states.

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

Public parts: 42

## 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.KeepSelection
    Files.Status
    Files.TrashEntries
      Files.TrashNotice
      Files.Undo
    Files.TrashFailed
    Files.RefreshTrash
    Files.WhenSelected
      Files.DeleteSelected
      Files.ClearSelection
    Files.NewFolder
    Files.NewFolderForm
      Files.NewFolderName
      Files.NewFolderSubmit
    Files.Clash
      Files.ClashReplace
      Files.ClashKeepBoth
      Files.ClashSkip
      Files.ClashMore
        Files.ClashAll
    Files.CarryAnnouncer
    Files.Listed
      Files.Entries
        Files.Entry
          Files.ContextMenu
            Files.ContextArea
            Files.ContextPopup
              Files.ContextItem
              Files.ContextLink
          Files.Rename
            Files.Renaming
              Files.RenameForm
                Files.RenameSubmit
                Files.RenameCancel
                Files.RenameInput
            Files.NotRenaming
              Files.RenameTrigger
            Files.RenameFailed
          Files.SelectEntry
          Files.DeleteEntry
          Files.Confirming
            Files.ConfirmDelete
            Files.CancelDelete
          Files.DropInto
      Files.SelectAll
```

## Minimal composition

```tsx
"use client"

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

export function Example() {
  return (
    <Files.Root>
      <Files.Entries>
        <Files.Entry>
          <Files.SelectEntry />
          <Files.EntryName />
          <Files.Download>Download</Files.Download>
          <Files.DeleteEntry>Delete</Files.DeleteEntry>
        </Files.Entry>
      </Files.Entries>
      <Files.Confirming>
        <Files.ConfirmDelete>Confirm deletion</Files.ConfirmDelete>
        <Files.CancelDelete>Cancel</Files.CancelDelete>
      </Files.Confirming>
      <Files.TrashNotice>
        <Files.Undo>Undo</Files.Undo>
      </Files.TrashNotice>
    </Files.Root>
  )
}

```

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

## Complete API reference

### Files.KeepSelection

scope

The selection stays among the rows on screen: a filter, a search or another folder that
takes a selected row away takes it out of the selection too.

The rows of a tree and the columns of a Miller view count as on screen: what was read under
the open folder and still passes the filters. Otherwise the file picked in the third column
would be let go of the moment it was picked.

A part of its own, not something every browser does, because a browser that keeps a
selection across folders (to move it, to send it) wants the rows it cannot see.

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

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

```tsx
RootProps
```

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

### Files.WhenSelected

gate

Something is selected. `count` is how many, for the line that says so and the actions on them.

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

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

```tsx
GateProps<{ count: number }>
```

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

### Files.Status

content · Element: div

The line under the listing: how many there are and what is picked.

Counts the entries of the folder, not the rows on screen, which a tree makes more by every
branch it opens: "12 items" is what the folder holds. Not a live region, because the
announcer already says what the listing is showing, and two of them said the same thing
twice.

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

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

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

type StatusState = {
  /** What the open folder shows, or what the search found, after the filters: the folder's own entries, not the rows a tree adds. */
  count: number
  searched: boolean
  loading: boolean
  /** The name of the one entry that is selected; `null` for none or for several. */
  selected: string | null
  selectedCount: number
}
```

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

### Files.SelectEntry

input · Element: input

The tick beside one row.

Its accessible name is the file's, because a list of them all called "Select" reads as
"Select, checkbox, unchecked" twenty times over with nothing to tell one row from another.
A pattern may still name it something else; nothing here is a word anybody wrote.

Shift extends from the last row touched, over the rows actually on screen — which is why
this reads the listing rather than asking the selection to remember it. Whether Shift was
held is a fact about the pointer, and a `change` event does not carry one, so the click
that precedes it leaves the answer here for it.

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

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

File.SelectEntry must be inside <File.Browser>.

```tsx
PartProps<"input">
```

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

### Files.SelectAll

input · Element: input

Everything on screen, ticked or cleared at once.

Over the rows that are actually shown rather than over the folder: a bucket is longer than
a page, and a control that claimed to select what has not been read would be selecting
things nobody could then see it had selected. → docs/rules/capability-honesty.md

Half a selection is `indeterminate`, which a native checkbox has as a property and not as
an attribute, so it is written to the element rather than rendered. It was an
`aria-checked="mixed"` standing in for it before, which is the statement without the fact:
a browser computes this control's state from the property, and half a selection reading as
"unchecked" is the half that gets cleared by accident.

Nothing at all where only one row may be selected: there is no "all" to offer.

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

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

```tsx
PartProps<"input">
```

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

### Files.DeleteSelected

action · Element: button

Takes everything that is selected out in one go, and one Undo puts all of it back.

Only where the backend has a trash that can give it back: deleting one entry for good asks
first, in its row, and there is no row to ask in for several. Nothing is drawn where there is
none, which is honest about what a backend can do rather than a button that asks nothing.
→ docs/rules/capability-honesty.md

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

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

Files.DeleteSelected requires Files.Root.

```tsx
PartProps<"button", { count: number }>
```

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

### Files.ClearSelection

action · Element: button

Lets go of everything that is selected.

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

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

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

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

### Files.ContextArea

content · Element: li

A right-click menu per row: the area, the popup, items and links.

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

Files.ContextArea requires Files.ContextMenu.

```tsx
PartProps<"li">
```

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

### Files.ContextMenu

scope

A right-click menu per row: the area, the popup, items and links.

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

Files.ContextMenu requires Files.Root.

Files.ContextMenu requires Files.Entries.

```tsx
RootProps
```

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

### Files.ContextPopup

content · Element: div

A right-click menu per row: the area, the popup, items and links.

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

Files.ContextPopup requires Files.ContextMenu.

```tsx
PartProps<"div"> & {
  /** The menu popup and its positioner, which the pattern styles. */
  popup?: ReactElement<ComponentProps<typeof MotionMenu>>
}
```

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

### Files.ContextItem

action · Element: button

A right-click menu per row: the area, the popup, items and links.

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

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

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

### Files.ContextLink

content · Element: a

A right-click menu per row: the area, the popup, items and links.

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

```tsx
PartProps<"a">
```

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

### Files.Rename

scope

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.Rename requires Files.Entries.

Files.Rename requires Files.Root.

```tsx
RootProps
```

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

### Files.Renaming

gate

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.Renaming requires Files.Rename.

```tsx
GateProps
```

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

### Files.RenameForm

action · Element: form

Renames the row it is in, in place. Submitting the old name does nothing.

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

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

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

```tsx
PartProps<"form">
```

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

### Files.RenameInput

input · Element: input

The new name, starting as the old one. `name="rename"` is what the form reads.

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

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

```tsx
PartProps<"input">
```

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

### Files.RenameSubmit

action · Element: button

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.RenameSubmit requires Files.Rename.

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

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

### Files.RenameCancel

action · Element: button

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.RenameCancel requires Files.Rename.

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

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

### Files.NotRenaming

gate

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.NotRenaming requires Files.Rename.

```tsx
GateProps
```

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

### Files.RenameTrigger

action · Element: button

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.RenameTrigger requires Files.Entries.

Files.RenameTrigger requires Files.Rename.

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

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

### Files.RenameFailed

gate

The rename scope of a row, gates for renaming or not, the trigger, the form with its input, save, cancel and the refusal.

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

Files.RenameFailed requires Files.Rename.

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

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

### Files.NewFolder

action · Element: button

Makes a folder in the open one, and opens its name for typing, as a file manager does.

The folder is made at once under `folderName`, or the next free name beside one that is there,
and its row takes the focus with the name selected: Enter keeps it, Escape keeps what it was
called. Nothing is asked first, because a form for a name is a step between wanting a folder
and having one, and the row is where the name was going to be typed anyway.

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

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

```tsx
PartProps<"button"> & { folderName: string }
```

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

### Files.NewFolderForm

action · Element: form

The form that makes a folder.

A form rather than a button with a prompt, because the name needs a label, an error and a
keyboard — and `window.prompt` has none of those. The name is read from the form on
submit rather than tracked on every keystroke, so the input stays uncontrolled and a skin
may put anything at all around it.

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

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

```tsx
PartProps<"form">
```

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

### Files.NewFolderName

input · Element: input

The new folder's name. `name="folder"` is what the form reads.

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

```tsx
PartProps<"input">
```

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

### Files.NewFolderSubmit

action · Element: button

Make a folder at once and type its name in its row, or make one from a name typed first.

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

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

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

### Files.Confirming

gate

This row is the one that has asked to be deleted: where the answer goes.

Escape calls it off, which is what everybody presses at a question they did not mean to
ask. `useEscape` watches for it only while something is armed, so there is no key being
watched for the rest of the time.

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

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

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

```tsx
GateProps<{ name: string; kind: "file" | "directory" }>
```

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

### Files.CancelDelete

action · Element: button

Calls the deletion off. The other half of the question, and the one Escape presses.

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

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

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

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

### Files.ConfirmDelete

action · Element: button

Deletes the row that asked, and everything under it when it is a folder.

It takes the focus when it appears, because the button that was pressed to get here has
just stopped existing: focus would otherwise fall back to the document and a keyboard
would have to walk the whole row again to answer a question it just asked.
→ https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/

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

Files.ConfirmDelete must be inside <Files.Confirming>.

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

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

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

### Files.DeleteEntry

action · Element: button

Uses reversible trash when the backend confirms it; otherwise asks before permanent deletion.

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

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

File.DeleteEntry must be inside <Files.Root>.

Files.DeleteEntry requires Files.Root.

```tsx
PartProps<"button", { name: string; kind: "file" | "directory" }>
```

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

### Files.TrashEntries

gate

The notice for the file taken out most recently, for `undoSeconds` after it was.

Only the latest, because a notice per removal stacks above the list until the list is pushed
off the screen. It is the notice that goes, never the receipt: Ctrl/Cmd+Z and
`Files.RefreshTrash` still reach every receipt the backend holds. A receipt that was already
old when this screen first had it, because it was loaded and not just made, never shows.
A number that is not finite keeps the notice there until it is undone, and so does a
partial one, which is a warning that something is still in place rather than a courtesy.

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

Capability: trash

Files.TrashEntries requires Files.Root.

```tsx
GateProps & { undoSeconds?: number }
```

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

### Files.TrashNotice

content · Element: div

The latest receipt for undoSeconds, its notice with { name, partial, count }, Undo, the refusal with its error slug, and reloading receipts after a lost response. All render nothing where the provider has no trash.

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

Capability: trash

Files.TrashNotice requires Files.TrashEntries.

```tsx
PartProps<"div", { name: string; partial: boolean; count: number }>
```

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

### Files.Undo

action · Element: button

The latest receipt for undoSeconds, its notice with { name, partial, count }, Undo, the refusal with its error slug, and reloading receipts after a lost response. All render nothing where the provider has no trash.

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

Capability: trash

Files.Undo requires Files.Root.

Files.Undo requires Files.TrashEntries.

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

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

### Files.TrashFailed

gate

The latest receipt for undoSeconds, its notice with { name, partial, count }, Undo, the refusal with its error slug, and reloading receipts after a lost response. All render nothing where the provider has no trash.

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

Capability: trash

Files.TrashFailed requires Files.Root.

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

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

### Files.RefreshTrash

action · Element: button

The latest receipt for undoSeconds, its notice with { name, partial, count }, Undo, the refusal with its error slug, and reloading receipts after a lost response. All render nothing where the provider has no trash.

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

Capability: trash

Files.RefreshTrash requires Files.Root.

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

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

### Files.Clash

gate

A name is taken where something was going, and somebody has to say what happens.

One gate for the two ways it happens, a file on its way in and an entry being moved, since to
the person they are one question: it is there while any is waiting and says the first. The
answers are `Files.ClashReplace`, `Files.ClashKeepBoth` and `Files.ClashSkip`, for this one or,
with `Files.ClashAll` ticked, for every one waiting and the ones of this batch still to come.

Escape is skip, as it is the answer that leaves everything as it was.

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

```tsx
GateProps<ClashSaid>
```

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

### Files.ClashReplace

action · Element: button

Writes the new one over the one that is there.

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

```tsx
PartProps<"button", ClashSaid>
```

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

### Files.ClashKeepBoth

action · Element: button

Gives the new one the next free name beside the one that is there. The answer that loses nothing.

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

```tsx
PartProps<"button", ClashSaid>
```

Source: @slices.sh/file-storage/src/headless/clash-keep-both.tsx

### Files.ClashSkip

action · Element: button

Leaves the new one out and the one that is there as it was.

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

```tsx
PartProps<"button", ClashSaid>
```

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

### Files.ClashMore

gate

More may follow this question, so "apply to all" has something to apply to. Around the words and the box that offer it.

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

Files.ClashMore must be inside <Files.Clash>.

```tsx
GateProps
```

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

### Files.ClashAll

input · Element: input

"Apply to all": the answer given next is the answer for every name that is taken in this
batch. Put inside `Files.ClashMore`, which is where there is something to apply it to.

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

Files.ClashAll must be inside <Files.Clash>.

```tsx
PartProps<"input">
```

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

### Files.DropInto

content · Element: div

An entry that can be picked up, and a folder that can be dropped into.

One part for both, because on screen they are one gesture and every row is capable
of each: a file is carried and a folder receives, and a folder is carried too. Splitting
them would leave a row choosing between two parts by its own kind, which is the
conditional that gates exist to remove. → docs/rules/states-gate-themselves.md

Keyboard as well as pointer, on the same reasoning as the board's `Collection.Row`: Enter
or Space picks a row up and puts it down, Escape drops it where it was. A drag that is
only a drag is a feature that nobody without a mouse has.

Which is why focus marks a target as well as the pointer does. `dragenter` fires for a
pointer alone, so a row carried by a keyboard used to travel over folder after folder
with none of them lit and nothing said — the gesture worked and gave no sign that it
would. `Files.CarryAnnouncer` is the other half of that.

Which drops are refused is `canReceive`, beside the hook, because it is path grammar and
a part reaches a service for types alone.

Files from outside the page are the other drop on a folder's row: where there is an upload
queue, the files and folders dropped on a folder go into it, as they do into the open folder
when dropped on the listing — through the same limits, and into the same queue, so what is
on its way is drawn where it is going. `move={false}` leaves the carrying out for a screen
that may upload and may not change what is there. A row that is itself an upload on its way
is neither carried nor dropped on.

`data-drop` says a folder is about to receive something, `data-invalid` that what is over it
is nothing it takes; `data-active` says this row is the one in the air.

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

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

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

```tsx
PartProps<"div"> & { move?: boolean }
```

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

### Files.CarryAnnouncer

content · Element: div

What is in the air, for somebody who cannot see it.

`Files.DropInto` can be worked with a keyboard — Enter picks a row up, Enter on a folder
puts it down, Escape leaves it where it was — and every part of that was silent: a row
lifted with no pointer said nothing, and the folder it was over was a ring nobody could
see. A gesture whose whole feedback is a highlight is a gesture only a sighted mouse has.

Its own region rather than a line in `Files.Announcer`, because a carry and a listing are
two different things happening to the same rows and one of them runs while the other is
being read out. The words are the pattern's, and so is whether to say them at all: a
configuration with nothing to move never renders this. → docs/rules/words-in-copy-looks-in-skin.md

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

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

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

type CarrySaid = {
  carrying: string | null
  /** How many go with it: the whole selection when the row in the air is one of several that are selected. */
  count: number
  over: string | null
}
```

Source: @slices.sh/file-storage/src/headless/carry-announcer.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.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.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.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.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
