# Media Dropzone

Media Dropzone [#media-dropzone]

The **Media Dropzone*&#x2A; is a drag-and-drop + click-to-browse upload zone for image/media files. It renders an idle state (icon + title + subtitle + accepted-formats hint), a drag-over state (highlight + tint + scale), and a processing state (spinner + label overlay), plus a compact &#x2A;*"add another"** variant for adding more files once some exist.

Files are validated with the copy-owned `validateFile` helper (from `@localmode/ui/lib/browser-utils`, installed automatically) against the `accept` list and `maxSize`; valid files are emitted via `onFiles(File[])` and rejected files via `onReject`. It is portable and presentational — no `@localmode/react` runtime requirement — so wire `processing` to any loading state and read the emitted files yourself.

**When to use it:** any image-AI surface — background removal, object detection, OCR, captioning, smart galleries — that takes an image from the user. It replaces the hand-rolled dropzone duplicated across many vision apps. For format-agnostic document uploads (PDF/CSV/JSON, no preview), use `FileDropzone` from the Data & Documents family instead.

Preview [#preview]

```tsx
'use client';

import * as React from 'react';
import { MediaDropzone } from '@/components/media-dropzone';

/**
 * Demo for the MediaDropzone component, used by the docs live preview.
 * Exercises the idle / drag-over / processing states and the "add another"
 * variant with no model download — selected files are listed locally.
 */
export default function MediaDropzoneDemo() {
  const [files, setFiles] = React.useState<File[]>([]);
  const [error, setError] = React.useState<string | null>(null);
  const [processing, setProcessing] = React.useState(false);

  function handleFiles(next: File[]) {
    setError(null);
    setFiles((prev) => [...prev, ...next]);
    // Simulate a short "processing" pass so the overlay is visible.
    setProcessing(true);
    setTimeout(() => setProcessing(false), 1200);
  }

  return (
    <div className="w-full max-w-md space-y-3">
      {files.length === 0 ? (
        <MediaDropzone
          accept={['image/png', 'image/jpeg', 'image/webp']}
          maxSize={5_000_000}
          processing={processing}
          onFiles={handleFiles}
          onReject={(r) => setError(r[0]?.reason ?? 'Rejected')}
        />
      ) : (
        <>
          <ul className="space-y-1 text-sm text-foreground">
            {files.map((file, i) => (
              <li
                key={`${file.name}-${i}`}
                className="flex items-center justify-between rounded-md border border-border bg-card px-3 py-2"
              >
                <span className="truncate">{file.name}</span>
                <span className="text-xs text-muted-foreground">
                  {(file.size / 1000).toFixed(0)} KB
                </span>
              </li>
            ))}
          </ul>
          <MediaDropzone
            addAnother
            accept={['image/png', 'image/jpeg', 'image/webp']}
            maxSize={5_000_000}
            processing={processing}
            onFiles={handleFiles}
            onReject={(r) => setError(r[0]?.reason ?? 'Rejected')}
          />
        </>
      )}

      {error && <p className="text-sm text-destructive">{error}</p>}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/media-vision/media-dropzone
```

Dependencies [#dependencies]

* **Data source:** validates files locally and emits them via `onFiles` / `onReject` — works with any backend; pass the emitted `File`s to whatever vision pipeline you use. Recommended LocalMode producers: any vision hook (`useDetectObjects` / `useSegmentImage` / `useCaptionImage`, …) from `@localmode/react` (optional).

* `@localmode/ui/lib/browser-utils` — the copy-owned `validateFile` helper (installed automatically as a registry dependency)

* `lucide-react` — icons

* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `media-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]

**MediaDropzone**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onFiles` | `function` | — | **Required.** Called with the files that pass the accept-list + max-size validation. Always receives only valid files. |
| `onReject` | `function` | — | Called with files that fail validation, paired with the reason. Optional — use it to surface an inline error. |
| `accept` | `array` | `["image/png","image/jpeg","image/webp","image/gif"]` | Accepted MIME types, e.g. `['image/png', 'image/jpeg', 'image/webp']`. When omitted, any file type is accepted. |
| `maxSize` | `number` | `10000000` | Maximum file size in bytes. When omitted, no size limit is enforced. |
| `multiple` | `boolean` | `true` | Allow selecting / dropping more than one file at a time. |
| `processing` | `boolean` | `false` | When true, render a spinner + label overlay (e.g. while a model processes the dropped image). The zone stops accepting input while processing. |
| `processingLabel` | `string` | `"Processing…"` | Label shown while `processing` is true. |
| `addAnother` | `boolean` | `false` | When true, render the compact "add another" variant — a short, inline tile instead of the full hero zone. Use it once images already exist. |
| `title` | `string` | `"Drop an image here"` | Title shown in the idle state. |
| `subtitle` | `string` | `"or click to browse"` | Subtitle shown in the idle state. |
| `disabled` | `boolean` | `false` | Disable all interaction. |

Backing hooks [#backing-hooks]

Turn the emitted `File`s into data URLs with the copy-owned `readFileAsDataUrl` helper (from `@localmode/ui/lib/browser-utils`, installed automatically with this component) and pass them to any vision hook (`useDetectObjects`, `useSegmentImage`, `useCaptionImage`, … from `@localmode/react`, or your own backend).

Examples [#examples]

Basic [#basic]

```tsx
import { MediaDropzone } from '@/components/media-dropzone';
import { readFileAsDataUrl } from '@/lib/browser-utils';

export function Example() {
  return (
    <MediaDropzone
      accept={['image/png', 'image/jpeg']}
      maxSize={5_000_000}
      onFiles={async (files) => {
        const dataUrl = await readFileAsDataUrl(files[0]);
        // pass dataUrl to a vision hook…
      }}
    />
  );
}
```

Add-another variant [#add-another-variant]

```tsx
<MediaDropzone addAnother onFiles={(files) => append(files)} />
```

Processing state [#processing-state]

```tsx
const { isLoading } = useDetectObjects({ model });

<MediaDropzone processing={isLoading} processingLabel="Detecting…" onFiles={handleFiles} />
```

Customization [#customization]

The zone is styled with shadcn/ui CSS-variable utilities (`border-border`, `bg-card`, `bg-primary`, `text-muted-foreground`), so it inherits your theme. Because you own the file, adjust the `accept` default, swap the `UploadCloud` / `ImagePlus` icons, or restyle the drag-over highlight directly in the copied `media-dropzone.tsx`.