<!-- source-hash: 7a3018246d0e272d26eee450458f6d52 -->
A client-side React component that renders a drag-and-drop / click-to-upload image input with preview, replace, and remove functionality. Wraps a hidden `<input type="file">` with validation, loading states, and accessible keyboard interaction.

## Key Components

- **`ImageUploader`** — Main export. Renders the dropzone or image preview depending on whether `value` is set.
- **`ActionIconButton`** — Internal button used for the "Replace" and "Remove" overlay actions on the image preview.
- **`formatSize(bytes)`** — Utility that formats a byte count as a human-readable `KB`/`MB` string.
- **`matchesAccept(file, accept)`** — Validates a `File` against an `accept` string (MIME types and extensions).

## Key Props (`ImageUploaderProps`)

| Prop | Type | Description |
|------|------|-------------|
| `value` | `string` | Current image URL; triggers preview state when set |
| `onChange` | `(file: File) => void` | Called with the selected/dropped `File` after validation |
| `onRemove` | `() => void` | Shows trash icon when provided; called on click |
| `accept` | `string` | Accepted MIME types/extensions. Default: `"image/*"` |
| `maxSize` | `number` | Max file size in bytes. Default: 25 MB |
| `aspectRatio` | `string` | CSS aspect-ratio override (e.g. `"16 / 9"`) |
| `loading` | `boolean` | Shows spinner overlay and disables interaction |
| `error` | `string` | External error message (internal validation errors take precedence) |

## Usage Example

```typescript
import { ImageUploader } from "@/components/ui/image-uploader"
import { useState } from "react"

export function CoverImageField() {
  const [imageUrl, setImageUrl] = useState<string | undefined>()

  const handleChange = async (file: File) => {
    const url = await uploadToStorage(file) // your upload logic
    setImageUrl(url)
  }

  return (
    <ImageUploader
      value={imageUrl}
      onChange={handleChange}
      onRemove={() => setImageUrl(undefined)}
      aspectRatio="16 / 9"
      maxSize={5 * 1024 * 1024} // 5 MB
      fieldLabel="Cover Image"
      label="Upload a cover photo"
      description="Click or drag and drop"
    />
  )
}
```

> **Note:** `onChange` receives the raw `File` — the consumer is responsible for uploading it to a server or object storage and updating `value` with the resulting URL.