# File upload

Lets people pick or drop files, checks them, and shows each one's progress while the app uploads it.

- **Import:** `import { FileUpload, FileUploadDropzone, FileUploadTrigger, FileUploadSubmit, FileUploadClear, FileUploadProgress, FileUploadErrors, FileUploadList, FileUploadItem, FileUploadPreview } from "@booleanpress/ui/file-upload"`
- **Page:** <https://ui.booleanpress.com/components/file-upload> · @booleanpress/ui 0.2.0

## Usage

`FileUpload` holds the files and their state; the parts inside it pick, drop, list and send them. The component never touches the network: `onUpload` receives the files and reports progress back through `onProgress`.

```tsx
import { FileUpload, FileUploadDropzone, FileUploadList, FileUploadSubmit } from "@booleanpress/ui/file-upload"

export function AttachmentUpload() {
  return (
    <FileUpload
      accept="image/*,application/pdf"
      multiple
      maxSize={5_000_000}
      onUpload={async (files, { onProgress, signal }) => {
        for (const file of files) {
          await sendToApi(file, { signal, onProgress: (percent) => onProgress(file, percent) })
        }
      }}
    >
      <FileUploadDropzone />
      <FileUploadList />
      <FileUploadSubmit />
    </FileUpload>
  )
}
```

The parts are flat and each does one job, so the same pieces make a single button, a toolbar with a list, a drop zone or a field: `FileUploadTrigger` opens the system's picker, `FileUploadDropzone` takes dropped files (and, with no children, is itself one large button), `FileUploadSubmit` sends the queued files and `FileUploadClear` aborts and empties everything. `FileUploadList` shows a `FileUploadItem` per file (name, size in the provider's locale, a picture or type icon, a progress bar while uploading, the status and a remove button); `layout="grid"` makes cards with large pictures. `FileUploadProgress` is one bar for the whole batch and `FileUploadErrors` lists why files were refused. For a layout of your own, `useFileUpload()` returns the files and the actions.

Files are checked as they arrive, in this order: `accept` (extensions, `image/*` wildcards and exact types), `maxSize` in bytes, then `maxFiles`. A dropped folder is refused with its own message (`folderNotAllowed`), as the browser lists it among the files but cannot read it. A refused file never joins the list; its message shows in `FileUploadErrors` until the next files arrive, and `onReject` receives the reasons. Without `multiple`, a new file replaces the one chosen. With `auto` each batch uploads as soon as it is added. In `onUpload`, call `onProgress(file, percent)` as bytes go out, `onError(file, message)` for a file the server refused, and resolve when the batch is done; a rejected promise marks the whole batch as failed. `signal` aborts when the list is cleared, when every file of the batch is removed or replaced, or when the component unmounts: pass it to `fetch`. `fileSignal(file)` is one file's signal, which also aborts when that file is removed: pass it to that file's request, so removing a file during its upload stops it, and catch that request's abort so the rest of the batch carries on. `defaultFiles` seeds the list once, checked like added files; `onFilesChange` reports every change. `formatFileSize(bytes, locale)` writes a size the way the list does.

## Examples

### Basic

A Choose button, the names of the chosen files beside it, and an Upload button that sends them.

```tsx
import {
  FileUpload,
  FileUploadErrors,
  FileUploadSubmit,
  FileUploadTrigger,
  useFileUpload,
  type FileUploadHelpers,
} from "@booleanpress/ui/file-upload"

// Stands in for the app's request: every file finishes after a fixed delay.
function upload(files: File[], { onProgress }: FileUploadHelpers) {
  return new Promise<void>((resolve) =>
    setTimeout(() => {
      files.forEach((file) => onProgress(file, 100))
      resolve()
    }, 600)
  )
}

function ChosenFiles() {
  const { files } = useFileUpload()
  return <span>{files.length > 0 ? files.map((entry) => entry.file.name).join(", ") : "No file chosen"}</span>
}

export default function FileUploadBasic() {
  return (
    <FileUpload accept="image/*" multiple maxSize={1_000_000} onUpload={upload} className="w-full max-w-xl">
      <FileUploadErrors />
      <div className="flex items-center justify-between gap-3">
        <div className="flex min-w-0 flex-wrap items-center gap-3">
          <FileUploadTrigger />
          <ChosenFiles />
        </div>
        <FileUploadSubmit />
      </div>
    </FileUpload>
  )
}
```

### Auto upload

`auto` uploads each file as soon as it is chosen; the list shows its progress and then its status.

```tsx
import { PlusIcon } from "lucide-react"
import { FileUpload, FileUploadErrors, FileUploadList, FileUploadTrigger, type FileUploadHelpers } from "@booleanpress/ui/file-upload"

// Stands in for the app's request: progress in fixed steps, every 250 ms.
function upload(files: File[], { onProgress, signal }: FileUploadHelpers) {
  return new Promise<void>((resolve) => {
    let percent = 0
    const timer = setInterval(() => {
      percent += 25
      files.forEach((file) => onProgress(file, percent))
      if (percent >= 100) {
        clearInterval(timer)
        resolve()
      }
    }, 250)
    signal.addEventListener("abort", () => clearInterval(timer))
  })
}

export default function FileUploadAuto() {
  return (
    <FileUpload accept="image/*" maxSize={1_000_000} auto onUpload={upload} className="w-full max-w-md items-center">
      <FileUploadTrigger>
        <PlusIcon />
        Browse
      </FileUploadTrigger>
      <FileUploadErrors className="w-full" />
      <FileUploadList className="w-full" />
    </FileUpload>
  )
}
```

### Advanced

A toolbar with Choose, Upload and Cancel above the batch's progress bar and the list; the whole card takes dropped files.

```tsx
import {
  FileUpload,
  FileUploadClear,
  FileUploadDropzone,
  FileUploadErrors,
  FileUploadList,
  FileUploadProgress,
  FileUploadSubmit,
  FileUploadTrigger,
  type FileUploadHelpers,
} from "@booleanpress/ui/file-upload"

// Stands in for the app's request: progress in fixed steps, every 200 ms.
function upload(files: File[], { onProgress, signal }: FileUploadHelpers) {
  return new Promise<void>((resolve) => {
    let percent = 0
    const timer = setInterval(() => {
      percent += 20
      files.forEach((file) => onProgress(file, percent))
      if (percent >= 100) {
        clearInterval(timer)
        resolve()
      }
    }, 200)
    signal.addEventListener("abort", () => clearInterval(timer))
  })
}

export default function FileUploadAdvanced() {
  return (
    <FileUpload accept="image/*" multiple maxSize={1_000_000} onUpload={upload} className="w-full max-w-xl">
      <FileUploadDropzone className="rounded-md border-solid p-0 data-[dragging]:border-dashed">
        <div className="flex flex-wrap items-center gap-2 p-5">
          <FileUploadTrigger />
          <FileUploadSubmit />
          <FileUploadClear />
        </div>
        <div className="flex flex-col gap-3.5 px-4 pb-4">
          <FileUploadErrors />
          <FileUploadProgress />
          <FileUploadList empty={<p>Drag and drop files here to upload.</p>} />
        </div>
      </FileUploadDropzone>
    </FileUpload>
  )
}
```

### In an input group

`FileUploadTrigger asChild` turns an input group's button into the picker, and the field shows the chosen name.

```tsx
import { TagIcon, UploadIcon } from "lucide-react"
import { FileUpload, FileUploadTrigger, useFileUpload } from "@booleanpress/ui/file-upload"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@booleanpress/ui/input-group"

function ImportField() {
  const { files } = useFileUpload()
  return (
    <InputGroup>
      <InputGroupAddon>
        <TagIcon />
      </InputGroupAddon>
      <InputGroupInput aria-label="Contacts file" placeholder="No file chosen" readOnly value={files[0]?.file.name ?? ""} />
      <InputGroupAddon align="inline-end">
        <FileUploadTrigger asChild>
          <InputGroupButton>
            <UploadIcon />
            Browse
          </InputGroupButton>
        </FileUploadTrigger>
      </InputGroupAddon>
    </InputGroup>
  )
}

export default function FileUploadInputGroup() {
  return (
    <FileUpload accept=".csv,text/csv" className="w-full max-w-sm">
      <ImportField />
    </FileUpload>
  )
}
```

### Custom upload

An `onUpload` of your own reports each file's progress through `onProgress`; here fixed timers stand in for the request.

```tsx
import { FileUpload, FileUploadList, FileUploadSubmit, type FileUploadHelpers } from "@booleanpress/ui/file-upload"

const LOGS = [
  new File([new Uint8Array(48_200)], "delivery-log-october.csv", { type: "text/csv" }),
  new File([new Uint8Array(1_250_000)], "bounce-report-october.pdf", { type: "application/pdf" }),
]

// The app's own upload, faked with fixed timers: each file moves at its own pace and the batch ends when all are done.
function upload(files: File[], { onProgress, signal }: FileUploadHelpers) {
  return new Promise<void>((resolve) => {
    const progress = files.map(() => 0)
    const timer = setInterval(() => {
      files.forEach((file, index) => {
        progress[index] = Math.min(100, progress[index] + 10 + index * 15)
        onProgress(file, progress[index])
      })
      if (progress.every((percent) => percent === 100)) {
        clearInterval(timer)
        resolve()
      }
    }, 300)
    signal.addEventListener("abort", () => clearInterval(timer))
  })
}

export default function FileUploadCustomUpload() {
  return (
    <FileUpload multiple defaultFiles={LOGS} onUpload={upload} className="w-full max-w-xl">
      <FileUploadList />
      <div className="flex justify-end">
        <FileUploadSubmit variant="default">Send to the archive</FileUploadSubmit>
      </div>
    </FileUpload>
  )
}
```

### Drop zone

With no children the drop zone is one large button: drop files on it, or press it to browse.

```tsx
import {
  FileUpload,
  FileUploadClear,
  FileUploadDropzone,
  FileUploadErrors,
  FileUploadList,
  FileUploadSubmit,
  type FileUploadHelpers,
} from "@booleanpress/ui/file-upload"

// Stands in for the app's request: progress in fixed steps, every 200 ms.
function upload(files: File[], { onProgress, signal }: FileUploadHelpers) {
  return new Promise<void>((resolve) => {
    let percent = 0
    const timer = setInterval(() => {
      percent += 20
      files.forEach((file) => onProgress(file, percent))
      if (percent >= 100) {
        clearInterval(timer)
        resolve()
      }
    }, 200)
    signal.addEventListener("abort", () => clearInterval(timer))
  })
}

export default function FileUploadDropzoneExample() {
  return (
    <FileUpload accept="image/*,application/pdf" multiple maxSize={5_000_000} onUpload={upload} className="w-full max-w-xl">
      <FileUploadDropzone />
      <FileUploadErrors />
      <FileUploadList />
      <div className="flex justify-end gap-2">
        <FileUploadClear variant="ghost" />
        <FileUploadSubmit variant="default" />
      </div>
    </FileUpload>
  )
}
```

### Image previews

`layout="grid"` shows each image as a card with its picture; the remove button shows on hover and on focus.

```tsx
import { PlusIcon, UploadIcon } from "lucide-react"
import { FileUpload, FileUploadList, FileUploadSubmit, FileUploadTrigger, type FileUploadHelpers } from "@booleanpress/ui/file-upload"

// Pictures drawn here, as SVG, so the example needs no network.
function picture(name: string, from: string, to: string) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200"><defs><linearGradient id="g" x2="1" y2="1"><stop stop-color="${from}"/><stop offset="1" stop-color="${to}"/></linearGradient></defs><rect width="320" height="200" fill="url(#g)"/><circle cx="244" cy="58" r="26" fill="#fff" fill-opacity=".75"/><path d="M0 200 110 92l80 66 52-40 78 72v10z" fill="#fff" fill-opacity=".4"/></svg>`
  return new File([svg], name, { type: "image/svg+xml" })
}

const PICTURES = [
  picture("newsletter-header.svg", "#0ea5e9", "#6366f1"),
  picture("welcome-banner.svg", "#f59e0b", "#ef4444"),
  picture("team-offsite.svg", "#10b981", "#0f766e"),
]

// Stands in for the app's request: every file finishes after a fixed delay.
function upload(files: File[], { onProgress }: FileUploadHelpers) {
  return new Promise<void>((resolve) =>
    setTimeout(() => {
      files.forEach((file) => onProgress(file, 100))
      resolve()
    }, 700)
  )
}

export default function FileUploadImagePreview() {
  return (
    <FileUpload accept="image/*" multiple defaultFiles={PICTURES} onUpload={upload} className="w-full max-w-xl">
      <div className="flex flex-wrap gap-2">
        <FileUploadTrigger variant="outline">
          <PlusIcon />
          Add images
        </FileUploadTrigger>
        <FileUploadSubmit variant="default">
          <UploadIcon />
          Upload all
        </FileUploadSubmit>
      </div>
      <FileUploadList
        layout="grid"
        empty={
          <p className="rounded-lg border border-dashed border-border p-10 text-center text-muted-foreground">
            No images chosen. Add images to see them here.
          </p>
        }
      />
    </FileUpload>
  )
}
```

### Validation errors

`accept`, `maxSize` and `maxFiles` refuse a wrong type, a file that is too large and one file too many, each with its message.

```tsx
import { FileUpload, FileUploadDropzone, FileUploadErrors, FileUploadList } from "@booleanpress/ui/file-upload"

// Five files offered at once: two pass, and one fails each rule.
const OFFERED = [
  new File([new Uint8Array(84_000)], "logo.png", { type: "image/png" }),
  new File(["Call notes, 3 October"], "notes.txt", { type: "text/plain" }),
  new File([new Uint8Array(2_400_000)], "office-photo.jpg", { type: "image/jpeg" }),
  new File([new Uint8Array(120_000)], "header.jpg", { type: "image/jpeg" }),
  new File([new Uint8Array(96_000)], "footer.png", { type: "image/png" }),
]

export default function FileUploadValidation() {
  return (
    <FileUpload
      accept="image/png,image/jpeg"
      multiple
      maxSize={1_000_000}
      maxFiles={2}
      defaultFiles={OFFERED}
      className="w-full max-w-xl"
    >
      <FileUploadDropzone description="PNG or JPG, up to 1 MB each, 2 files at most" />
      <FileUploadErrors />
      <FileUploadList />
    </FileUpload>
  )
}
```

### Disabled

`disabled` stops picking, dropping, uploading and removing; the files already listed stay.

```tsx
import { FileUpload, FileUploadDropzone, FileUploadList } from "@booleanpress/ui/file-upload"

const ATTACHED = [new File([new Uint8Array(48_200)], "delivery-log-october.csv", { type: "text/csv" })]

export default function FileUploadDisabled() {
  return (
    <FileUpload disabled multiple defaultFiles={ATTACHED} className="w-full max-w-xl">
      <FileUploadDropzone description="Uploads are paused while the archive is moved" />
      <FileUploadList />
    </FileUpload>
  )
}
```

### Upload error

An `onUpload` that rejects: the file's row shows "Upload failed" while the rest of the page stays as it was.

```tsx
import { FileUpload, FileUploadList, FileUploadSubmit } from "@booleanpress/ui/file-upload"

const REPORT = new File([new Uint8Array(210_000)], "bounce-report-october.csv", { type: "text/csv" })

// The server refuses the whole batch after a moment: the rejection marks every file of it as failed.
function upload() {
  return new Promise<void>((_, reject) => {
    setTimeout(() => reject(new Error("Acme Archive answered with 503")), 800)
  })
}

export default function FileUploadUploadError() {
  return (
    <FileUpload defaultFiles={[REPORT]} onUpload={upload} className="w-full max-w-xl">
      <FileUploadList />
      <div className="flex justify-end">
        <FileUploadSubmit variant="default">Send to the archive</FileUploadSubmit>
      </div>
    </FileUpload>
  )
}
```

## Accessibility

**Semantics.** Every way to add files is a native `button`; the file input itself is hidden. The list is a `ul` of `li` rows; a row's progress is a `progressbar` named after its file, and its status is text. Messages about added, refused, uploaded and failed files go to a polite live region (`role="status"`) inside the root, so they are read without moving focus.

**Labels.** The buttons carry their own words (`chooseFiles`, `upload`, `cancel`); a remove button is named "Remove {file name}" (`removeItem`). The drop zone's button is named by its two lines. With `asChild`, name your element yourself. Refusal messages name the file and the rule: `fileTooLarge`, `fileTypeNotAllowed`, `tooManyFiles`, `folderNotAllowed`.

**Focus.** The picker is a system dialog: focus returns to the button that opened it. Removing a file moves focus to the next row's remove button, else the previous one's, else the first button that adds files, so it never falls to the page.

**Known limits.**

- Dragging files is a pointer gesture only; every drop zone also opens the picker by click, Enter or Space, so nothing depends on dragging (WCAG 2.5.7).
- A refused file is not listed. Its message stays in `FileUploadErrors` until the next files arrive or the list is cleared.
- Removing a file during its upload aborts its `fileSignal(file)`; a request given only the batch's `signal` stops when the last file of the batch is removed.
- A dropped folder is refused, not opened: its files are not added. People choose or drop the files themselves.
- Files leave through your upload function, not with a form's native submit: FileUpload takes no `name`, and a form's Reset leaves its list alone.
- Sizes are in steps of 1,000 with the short units B, KB, MB and GB, the same in every language; the number is formatted by `Intl` in the provider's locale.
- An image's picture is a blob URL made in the browser after it loads; the server renders the type icon in its place.
- On a card the remove button is hidden until the card is hovered or holds focus; on touch screens it always shows.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter or Space | On Choose, or on a drop zone with no children, opens the system's file picker. |
| Enter or Space | On Upload, sends the queued files; on Cancel, aborts the uploads and empties the list. |
| Enter or Space | On a file's remove button, removes it and moves focus to the next file's remove button. |
| Tab | Moves through the buttons and each file's remove button in reading order. |

## API

### FileUpload

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `accept` | `string` |  | The file types to take, as the native input's `accept`: `"image/*"`, `".csv,.txt"`, `"application/pdf"`. |
| `auto` | `boolean` | `false` | Uploads files as soon as they are added, so no Upload button is needed. |
| `defaultFiles` | `File[]` |  | Files in the list from the start, checked like added ones. Read once, on mount. |
| `disabled` | `boolean` | `false` | Stops picking, dropping, uploading and removing; the parts show as disabled. |
| `maxFiles` | `number` |  | The most files the list holds, with `multiple`. Files over it are refused with `tooManyFiles`. |
| `maxSize` | `number` |  | The largest file, in bytes. A larger one is refused with the provider's `fileTooLarge`. |
| `multiple` | `boolean` | `false` | Takes several files; without it, a new file replaces the one chosen. |
| `onFilesChange` | `((files: FileUploadFile[]) => void)` |  | Called with the whole list whenever a file is added, removed or changes status. |
| `onReject` | `((rejections: FileUploadRejection[]) => void)` |  | Called with the files that were refused, and why. |
| `onUpload` | `FileUploadHandler` |  | Sends the files; the component itself never touches the network. |

### FileUploadDropzone

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `description` | `ReactNode` |  | The prompt's second line, without children. Defaults to the provider's `browseFiles`. |
| `title` | `ReactNode` |  | The prompt's first line, without children. Defaults to the provider's `dropFilesHere`. |

### FileUploadTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Gives the part's behaviour to your element, such as an `InputGroupButton`, and none of `Button`'s look: `variant`, `size` and the other style props are not passed on. |
| `loading` | `boolean` |  | Shows the spinner in place of the leading icon, sets `aria-busy` and `aria-disabled` and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With `asChild` the child (a link) gets the same. |
| `raised` | `boolean \| null` |  | `Button`'s raised shadow. |
| `rounded` | `boolean \| null` |  | `Button`'s pill shape. |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | `Button`'s severity colour. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  | `Button`'s size. Without children the part shows its own icon and words. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` |  | The button's look; `default` (the primary fill) by default. |

### FileUploadSubmit

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Gives the part's behaviour to your element, such as an `InputGroupButton`, and none of `Button`'s look: `variant`, `size` and the other style props are not passed on. |
| `loading` | `boolean` |  | Shows the spinner in place of the leading icon, sets `aria-busy` and `aria-disabled` and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With `asChild` the child (a link) gets the same. |
| `raised` | `boolean \| null` |  | `Button`'s raised shadow. |
| `rounded` | `boolean \| null` |  | `Button`'s pill shape. |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | `Button`'s severity colour. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  | `Button`'s size. Without children the part shows its own icon and words. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `secondary` | The button's look; `secondary` by default. It is disabled while no file is queued. |

### FileUploadClear

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Gives the part's behaviour to your element, such as an `InputGroupButton`, and none of `Button`'s look: `variant`, `size` and the other style props are not passed on. |
| `loading` | `boolean` |  | Shows the spinner in place of the leading icon, sets `aria-busy` and `aria-disabled` and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With `asChild` the child (a link) gets the same. |
| `raised` | `boolean \| null` |  | `Button`'s raised shadow. |
| `rounded` | `boolean \| null` |  | `Button`'s pill shape. |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | `Button`'s severity colour. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  | `Button`'s size. Without children the part shows its own icon and words. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `secondary` | The button's look; `secondary` by default. It is disabled while there is nothing to clear. |

### FileUploadProgress

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `getValueLabel` | `((value: number, max: number) => string)` |  |  |
| `max` | `number` |  |  |
| `showValue` | `boolean` |  | Writes the value inside the filled part: `getValueLabel`'s text, a percentage by default. Not drawn at `sm`, in segments or while indeterminate. |
| `size` | `"default" \| "sm" \| "lg"` |  | The bar's height: `sm` 8 px, `default` 18 px, `lg` 24 px. |
| `steps` | `number` |  | Draws the bar in this many equal segments, such as the steps of a setup. |

### FileUploadErrors

Renders a `ul` and passes it every other prop.

### FileUploadList

Renders a `ul` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `((file: FileUploadFile) => ReactNode)` |  | Draws each file yourself; by default each is a `FileUploadItem`. |
| `empty` | `ReactNode` |  | Shown in place of the list while it is empty. |
| `layout` | `"grid" \| "list"` | `list` | `list` puts one file per row; `grid` lays the files out as cards with a large picture. |

### FileUploadItem

Renders a `li` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `file` (required) | `FileUploadFile` |  | The entry to show, from `useFileUpload().files` or the list's render function. |
| `preview` | `boolean` | `true` | Shows the picture of an image, or the icon of the file's type. |

### FileUploadPreview

Renders a `span` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `file` (required) | `File` |  | The file to show: a picture for an image, the icon of its type otherwise. |

**Also exported:** `useFileUpload`, a hook for the parts' shared state; call it inside the component's provider; `formatFileSize`, a helper the parts use.

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="file-upload"` (FileUpload), `data-slot="file-upload-dropzone"` (FileUploadDropzone), `data-slot="file-upload-progress"` (FileUploadProgress), `data-slot="file-upload-errors"` (FileUploadErrors), `data-slot="file-upload-list"` (FileUploadList), `data-slot="file-upload-item-status"` (FileUploadItem), `data-slot="file-upload-preview"` (FileUploadPreview), and `data-disabled`, `data-dragging`, `data-kind`, `data-layout`, `data-status`.

**Provider strings:** `folderNotAllowed`, `fileTypeNotAllowed`, `fileTooLarge`, `tooManyFiles`, `uploadFailed`, `uploadComplete`, `fileAdded`, `filesAdded`, `dropFilesHere`, `browseFiles`, `chooseFiles`, `upload`, `cancel`, `removeItem` (`BooleanUIProvider`'s `strings`).

## Theming

The drop zone is a dashed `--border` edge that turns `--primary` while files are dragged over it; its icon and second line are `--muted-foreground`. Rows are divided by `--border`; a file with no picture shows its type icon on `--secondary`. Progress bars are `Progress` at 4 px. The status uses the success and destructive tag tokens, and refusal messages `--destructive-subtle`, `--destructive-border` and `--destructive-strong`.

| Token | Used for |
| --- | --- |
| `--border` | border |
| `--destructive-border` | border |
| `--destructive-strong` | text |
| `--destructive-subtle` | background |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--primary` | border |
| `--ring` | outline |
| `--secondary` | background |
| `--secondary-foreground` | text |
