# DropZone

**When to use:** Drag-and-drop file upload area, optionally click-to-open the native picker. Filters by type / size / count, reads files as raw `File`, base64, or text, and reports rejections. Stateless — it hands accepted files to a callback; you own the file state.

**Keywords:** drag and drop upload

**Import:** `import { DropZone, DropZoneReturnType, DropZoneStatus } from '@ahrowe/ui'`
**Types:** `import type { DropZoneProps, LoadedFile, DropZoneRejection, DropZoneRenderState } from '@ahrowe/ui'`

**Enums:**

- `DropZoneReturnType`: `File` (default) | `Base64` | `Text`
- `DropZoneStatus`: `Idle` | `Dragging` (drag anywhere in the window) | `Active` (drag over this zone) | `Error`

```tsx
import { useState } from 'react';
import { DropZone, DropZoneReturnType } from '@ahrowe/ui';
import type { LoadedFile, DropZoneRejection } from '@ahrowe/ui';

// Basic drag & drop
const [files, setFiles] = useState<(File | LoadedFile)[]>([]);
<DropZone
  acceptedTypes={['.png', '.jpg', 'application/pdf']}
  showFileNames
  files={files}
  onFilesChanged={setFiles}
>
  Drop images or PDFs here
</DropZone>

// Click to open the native picker
<DropZone openPickerOnClick acceptedTypes={['image/*']} onFilesChanged={setFiles}>
  Click or drop an image
</DropZone>

// Render-prop child reacting to the live drag state
<DropZone unstyled openPickerOnClick acceptedTypes={['image/*']}>
  {({ isActive }) => (
    <div className={isActive ? 'zone-active' : 'zone'}>
      {isActive ? 'Release to upload' : 'Drag a file or click'}
    </div>
  )}
</DropZone>

// Limits + rejection reporting
<DropZone
  acceptedTypes={['image/*']}
  maxFiles={2}
  maxSize={5 * 1024 * 1024}      // 5 MB
  onFilesChanged={setFiles}
  onReject={(rejections: DropZoneRejection[]) =>
    rejections.forEach((r) => console.warn(r.file.name, r.reason))
  }
/>

// Read as base64 (e.g. for an inline preview)
<DropZone
  multiple={false}
  returnType={DropZoneReturnType.Base64}
  acceptedTypes={['image/*']}
  onFilesChanged={(loaded) => {
    const file = loaded[0] as LoadedFile;
    setPreview(file.data); // data URL
  }}
/>
```

**Accepted types:** `acceptedTypes` accepts extensions (`'.png'`), exact mime types (`'image/png'`), and wildcards (`'image/*'`). Matching is case-insensitive. The same list drives the native picker's `accept` attribute. Omit / empty = accept everything.

**`onFilesChanged` payload:** raw `File[]` for `returnType=File`; `LoadedFile[]` (`{ name, size, type, lastModified, data }`) for `Base64` / `Text`. Files are always emitted in input order.

**DropZoneProps:**

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode \| (state: DropZoneRenderState) => ReactNode` | Static content, or a render function receiving `{ status, isDragging, isActive, disabled }` |
| `acceptedTypes` | `string[]` | Extensions / mime types / wildcards (default: accept all) |
| `returnType` | `DropZoneReturnType` | `File` (default), `Base64`, or `Text` |
| `openPickerOnClick` | `boolean` | Open the native file picker on click, and make the zone a focus stop that opens it on Enter or Space |
| `multiple` | `boolean` | Allow multiple files (default `true`) |
| `maxFiles` | `number` | Reject files beyond this count in one drop or pick (`tooMany`). It does not count files from earlier drops: those are yours |
| `maxSize` | `number` | Reject files larger than this many bytes |
| `enablePaste` | `boolean` | Accept clipboard-pasted files when focused |
| `allowDirectories` | `boolean` | Recurse into dropped folders (default `true`) |
| `capture` | `boolean \| 'user' \| 'environment'` | Native mobile capture attribute |
| `disabled` | `boolean` | Disable drops, picker, and paste |
| `unstyled` | `boolean` | Drop the built-in border/background; keep base layout only |
| `showFileNames` | `boolean` | Render the names of `files` below the content |
| `files` | `(File \| LoadedFile)[]` | Files whose names are listed when `showFileNames` is set |
| `onFilesChanged` | `(files: (File \| LoadedFile)[]) => void` | The files accepted from this drop or pick, in input order, not every file so far: add them to a list of your own to keep earlier ones. With `returnType` `File` (the default) each one is a `File`, which `instanceof File` narrows to. Picking the same file again calls it again |
| `onReject` | `(rejections: DropZoneRejection[]) => void` | Rejected files with a `reason` (`unacceptedType` / `tooLarge` / `tooMany` / `readError`) |
| `onStatusChanged` | `(status: DropZoneStatus) => void` | Status transitions |

**Slots:** `root` `content` `fileList` `fileItem` `input`
