# File Dropzone

File Dropzone [#file-dropzone]

The **File Dropzone** is a generic, format-agnostic drag-and-drop + click-to-browse upload zone built for non-image files — PDFs, CSV/JSON vector exports, audio, and the like. It validates every file with the copy-owned `validateFile()` helper against an `accept` MIME list and a `maxSize`, emitting only the valid files via `onUpload(files)`; rejected files go to `onReject`. The helper is installed automatically as a registry dependency, so the dropzone is portable — no `@localmode/react` requirement.

It is intentionally distinct from media-vision's `MediaDropzone`: there are **no image-preview semantics** here (no thumbnails). Two focused primitives beat one prop-bloated dropzone, and both reuse `validateFile`.

**When to use it:** ingest documents into a RAG pipeline (pdf-search), import vector exports (data-migrator), or accept meeting recordings (meeting-assistant) — anywhere you take non-image files and need accept-list + size validation with a processing overlay.

Preview [#preview]

```tsx
'use client';

import * as React from 'react';
import { FileDropzone, type RejectedFile } from '@/components/file-dropzone';

/**
 * Demo for the FileDropzone component, used by the docs live preview.
 * Accepts PDF/CSV/JSON up to 10MB. Shows accepted + rejected files and a
 * "processing" toggle so the disabled overlay is visible. No model download —
 * validation runs entirely in the browser.
 */
export default function FileDropzoneDemo() {
  const [accepted, setAccepted] = React.useState<File[]>([]);
  const [rejected, setRejected] = React.useState<RejectedFile[]>([]);
  const [processing, setProcessing] = React.useState(false);

  return (
    <div className="w-full max-w-md space-y-3">
      <FileDropzone
        accept={['application/pdf', 'text/csv', 'application/json']}
        maxSize={10_000_000}
        processing={processing}
        onUpload={(files) => {
          setRejected([]);
          setAccepted(files);
        }}
        onReject={setRejected}
      />

      <label className="flex items-center gap-2 text-xs text-muted-foreground">
        <input
          type="checkbox"
          checked={processing}
          onChange={(e) => setProcessing(e.target.checked)}
        />
        Simulate processing overlay
      </label>

      {accepted.length > 0 && (
        <ul className="space-y-1 text-xs text-foreground">
          {accepted.map((f) => (
            <li key={f.name}>
              ✓ {f.name} ({(f.size / 1000).toFixed(0)}KB)
            </li>
          ))}
        </ul>
      )}

      {rejected.length > 0 && (
        <ul className="space-y-1 text-xs text-destructive">
          {rejected.map((r) => (
            <li key={r.file.name}>✕ {r.reason}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/data-documents/file-dropzone
```

Dependencies [#dependencies]

* **Data source:** validates and emits the `File[]` you drop — works in any React app; no backend or model required.
* `@localmode/ui/lib/browser-utils` — the copy-owned `validateFile()` helper (installed automatically as a registry dependency)
* `lucide-react` — the upload / spinner icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `file-dropzone.tsx` — the component
* `lib/browser-utils.ts` — the copy-owned file helpers (if not already present)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**FileDropzone**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onUpload` | `function` | — | **Required.** Called with the files that passed validation (accept-list + max-size). Only valid files are included. |
| `onReject` | `function` | — | Called when one or more dropped/selected files fail validation. Optional — use it to surface per-file errors. |
| `accept` | `array` | — | Accepted MIME types, e.g. `['application/pdf', 'text/csv', 'application/json']`. Passed to the native input's `accept` attribute (as a comma-joined list) and enforced by `validateFile`. When omitted, all types are accepted. |
| `maxSize` | `number` | — | Maximum file size in bytes. Files larger than this are rejected. |
| `multiple` | `boolean` | `true` | Allow selecting more than one file at a time. |
| `disabled` | `boolean` | `false` | Disable the zone (blocks drag and click). Combine with `processing` for an "uploading…" state. |
| `processing` | `boolean` | `false` | Show the processing overlay and block input. Use while files are being indexed/parsed. |
| `processingLabel` | `string` | `"Processing…"` | Message shown in the processing overlay. |
| `label` | `string` | `"Drop files or click to browse"` | Primary call-to-action text. |
| `hint` | `string` | — | Secondary hint line. Defaults to a human-readable summary of `accept` and `maxSize`. |

Examples [#examples]

Accept PDF / CSV / JSON [#accept-pdf--csv--json]

```tsx
import { FileDropzone } from '@/components/file-dropzone';

export function Ingest() {
  return (
    <FileDropzone
      accept={['application/pdf', 'text/csv', 'application/json']}
      maxSize={10_000_000}
      onUpload={(files) => ingest(files)}
      onReject={(rejected) => rejected.forEach((r) => console.warn(r.reason))}
    />
  );
}
```

Processing overlay [#processing-overlay]

```tsx
const [busy, setBusy] = useState(false);

<FileDropzone
  accept={['application/pdf']}
  processing={busy}
  processingLabel="Indexing…"
  onUpload={async (files) => {
    setBusy(true);
    await indexAll(files);
    setBusy(false);
  }}
/>
```

Single-file, custom hint [#single-file-custom-hint]

```tsx
<FileDropzone
  accept={['application/json']}
  multiple={false}
  label="Drop a vector export"
  hint="One .json file, up to 50MB"
  maxSize={50_000_000}
  onUpload={([file]) => importExport(file)}
/>
```

Customization [#customization]

The zone is styled with shadcn/ui CSS-variable utilities (`border-border`, `bg-card`, `bg-accent`, `text-muted-foreground`, `bg-primary/5`), so the dashed border, hover highlight, drag state, and overlay all inherit your theme. Because you own the file, you can swap the `UploadCloud` icon, change the `min-h-40` footprint, or extend the validation (e.g. dedupe by name) in the copied `file-dropzone.tsx`.

The `hint` line defaults to a human-readable summary derived from `accept` (extensions) and `maxSize`; pass an explicit `hint` to override it.