# FileDropzone

Drop-or-browse file picker with native HTML5 drag-and-drop, click-to-select fallback, and built-in `accept` / `maxSize` / `maxFiles` validation. Always controlled — the parent owns the file list. RTL-safe via logical Tailwind utilities.

[View examples](https://marsam.rahhal.app/components/file-dropzone) · [Agent index](https://marsam.rahhal.app/llms.txt)

**Import:** `@rahhal/ui/file-dropzone`

**Category:** `forms`

## Behavior

- The whole surface is a focusable `<button>` that opens the hidden `<input type="file">`. Drop, click, and Enter / Space all work.
- `accept`, `maxSize`, and `maxFiles` are enforced **before** files reach `onChange`. Rejected files are surfaced via `onReject` so the caller can render a toast or inline error.
- Always controlled — pass `value` (`File[]`) and `onChange`. The component never holds its own file state because real upload flows always need the parent to track progress and submission.
- Removing a file via the row's × button calls `onChange` with the file removed.
- Layout uses logical utilities (`text-start`, `me-*`) so the file list mirrors correctly under `[dir="rtl"]`.

## Props

| prop | type | purpose |
| --- | --- | --- |
| value | File[] | controlled file list (required) |
| onChange | (next: File[]) => void | fires on drop / pick / remove (required) |
| accept | string | native accept pattern, e.g. "image/*,.pdf" |
| multiple | boolean | default true — set false for single-file pickers |
| maxFiles | number | hard cap on total accepted files |
| maxSize | number | per-file size cap in bytes |
| disabled | boolean | disables drop / browse / remove |
| onReject | (rejections) => void | fired when files fail accept / size / max-files |
| title / description | ReactNode | override the empty-state copy |
| icon | ReactNode | override the cloud-upload glyph |
| id / name | string | wire up `<Label htmlFor>` and form posting |

## Helpers

- `formatFileSize(bytes)` — pretty-prints byte counts (`1.4 MB`).
- `type FileRejection = { file: File; reason: "type" | "size" | "max-files" }`.

## Patterns

- For images, set `accept="image/*"` and validate dimensions in your own `onChange`.
- For single-file flows, set `multiple={false}`. Re-picking replaces the prior file rather than appending.
- The component does **not** upload — wire `onChange` into your existing upload pipeline (Supabase Storage, S3 presigned URL, fetch, …).
