# Upload files

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

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

Public parts: 50

## 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
Upload.Root
  Upload.Offline
  Upload.Available
    Upload.Tray
    Upload.Announcer
    Upload.DropZone
      Upload.CountLimit
      Upload.Over
      Upload.Limits
      Upload.Picker
    Upload.RetryAll
    Upload.Uploads
      Upload.Row
        Upload.Completed
        Upload.Retrying
        Upload.Processing
        Upload.Thumbnail
        Upload.Name
        Upload.Size
        Upload.Location
        Upload.Sending
          Upload.Ring
          Upload.Progress
          Upload.Cancel
        Upload.Retry
        Upload.Remove
        Upload.Failed
        Upload.Pause
        Upload.Resume
    Upload.Uploading
      Upload.Speed
      Upload.Eta
      Upload.Summary
    Upload.Done
      Upload.ClearFinished
    Upload.Incomplete
    Upload.Empty
    Upload.ResumeAll
    Upload.PauseAll
    Upload.QuotaMeter
    Upload.Quota
  Upload.Refused
    Upload.Refusal
  Upload.Composer
    Upload.Message
    Upload.Send
  Upload.Unavailable
  Upload.MissingConfiguration
  Upload.Recovery
  Upload.QuotaExceeded
```

## Upload a file

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Upload } from "@slices.sh/file-storage/headless"

export function Example() {
  return (
    <Upload.Root>
      <Upload.DropZone>
        <Upload.Picker aria-label="Choose files" />
        Drop files here
      </Upload.DropZone>
      <Upload.Uploads>
        <Upload.Row>
          <Upload.Name />
          <Upload.Progress />
          <Upload.Cancel>Cancel</Upload.Cancel>
        </Upload.Row>
      </Upload.Uploads>
    </Upload.Root>
  )
}

```

The provider controls storage. Local storage works without a cloud account.

## Images only

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Upload } from "@slices.sh/file-storage/headless"

export function Example() {
  return (
    <Upload.Root accept="image/*" maxFiles={1} maxSize={5 * 1024 * 1024}>
      <Upload.Picker aria-label="Choose an image" />
      <Upload.Uploads>
        <Upload.Row>
          <Upload.Name />
        </Upload.Row>
      </Upload.Uploads>
      <Upload.Refused>
        <Upload.Refusal />
      </Upload.Refused>
    </Upload.Root>
  )
}

```

The picker checks type, count and size. The server also enforces its own limits.

## Pause, resume and recover

```tsx
"use client"

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

export function Example() {
  return (
    <Upload.Root>
      <Upload.Picker aria-label="Choose files" />
      <Upload.Uploads>
        <Upload.Row>
          <Upload.Name />
          <Upload.Progress />
          <Upload.Pause>Pause</Upload.Pause>
          <Upload.Resume>Resume</Upload.Resume>
          <Upload.Retry>Retry</Upload.Retry>
        </Upload.Row>
      </Upload.Uploads>
      <Upload.Recovery>
        <Upload.ResumeAll>Resume unfinished uploads</Upload.ResumeAll>
      </Upload.Recovery>
      <Upload.QuotaExceeded>Storage quota exceeded.</Upload.QuotaExceeded>
    </Upload.Root>
  )
}

```

Resume and quota controls hide when the storage provider does not support them.

## Complete API reference

### Upload.Root

scope

The queue, its limits, and its field registration when it has a name inside a Composer.

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

```tsx
UploadRootProps

type UploadRootProps = RootProps & {
  /**
   * The key of this value in a Composer's message. With a name, inside a Composer, the uploader
   * is a field: its value is the finished files, and it holds the submit while one uploads.
   * Without one, or outside a Composer, it is a plain uploader.
   */
  name?: string
  /**
   * Left out or `true`: required, and a missing service throws. `"optional"`: renders its
   * children and every upload part does nothing, which is what a pattern's paperclip passes.
   * `false`: off. A Composer whose sink takes no files turns it off too.
   */
  enabled?: Enabled
  /** The file-storage config, `"default"` when left out. */
  config?: string
  bucket?: string
  /** The directory files land in. */
  path?: string
  /**
   * Bytes. A larger file is turned away before anything is sent. The server's own limit, in
   * `services/file-storage/limits.ts`, applies too: the lower of the two is the one enforced.
   */
  maxSize?: number
  maxFiles?: number
  /** An `accept` attribute: `"image/*,.pdf"`. Offered to the picker and checked on the way in. */
  accept?: string
  /**
   * Files from 5 MiB go up in resumable parts wherever the backend has them, so a large file
   * survives a dropped connection and can be paused. On unless turned off: a screen that had
   * to opt in sent a 2 GB file as one request. A backend without parts is sent whole.
   */
  multipart?: boolean
  /**
   * Hidden files inside a folder (`.DS_Store`, `.git/…`, `Thumbs.db`) are left out of what a
   * folder drop or folder pick brings in. Say `includeHidden` to send them. A hidden file
   * chosen on its own is always taken.
   */
  includeHidden?: boolean
  /** What a name that is taken does: replace, keep both, refuse, or `"ask"` and wait to be told. Left out, replace. */
  ifExists?: IfExists | "ask"
  /** Each file as it lands, with the permalink to keep for it. */
  onUploaded?(uploaded: Uploaded): void
  /** Where a message and its finished attachments go. Without it there is nowhere to send. */
  onSend?(message: { text: string; files: FileRecord[] }): void
}
```

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

### Upload.Available

gate

Draws its children only where `Upload.Picker` would draw an input: a file service is there
and the root is on.

It exists so what surrounds the paperclip goes with it: a label left behind an absent
picker is a word with nothing to open. → docs/rules/capability-honesty.md

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

```tsx
GateProps
```

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

### Upload.DropZone

content · Element: div

The surface files are dropped on. `data-active` while files are over it, and
`data-invalid` while none of what is over it is something it takes.

Any element can be the surface, a part included: `render={<Upload.Composer />}` makes the
whole message box the target, so a file let go over the words, the attachments or the
buttons is attached — rather than only over a strip inside it that nobody can see.

No `role` and no `tabIndex` of its own, on purpose. The zone is the biggest thing on the
screen and it does have to be reachable, and the way to do that is to make it the `<label>`
of the picker inside it — `render={<label />}` — so the whole area opens the dialog, the
input is what takes focus, and the label's own text is its accessible name. A `<div>` given
`role="button"` and a keydown handler would announce a button that is not one and, with an
input inside it, one that may not hold anything interactive either.

What a person using a screen reader is told about the drag itself is `Upload.Announcer`:
`data-active` is a colour, and a colour is not an announcement.

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

Upload.DropZone must be inside <Upload.Root>.

```tsx
DropZoneProps

type DropZoneProps = PartProps<"div", { over: boolean; refusing: boolean }> & {
  /** Files pasted anywhere inside are attached too: the way people attach a screenshot. */
  paste?: boolean
  /** With `paste`: a text of this many characters pasted into the message becomes a file. `false` keeps it inline. */
  longPaste?: number | false
}
```

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

### Upload.Picker

input · Element: input

The file input itself.

It stays in the DOM rather than being created on click, because a label pointing at it is
the one way to open a file dialog that works with a keyboard and says what it opens.
Hiding it is the skin's business; an input with `display: none` is still focusable by its
label.

Its `accept` is the root's, so the dialog offers what the queue will take — and on a phone
`"image/*"` is what puts the camera in it. What is chosen anyway still goes past the guard:
every dialog has a way to show all files.

Given `render={<Button>Attach</Button>}` it is that button instead, which opens the dialog
of a hidden input it keeps beside it. The button gets `className`, `ref` and the rest; the
input gets `multiple`, `accept`, `capture`, `name` and `disabled`. An element with nothing
inside, like `render={<Input />}` or a skin's input, is still the input: a button has a label.

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

Upload.Picker must be inside <Upload.Root>.

```tsx
PickerProps

type PickerProps = PartProps<"input"> & { directory?: boolean }
```

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

### Upload.Limits

content · Element: p

What this screen takes, said before anybody tries: how big, and what kinds.

The numbers are the root's, which are the lower of the screen's and the server's, so what is
said here is what is enforced — never a courtesy the server then contradicts. Nothing at all
where there is no limit of either kind: "Any file" is a sentence about nothing.
→ docs/rules/capability-honesty.md

`types` are the `accept` entries as they were written, `image/*` and `.pdf`; turning them
into "images or PDFs" is the pattern's, because it is words.
→ docs/rules/words-in-copy-looks-in-skin.md

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

Upload.Limits must be inside <Upload.Root>.

```tsx
PartProps<"p", LimitsSaid>

type LimitsSaid = { size: string | null; types: readonly string[] }
```

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

### Upload.CountLimit

content · Element: p

The drop surface with data-active and data-invalid, the gate while files are over it, the file input (a folder picker with directory, or a button of your own: render={<Button>Attach</Button>} opens a hidden input), the limits in words (nothing where there is none), and the count limit when maxFiles is set.

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

Upload.CountLimit requires Upload.Root

```tsx
PartProps<"p", { max: number }>
```

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

### Upload.Uploads

scope

Every upload in the queue, each in its own scope.

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

File.Uploads must be inside <File.Upload>.

```tsx
RootProps
```

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

### Upload.Row

content · Element: li

One upload's row. `data-state` is where it is, which is what a skin colours by, and
`data-kind` what it is, which is what a skin draws an icon for.

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

File.UploadRow must be inside <File.Uploads>.

```tsx
PartProps<"li", { state: UploadState }>
```

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

### Upload.Thumbnail

content · Element: img

The picture an upload is of, drawn from the file on this device.

There from the moment the file is chosen, rather than once the store has it back, and for
as long as the upload is in the queue. Nothing at all for anything that is not an image a
browser can draw: the icon beside it is the skin's, drawn from the row's `data-kind`, and
this part's answer to "no picture" is to render none. → docs/rules/capability-honesty.md

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

Upload.Thumbnail must be inside <Upload.Uploads>.

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

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

### Upload.Name

content · Element: span

The tray that is present while the queue holds anything, one scope per upload, the row with data-state and data-kind, the picture read from the device, name, size, the folders a file came from, the totals, and the live announcement.

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

File.UploadName must be inside <File.Uploads>.

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

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

### Upload.Size

content · Element: span

How big the file is. Known before a byte is sent, which is why it is worth showing.

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

Upload.Size must be inside <Upload.Uploads>.

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

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

### Upload.Location

content · Element: span

The folders a file came from, `holiday/friday`, so a queue of a dropped folder's files is
not forty names that could each be anywhere. The whole destination is its `title`.

Nothing for a file that was chosen on its own, because a line that said "/" under every row
would say nothing. → docs/rules/capability-honesty.md

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

Upload.Location must be inside <Upload.Uploads>.

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

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

### Upload.Sending

gate

This one is still on its way.

What a progress bar is for. Once a file has landed the bar is full and stays full, which
reads as still working — so the row shows its size instead, and the bar is inside this.

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

Upload.Sending must be inside <Upload.Uploads>.

```tsx
GateProps
```

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

### Upload.Progress

content · Element: progress

How far one upload has got, as a real `<progress>`.

`value` is left off while nothing has been reported, which is what makes a progress bar
indeterminate — a transport that cannot count bytes then says so, instead of sitting at
zero as if it were stuck.

The file's name is its accessible name, unless the pattern gives it a better one. A screen
with five bars on it reads as "40 percent, 12 percent, 90 percent" otherwise, and which
file each belongs to is the one thing a person cannot see.

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

File.Progress must be inside <File.Uploads>.

```tsx
PartProps<"progress", { sent: number; total: number }>
```

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

### Upload.Ring

content · Element: div

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Ring requires Upload.Uploads.

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

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

### Upload.Cancel

action · Element: button

Stops one upload, and is not rendered once it has stopped on its own.

The file's name is in `state`: a queue of twenty files is twenty buttons called "Stop
this upload" otherwise, with nothing saying which one stops which.

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

File.CancelUpload must be inside <File.Upload>.

File.CancelUpload must be inside <File.Uploads>.

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

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

### Upload.Pause

action · Element: button

Pause and resume one upload or all of them, keeping confirmed parts, and the uploads a reload interrupted, to resume by choosing the same files again. All render nothing for ordinary uploads or a provider without multipart.

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

Capability: resumable

Upload.Pause requires Upload.Root

Upload.Pause requires Upload.Uploads

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

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

### Upload.Uploading

gate

Something is going up. `count` is how many, for a line that says so.

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

File.Uploading must be inside <File.Upload>.

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

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

### Upload.Summary

content · Element: p

The tray that is present while the queue holds anything, one scope per upload, the row with data-state and data-kind, the picture read from the device, name, size, the folders a file came from, the totals, and the live announcement.

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

Upload.Summary requires Upload.Root.

```tsx
PartProps<
    "p",
    { count: number; sent: string; total: string; pending: number; failed: number; percent: number }
  >
```

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

### Upload.Speed

content · Element: span

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Speed requires Upload.Uploads.

```tsx
PartProps<"span", { rate: string; bytesPerSecond: number }>
```

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

### Upload.Eta

content · Element: span

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Eta requires Upload.Uploads.

```tsx
PartProps<"span", { seconds: number }>
```

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

### Upload.PauseAll

action · Element: button

Pause and resume one upload or all of them, keeping confirmed parts, and the uploads a reload interrupted, to resume by choosing the same files again. All render nothing for ordinary uploads or a provider without multipart.

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

Capability: resumable

Upload.PauseAll requires Upload.Root

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

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

### Upload.QuotaMeter

content · Element: meter

The bucket's use as words and as a meter, and the gate when it is full. All render nothing where the provider does not report capacity.

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

Capability: quota

Upload.QuotaMeter requires Upload.Root.

```tsx
PartProps<"meter">
```

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

### Upload.Quota

content · Element: p

The bucket's use as words and as a meter, and the gate when it is full. All render nothing where the provider does not report capacity.

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

Capability: quota

Upload.Quota requires Upload.Root.

```tsx
PartProps<"p", { used: string; limit: string; percent: number }>
```

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

### Upload.Over

gate

Files are being carried over the drop zone: where "Drop to attach" goes.

A gate rather than a colour, so what is said while a file is in the air can be a sentence —
and a different one when nothing being carried is something this zone takes. Whatever it
draws over the zone should let the pointer through, or the zone loses the drag to it.

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

Upload.Over must be inside <Upload.Root>.

```tsx
GateProps<{ refusing: boolean }>
```

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

### Upload.Empty

gate

Nothing has been dropped yet: where the invitation to drop something goes.

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

File.QueueEmpty must be inside <File.Upload>.

```tsx
GateProps
```

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

### Upload.Completed

gate

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Completed requires Upload.Uploads.

```tsx
GateProps
```

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

### Upload.Remove

action · Element: button

Takes one file off the list, whatever state it is in.

Not `Upload.Cancel`, which stops a file on its way and leaves the row there to be tried
again. This is the one a person reaches for when the file is already up and they have
changed their mind about attaching it — so it renders for every state, including the ones
where there is nothing left to stop.

The file's name is in `state`, because five of these called "Remove this attachment" are
five buttons nobody listening can tell apart. → docs/rules/words-in-copy-looks-in-skin.md

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

File.RemoveUpload must be inside <File.Upload>.

File.RemoveUpload must be inside <File.Uploads>.

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

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

### Upload.Done

gate

Everything the queue set out to send has arrived.

The mirror of `Upload.Uploading`, and not the same as "nothing is going up": a queue
nobody has put anything in has finished nothing, so this stays shut until at least one
upload has been through it. That is what lets a screen say "three files added" without
saying it to somebody who has added none.

Cancelled and failed uploads are ended but they are not done, so neither counts here —
the queue is finished with them, and the person is not.
→ docs/rules/capability-honesty.md

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

File.Done must be inside <File.Upload>.

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

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

### Upload.ClearFinished

action · Element: button

Forgets everything that ended, one way or another — the files that were turned away at the
door included, which ended before they began. Nothing ended, nothing rendered.

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

Upload.ClearFinished must be inside <Upload.Root>.

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

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

### Upload.Failed

gate

This upload was refused, with the reason.

A file that is too large and a bucket that is not yours want different sentences: one is
about the file, the other about the person. → docs/rules/capability-honesty.md

The name is in the state too, for a sentence said away from the row it belongs to — under
a tray of chips, where a chip has no room for one.

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

Upload.Failed must be inside <Upload.Uploads>.

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

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

### Upload.Retry

action · Element: button

Sends one again. Rendered for a failure and for a cancel, because both are a file the
person still has and may have changed their mind about.

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

File.RetryUpload must be inside <File.Upload>.

File.RetryUpload must be inside <File.Uploads>.

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

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

### Upload.Incomplete

gate

This upload is not a stored file yet.

Its job is to keep one file from being on screen twice. A list that shows what is in the
bucket *and* what is going up has, for a moment after the last byte lands, both — the row
the queue is still holding and the record the reload just brought back. Wrapping the queue
row in this hands each file over to the other list exactly once.

Cancelled and failed uploads stay inside it: neither is a stored file, and both still have
something to offer — a reason, and another go.

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

File.Incomplete must be inside <File.Uploads>.

```tsx
GateProps
```

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

### Upload.RetryAll

action · Element: button

Stop one on its way, send a failed one again, take one off the list, retry every failure, and forget what ended.

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

Upload.RetryAll requires Upload.Root.

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

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

### Upload.Retrying

gate

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Retrying requires Upload.Uploads.

```tsx
GateProps
```

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

### Upload.Processing

gate

Gates for still on its way, not yet stored and arrived; the bar and the ring (indeterminate until bytes are reported); speed and time left; retrying after a dropped connection; finalizing after the last byte; and the refusal with its error slug.

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

Upload.Processing requires Upload.Uploads.

```tsx
GateProps
```

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

### Upload.Resume

action · Element: button

Pause and resume one upload or all of them, keeping confirmed parts, and the uploads a reload interrupted, to resume by choosing the same files again. All render nothing for ordinary uploads or a provider without multipart.

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

Capability: resumable

Upload.Resume requires Upload.Root

Upload.Resume requires Upload.Uploads

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

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

### Upload.ResumeAll

action · Element: button

Pause and resume one upload or all of them, keeping confirmed parts, and the uploads a reload interrupted, to resume by choosing the same files again. All render nothing for ordinary uploads or a provider without multipart.

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

Capability: resumable

Upload.ResumeAll requires Upload.Root

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

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

### Upload.Recovery

content · Element: p

Pause and resume one upload or all of them, keeping confirmed parts, and the uploads a reload interrupted, to resume by choosing the same files again. All render nothing for ordinary uploads or a provider without multipart.

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

Capability: resumable

Upload.Recovery requires Upload.Root

```tsx
PartProps<"p", { count: number; names: string }>
```

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

### Upload.Refused

scope

Every file the last drop, pick or paste turned away, each in its own scope.

Apart from the queue on purpose: a file refused for its size never went anywhere, and a
row for it among the uploads would offer "Try again" on something no try could change.

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

Upload.Refused must be inside <Upload.Root>.

```tsx
RootProps
```

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

### Upload.Refusal

content · Element: li

Why one file was turned away, with everything a sentence about it needs.

The file's size beside the limit, and the kinds that are taken, because "that file is over
the limit" leaves a person guessing what the limit was. `data-state` is the reason, for a
skin that marks the two differently. The sentence is the pattern's.

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

Upload.Refusal must be inside <Upload.Refused>.

Upload.Refusal must be inside <Upload.Root>.

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

type RefusalSaid = {
  name: string
  reason: RefusalRecord["reason"]
  /** How big the file is, as people write it. */
  size: string
  /** The ceiling it went over, or `null` where it was refused for its kind. */
  limit: string | null
  /** What would have been taken instead, as the `accept` entries were written. */
  types: readonly string[]
}
```

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

### Upload.QuotaExceeded

gate

The bucket's use as words and as a meter, and the gate when it is full. All render nothing where the provider does not report capacity.

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

Capability: quota

Upload.QuotaExceeded requires Upload.Root.

```tsx
GateProps
```

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

### Upload.Offline

gate

The browser offline (queued files wait), the service missing under enabled="optional", and a production server that answers config-missing.

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

```tsx
GateProps
```

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

### Upload.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

### Upload.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

### Upload.Tray

content · Element: div

The tray that is present while the queue holds anything, one scope per upload, the row with data-state and data-kind, the picture read from the device, name, size, the folders a file came from, the totals, and the live announcement.

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

Upload.Tray requires Upload.Root.

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

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

### Upload.Announcer

content · Element: div

What the queue is doing, for somebody who cannot see it.

Everything else about an upload is on screen and nowhere else: a bar fills, a row turns
red, a footer counts. A screen reader announces none of that, so a person using one drops
a file and hears silence — and hears the same silence when it is refused.

`role="status"` with `aria-live="polite"` waits for a gap rather than interrupting, and the
text is counts rather than events so that twenty files landing is one sentence and not
twenty. A file turned away before it was sent counts as refused along with the ones the
server refused: to the person, both are a file that did not go. The words are the
pattern's: what to say about three files and one refusal is copy.
→ docs/rules/words-in-copy-looks-in-skin.md

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

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

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

type QueueSaid = { sending: number; done: number; failed: number; over: boolean; refusing: boolean }
```

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

### Upload.Composer

action · Element: form

The form a message and its attachments are sent from.

Paste is handled here rather than on the textarea, so it works wherever the focus is
inside the composer and the handler exists once. It is the way people attach a screenshot
— Slack, Linear, GitHub and Notion all take one — and the only reason to reach for the
paperclip instead is a file already on disk.

Only a paste that carries files is taken. Pasted text is text, and a clipboard that holds
both is a copied file with a name in it.

Drop it in `Upload.DropZone`'s `render` to make the whole form the place files are dropped:
`<Upload.DropZone render={<Upload.Composer />}>`.

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

Upload.Composer must be inside <Upload.Root>.

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

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

### Upload.Message

input · Element: textarea

A textarea rather than an input, because Shift+Enter has to mean something.

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

File.Message must be inside <File.Upload>.

```tsx
PartProps<"textarea">
```

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

### Upload.Send

action · Element: button

The one submit in the composer: everything beside it is `type="button"`.
→ docs/rules/one-submit-per-form.md

It is disabled while a file is still going up, which is the honest answer — a message sent
then would carry whichever of its attachments the network had happened to finish. What says
so out loud is `Upload.Uploading` beside it.

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

File.Send must be inside <File.Upload>.

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

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