# File upload

File upload is a neobrutalist React component from Cruise UI. A drag-and-drop zone that's also a button, with a file list, thumbnails, progress and remove.

Install (with the @cruiseui registry in components.json, see Installation):

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

Import: `import { CruiseFileUpload } from "@/components/cruise/file-upload";`

## When to use

Photos, receipts and documents in a form.

## When not to

Pasting a link to a file (use an Input).

## Example (React)

```tsx
"use client";

import { CruiseFileUpload } from "@/components/cruise/file-upload";

/** A pretend upload: reports progress, then finishes. Use your own (S3, Cloudinary…). */
async function fakeUpload(file: File, report: (percent: number) => void) {
  for (let p = 0; p <= 100; p += 20) {
    report(p);
    await new Promise((r) => setTimeout(r, 250));
  }
}

export default function FileUploadExample() {
  return (
    <div className="grid gap-6 md:grid-cols-2">
      <CruiseFileUpload
        label="Product photos"
        hint="Up to 3 photos."
        accept="image/*"
        maxSize={5 * 1024 * 1024}
        multiple
        maxFiles={3}
        uploader={fakeUpload}
      />
      <CruiseFileUpload label="Receipt" accept="image/*,.pdf" maxSize={2 * 1024 * 1024} />
    </div>
  );
}
```

## HTML

Needs theme.css (https://cruiseui.dev/theme.css).

```html
<div class="field">
  <label class="field-label" for="photos">Product photos</label>
  <label class="upload-zone">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 13v8M8 17l4-4 4 4"/><path d="M20.4 14.5A5 5 0 0 0 17 6h-1.3A8 8 0 1 0 4 15.3"/></svg>
    <span class="upload-title">Drop files here or browse</span>
    <span class="upload-hint">Images · max 5MB · up to 3 files</span>
    <input class="visually-hidden" type="file" id="photos" accept="image/*" multiple>
  </label>
  <ul class="upload-list" aria-label="Chosen files">
    <li class="upload-item">
      <span class="upload-thumb" aria-hidden="true"><img src="blob:…" alt=""></span>
      <span class="upload-meta"><span class="upload-name">cake.jpg</span><span class="upload-size">1.2MB</span></span>
      <button type="button" class="upload-remove" aria-label="Remove cake.jpg"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </li>
  </ul>
</div>
```

## Props: CruiseFileUpload

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `string` | — | Visible label. Leave out inside a CruiseFormField (it brings the label). |
| `hint?` | `string` | — | — |
| `error?` | `string` | — | Your own error; size, type and count errors are added for you. |
| `required?` | `boolean` | — | — |
| `accept?` | `string` | — | e.g. "image/*,.pdf" |
| `maxSize?` | `number` | — | In bytes, e.g. 5 * 1024 * 1024. |
| `multiple?` | `boolean` | `false` | — |
| `maxFiles?` | `number` | — | — |
| `disabled?` | `boolean` | — | — |
| `name?` | `string` | — | — |
| `id?` | `string` | — | — |
| `onFilesChange?` | `((files: File[]) => void)` | — | Called with the accepted files whenever the list changes. |
| `uploader?` | `((file: File, report: (percent: number) => void) => Promise<void>)` | — | Uploads one file; call report(0-100) as it goes. Throw to fail it. |
| `className?` | `string` | — | — |

## Accessibility

- The zone is a <label> for a real file input: click, Enter or Space opens the picker.
- Size, type and count errors show in words through the Form field.
- Each file has a "Remove photo.jpg" button and a labelled progress bar.
