## Drop Zone (S—017)

A file upload zone with live drag feedback, validation, per-file progress, cancel, retry and paste support.

- URL: https://sundaeui.dev/components/drop-zone
- Category: Forms & inputs · Art direction: Refined
- Access: Sundae Pro (authenticated install)
- Stack: React 18+, TypeScript, plain CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/drop-zone.json`
- Tags: upload, file, form, dropzone, progress, react, drag-and-drop

The zone reacts while a file is still being dragged across the window. The dashed frame turns accent-coloured and marches, the icon lifts, and the label changes to “Release to upload”. The frame is an SVG rect, so the dashes stay even at any size.
Each file gets a row with a type glyph, a progress bar fed by your own upload function, and the actions that make sense for its state: cancel while uploading, retry after an error or cancel, and remove when done. Type and size limits are checked before anything is uploaded, and rejections are explained in plain words.
You bring the transport. onUpload receives the file, a progress callback and an AbortSignal, so it works with fetch, XHR, presigned S3 URLs, or anything else that can report progress.

### Usage

```tsx
import { DropZone, type UploadFn } from '@/components/sundae/drop-zone'

const upload: UploadFn = async (file, onProgress, signal) => {
  const url = await getPresignedUrl(file)
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.upload.onprogress = (e) => onProgress(e.loaded / e.total)
    xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error('Upload failed')))
    xhr.onerror = () => reject(new Error('Network error'))
    signal.addEventListener('abort', () => xhr.abort())
    xhr.open('PUT', url)
    xhr.send(file)
  })
}

<DropZone onUpload={upload} accept={['image/*', '.pdf']} maxSize={20 * 1024 * 1024} />
```

### Props

- `onUpload` (`(file, onProgress, signal) => Promise<void>`, required): Your upload. Report progress from 0 to 1, and stop when the signal aborts.
- `accept` (`string[]`): MIME types or extensions, such as image/* or .pdf.
- `maxSize` (`number`, default `25 MB`): Maximum bytes per file.
- `maxFiles` (`number`, default `10`): Maximum number of files.
- `label` (`string`, default `'Drop files here'`): Main label.
- `hint` (`ReactNode`): Replaces the default hint line.
- `allowPaste` (`boolean`, default `true`): Accept files pasted with ⌘V or Ctrl+V.
- `onChange` (`(files) => void`): Receives the current files and their statuses.

### Accessibility

- The zone is a real button that opens the native file picker, so keyboard and screen-reader users never need to drag.
- Each row has a labelled progressbar, and the action buttons are labelled with the file name.
- Rejections are announced through role="alert".
