# Upload (https://v2.s3.dimah.dev/docs/react/ui/components/upload)

The upload family renders one [`useUpload`](https://v2.s3.dimah.dev/docs/react/hooks/upload) hook.
`Upload` holds the hook, and the parts inside it read it: a button that opens
the file picker, a dropzone with the progress inside it, and the status of
the queue. `UploadToasts` reports on the same hook from anywhere in the tree.

For a form value, use [`UploadField`](https://v2.s3.dimah.dev/docs/react/ui/components/upload-field).

## Preview [#preview]

Drop files on the dropzone or click it. Progress shows inside the dropzone.

<DemoPreview name="upload-demo.tsx">
  <UploadDemo />
</DemoPreview>

## Install [#install]

Complete [UI Setup](https://v2.s3.dimah.dev/docs/react/ui) first: the stylesheet and one
`<Toaster />`. Then add this component:

<Tabs items={["npm package", "shadcn registry"]}>

<Tab value="npm package">

```tsx
import {
  Upload,
  UploadDropzone,
  UploadDropzoneTrigger,
  UploadFileItem,
  UploadFileList,
  UploadStatus,
  UploadToasts,
  UploadTrigger,
  usePasteFiles,
  useUploadHints,
} from "@dimah-s3/ui";
```

</Tab>

<Tab value="shadcn registry">

```npm
npx shadcn@latest add @dimah-s3/upload
```

</Tab>

</Tabs>

---

## Usage [#usage]

```tsx
"use client";

import { useUpload } from "@dimah-s3/react";
import { Upload, UploadDropzone, UploadToasts } from "@dimah-s3/ui";

export function Attachments() {
  const upload = useUpload({ route: "attachments", maxFiles: 5, queue: true });

  return (
    <>
      <Upload upload={upload}>
        <UploadDropzone />
      </Upload>
      <UploadToasts upload={upload} />
    </>
  );
}
```

`Upload` renders the hidden file input and no element of its own, so place
its parts in your own layout. One hook can have several parts: a button in a
toolbar and a dropzone below it share the same queue.

| Part                    | Renders                                                                   |
| ----------------------- | ------------------------------------------------------------------------- |
| `UploadTrigger`         | A `Button` that opens the file picker                                     |
| `UploadDropzone`        | A drop area. By default, a trigger with the route's limits and the status |
| `UploadDropzoneTrigger` | The part of a dropzone you click, or focus and press Enter, to pick files |
| `UploadStatus`          | One card for one file, or a summary above the file list for several       |
| `UploadFileList`        | Every file in the queue, as rows or a grid. A function child renders each |
| `UploadFileItem`        | The card for one file, with its progress, error, and cancel or retry      |
| `UploadToasts`          | Progress, results, and rejected files as toasts. It renders no element    |

## Examples [#examples]

### Button [#button]

`UploadTrigger` with `UploadStatus` under it. The status renders nothing
until a file is picked.

<DemoPreview name="upload-button-demo.tsx">
  <UploadButtonDemo />
</DemoPreview>

### Button styles [#button-styles]

`UploadTrigger` is a `Button`: `variant`, `size`, and every other prop are
forwarded, and `children` replace the icon and label. An icon-only trigger
needs an `aria-label`. `tooltip` adds a tooltip. All the triggers below share
one hook and one status.

<DemoPreview name="upload-trigger-variants-demo.tsx">
  <UploadTriggerVariantsDemo />
</DemoPreview>

### Image grid [#image-grid]

Give `UploadDropzone` children to arrange it yourself. Here the trigger has
its own text, and `UploadStatus layout="grid"` shows the images as tiles
inside the dropzone. The route's limits still describe the trigger for
screen readers.

<DemoPreview name="upload-gallery-demo.tsx">
  <UploadGalleryDemo />
</DemoPreview>

### Compact dropzone [#compact-dropzone]

A one-line dropzone for a form, with the status outside it. `useUploadHints`
returns the route's limits as text, the same lines the default dropzone
shows.

<DemoPreview name="upload-compact-demo.tsx">
  <UploadCompactDemo />
</DemoPreview>

### Custom file rows [#custom-file-rows]

Pass a function to `UploadFileList` to render each file yourself. It gets one
entry of `upload.files` with its `phase`, `progress`, and `error`.

<DemoPreview name="upload-file-list-demo.tsx">
  <UploadFileListDemo />
</DemoPreview>

### Paste [#paste]

`usePasteFiles` uploads files pasted anywhere on the page. It leaves pastes
into form fields and editable text alone, and pastes a focused dropzone
already took.

<DemoPreview name="upload-paste-demo.tsx">
  <UploadPasteDemo />
</DemoPreview>

### Disabled [#disabled]

Pass `disabled` to `useUpload`. Every part follows it: the trigger and the
dropzone disable, and drops and pastes do nothing.

<DemoPreview name="upload-disabled-demo.tsx">
  <UploadDisabledDemo />
</DemoPreview>

## Dropzone [#dropzone]

`UploadDropzone` takes files dropped anywhere on it, also over the status
inside it. Its default content is an `UploadDropzoneTrigger` and an
`UploadStatus`:

```tsx
<UploadDropzone />

// is the same as
<UploadDropzone>
  <UploadDropzoneTrigger />
  <UploadStatus />
</UploadDropzone>
```

Pass `children` to change that. Leave out `UploadStatus` to render it
somewhere else, or give it `layout="grid"`.

```tsx
<UploadDropzone className="min-h-48">
  <UploadDropzoneTrigger>
    <p className="text-sm font-medium">Drop photos here</p>
  </UploadDropzoneTrigger>
</UploadDropzone>
<UploadStatus />
```

`UploadDropzoneTrigger` is a button: a click, Enter, or Space opens the
picker, and files pasted while it has focus upload. Its default content is
an icon, a title, and the route's size and type limits. `children` replace
them; the limits stay attached as its description. Do not put buttons or
links inside the trigger. Put them next to it, in the dropzone.

While files are dragged over it, the dropzone sets `data-drag-active`, and
`data-drag-reject` when they cannot be dropped. The default title says which,
and a polite live region announces it. The dropzone sets `data-disabled`
while the hook is disabled or the root is full.

The trigger and the dropzone stay enabled while files upload. New files join
the queue when the hook has `queue: true`, and replace it otherwise. While
the root is full, as in a full [`UploadField`](https://v2.s3.dimah.dev/docs/react/ui/components/upload-field),
"Maximum files reached." leads the trigger's description.

## Status [#status]

`UploadStatus` renders nothing while the hook is idle. One file shows as its
own card. Several files show as one card: a summary with the overall
progress ring, the count, and the time left, then a row for each file. The
summary has "Cancel all uploads" while they upload, and a dismiss action once
they settle, with "Retry failed uploads" when some failed. Each failed file
has its own retry action, which turns into its cancel action as the file goes
back in flight, so focus stays on it. The rows scroll past 20rem.
Pause appears while files upload when the hook has an `uploadStore`, so the
uploads can [resume](https://v2.s3.dimah.dev/docs/react/upload-store). It sets `data-phase` to the
hook's phase.

`layout="grid"` shows the files as tiles, with the summary above them, on
both `UploadStatus` and `UploadFileList`.

When cancel, pause, dismiss, or remove takes away the card that has focus,
focus moves to the next action in the same list, then the previous one, then
the root's trigger or dropzone.

## Toasts [#toasts]

Parts never toast. Mount `UploadToasts` once per hook, and one `<Toaster />`
in your root layout ([UI Setup](https://v2.s3.dimah.dev/docs/react/ui)).

- While files upload, a progress toast with a cancel action.
- Once the queue settles, the result. A failure has a Retry action.
- For each call that rejects files, the reason, formatted from the error code.

A second `UploadToasts` for the same hook renders nothing and warns in
development. Inside an [`ObjectList`](https://v2.s3.dimah.dev/docs/react/ui/components/object-list)
your `UploadToasts` replaces the list's built-in toasts.

## Call options [#call-options]

`call` on `UploadTrigger` and `UploadDropzone` applies to the files they take
in. Its `uploadOptions` merge over the hook's, and `maxFiles` lowers the
hook's limit for that call.

```tsx
<UploadTrigger
  call={{ uploadOptions: { metadata: { album: albumId } } }}
  tooltip="Add photos"
/>
```

Inside an `ObjectList` with the same hook, the source's `uploadOptions` for
the open folder apply first, so files land in that folder. Your `call` merges
over them.

## Your own parts [#your-own-parts]

`useUploadContext()` returns the `upload` of the nearest `Upload` and its
`isFull` flag. Pass `isFull` to `Upload` (as a full `UploadField` does) to
render `UploadTrigger` and `UploadDropzoneTrigger` focusable but
`aria-disabled`; it changes only how they render, so the `upload` must take
no files itself.

---

## Props [#props]

```ts
import type {
  UploadProps,
  UploadTriggerProps,
  UploadDropzoneProps,
  UploadDropzoneTriggerProps,
  UploadStatusProps,
  UploadFileListProps,
  UploadFileItemProps,
  UploadToastsProps,
  UsePasteFilesOptions,
} from "@dimah-s3/ui";
```

### Upload [#upload]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload.tsx"
  name="UploadProps"
/>

### UploadTrigger [#uploadtrigger]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-trigger.tsx"
  name="UploadTriggerProps"
/>

### UploadDropzone [#uploaddropzone]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-dropzone.tsx"
  name="UploadDropzoneProps"
/>

### UploadDropzoneTrigger [#uploaddropzonetrigger]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-dropzone.tsx"
  name="UploadDropzoneTriggerProps"
/>

### UploadStatus [#uploadstatus]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-status.tsx"
  name="UploadStatusProps"
/>

### UploadFileList [#uploadfilelist]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-file-list.tsx"
  name="UploadFileListProps"
/>

### UploadFileItem [#uploadfileitem]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-file-list.tsx"
  name="UploadFileItemProps"
/>

### UploadToasts [#uploadtoasts]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-toasts.tsx"
  name="UploadToastsProps"
/>

### usePasteFiles [#usepastefiles]

<AutoTypeTable
  path="packages/ui/src/hooks/use-paste-files.ts"
  name="UsePasteFilesOptions"
/>

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="How do I keep the picker closed in some cases?">

Call `event.preventDefault()` in the trigger's `onClick`:

```tsx
<UploadTrigger
  onClick={(event) => {
    if (!signedIn) event.preventDefault();
  }}
/>
```

</Accordion>

<Accordion title="How do I disable only the button?">

Pass `disabled` to `UploadTrigger` to disable only that trigger while the
hook stays enabled. While the hook is disabled, the picker stays closed
whatever the prop says:

```tsx
<UploadTrigger disabled={!canEdit} />
```

</Accordion>

<Accordion title="How do I clear the status after an upload?">

Use the dismiss action on a settled card, or call `upload.reset()`.

</Accordion>

<Accordion title="Where did UploadButton go?">

`UploadTrigger` is the button, and `UploadStatus` is the status it used to
render under it. Put them together, as in [Button](#button).

</Accordion>

</Accordions>
