# File Upload

> People add files by dropping them on the drop zone or browsing. Each file gets a row with its name, size and progress; it can be cancelled while it uploads and removed when it's done. In Arabic the rows mirror and progress fills from the right.

Source: https://ritla.app/ui/file-upload

## Installation

Installing from the registry is coming soon. Until then, these are the commands you will run.

```bash
npx ritla add file-upload
```

With the shadcn CLI instead:

```bash
npx shadcn@latest add @ritla/file-upload
```

Dependencies: `@base-ui/react`, `@phosphor-icons/react`

## Usage

```tsx
import { FileUpload, FileUploadItem, FileUploadList } from "@/components/ui/file-upload";

<FileUpload
  label="Receipts"
  accept=".pdf,.jpg"
  maxSize={10_000_000}
  helperText="PDF or JPG, up to 10 MB each"
  onFilesAdd={(accepted, rejected) => startUploads(accepted, rejected)}
>
  <FileUploadList>
    {files.map((file) => (
      <FileUploadItem
        key={file.id}
        name={file.name}
        size={file.size}
        status={file.status}
        progress={file.progress}
        onCancel={() => cancel(file.id)}
        onRemove={() => remove(file.id)}
      />
    ))}
  </FileUploadList>
</FileUpload>
```

## Examples

### Row states

While uploading the X cancels (`status="uploading"`, `progress` 0 to 100, or `null` when unknown). When done the Trash removes and the bar is full. On failure the reason shows under the name, with Try again (`onRetry`). `disabled` dims the zone and the rows.

Live preview: https://ritla.app/_kanz/preview/ltr/file-upload-states

### One file, and the camera on phones

`multiple={false}` takes one file, and `capture="environment"` opens the back camera on phones, where there is no dragging. The file name stays as its owner wrote it, in any language.

Live preview: https://ritla.app/_kanz/preview/ltr/file-upload-single

## API reference

### `FileUpload`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `ReactNode` | None | The label above the drop zone, named after the document you want ("Receipts"). |
| `required` | `boolean` | `false` | The asterisk after the label. |
| `helperText` | `ReactNode` | None | Under the zone: the allowed types, size and count, before anyone tries. |
| `error` | `ReactNode` | None | An error for the whole upload ("Add at least one receipt"), in red in place of the helper text. |
| `accept` | `string` | None | Allowed types as in `<input accept>`: `.pdf,.jpg` or `image/*`. Every file is checked, dropped or picked. |
| `maxSize` | `number` | None | Largest file, in bytes. |
| `maxFiles` | `number` | None | Most files in the list (failed rows don't count). |
| `multiple` | `boolean` | `true` | More than one file at a time. |
| `capture` | `"environment" \| "user"` | None | Opens the camera on phones instead of the file picker. |
| `disabled` | `boolean` | `false` | State = Disabled. |
| `name` | `string` | None | The file input's name, for a form that posts files itself. |
| `onFilesAdd` | `(accepted: File[], rejected: FileRejection[]) => void` | None | Picked or dropped files after the checks: start uploading `accepted` and show each rejected file as a row with its reason (`type`, `size` or `count`). Nothing uploads by itself. |
| `dropText` | `ReactNode` | None | The zone's line. Default: Figma's text in the page language. |
| `browseLabel` | `ReactNode` | None | The button's label. Default: "Browse files" (Arabic «اختيار ملفات», or «اختيار ملف» for one file). |

### `FileUploadItem`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | None | The file's own name, never translated or transliterated. |
| `size` | `number` | None | Size in bytes, shown in the page's language and numerals. |
| `status` | `"uploading" \| "done" \| "error"` | `"uploading"` | Sets the row's button: the X cancels while uploading, the Trash removes after. |
| `progress` | `number \| null` | `null` | Progress 0 to 100 while uploading; `null` for the striped bar when it isn't known. |
| `error` | `ReactNode` | None | Why it failed, in plain words ("The file is larger than 10 MB"). |
| `onCancel / onRemove` | `() => void` | None | The row's button. After a removal, focus moves to the next row's button, or the previous one, or Browse. |
| `onRetry` | `() => void` | None | Shows Try again on a failed row. |
| `disabled` | `boolean` | `false` | State = Disabled. |
| `showFileSize / showIcon` | `boolean` | `true` | Figma's Show file size and Show icon. |

## Accessibility

Keyboard (keys as in left-to-right; arrows swap in right-to-left):

- `Tab`: To Browse, then to each row's button.
- `Enter`: On Browse, opens the file dialog; on a row's button, cancels or removes.
- `Space`: The same as Enter.

ARIA:

- The drop zone is only a shortcut: Browse is a real button that opens a native file input, so everything works with a keyboard, a screen reader and on phones.
- The whole upload is a group named by its label; Browse is read with the label and described by the helper text, so a screen reader hears the types and limits before choosing.
- Each row's button carries the file name: "Cancel upload of hotel-invoice.pdf", "Remove taxi-receipt.jpg".
- A polite live region announces when a file is done or failed, not every percent. Each bar is `role="progressbar"`, named by its file.

## Arabic and RTL notes

Mirrors:

- The rows: the type icon at the start (the right), then the name and size, the button at the end (the left).
- The progress bar fills from the inline start, so from the right.

Does not mirror:

- The type icon doesn't mirror, and its extension stays Latin (PDF, JPG).
- The file name stays as written, isolated with `dir="auto"`; it is never translated or transliterated.
- The cloud, Trash and X icons don't mirror.

Numerals: The size follows `numerals`, with its unit in the page language: "2.3 MB" in English and "2.3 ميغابايت" or "٢٫٣ ميغابايت" in Arabic.

Arabic typography: The name is `body-16` Medium and the size `body-12`, with the taller Arabic line height; a long name is cut with an ellipsis rather than wrapping.
