# File Upload

## Overview

A drag-and-drop file input component that accepts one or multiple files. Renders a dropzone area with visual feedback on drag, displays selected file names, and validates against allowed MIME types and file size limits.

The package also exports a **headless `useFileUpload` hook** for building fully custom drop-zone UIs while reusing all the validation and drag-state logic.

---

## Exports

| Export                | Description                                     |
| --------------------- | ----------------------------------------------- |
| `FileUpload`          | Ready-to-use drag-and-drop file input component |
| `useFileUpload`       | Headless hook — all logic, no UI                |
| `UseFileUploadProps`  | TypeScript props type for the hook              |
| `UseFileUploadReturn` | TypeScript return type for the hook             |

---

## When to Use

- Document upload forms
- Avatar/profile image replacement
- Data import (CSV, JSON uploads)
- Any file selection where UX is important

---

## `FileUpload` Component Props

| Prop                 | Type                                                     | Default                                                              | Required | Description                                                             |
| -------------------- | --------------------------------------------------------- | --------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------ |
| `accept`             | `string`                                                   | `'*'`                                                                  | No       | Accepted MIME types (e.g., `'image/*,.pdf'`)                             |
| `maxFiles`           | `number`                                                   | `1`                                                                    | No       | Maximum number of files allowed                                          |
| `maxSize`            | `number`                                                   | `5242880` (5MB)                                                        | No       | Maximum file size in bytes                                               |
| `showPreview`        | `boolean`                                                  | `true`                                                                 | No       | Show the selected-file preview list                                      |
| `size`               | `'default' \| 'compact'`                                   | `'default'`                                                            | No       | `compact` uses a horizontal layout with smaller icons and tighter spacing |
| `onFilesChange`      | `(files: File[]) => void`                                  | —                                                                       | No       | Called when selection changes                                           |
| `onError`            | `(rejected: File[], reason: 'size' \| 'count') => void`    | —                                                                       | No       | Called when files are rejected                                          |
| `disabled`           | `boolean`                                                  | `false`                                                                | No       | Disables the dropzone                                                    |
| `className`          | `string`                                                   | —                                                                       | No       | Additional CSS classes                                                   |
| `uploadLabel`        | `string`                                                   | `'Click to upload'`                                                    | No       | Label for the clickable upload action                                   |
| `dragAndDropLabel`   | `string`                                                   | `'or drag and drop'`                                                   | No       | Drag-and-drop hint, shown after `uploadLabel`                            |
| `multipleFilesLabel` | `string`                                                   | `'Up to {count} files'`                                                | No       | File-count hint when `maxFiles > 1`; `{count}` is replaced with `maxFiles` |
| `singleFileLabel`    | `string`                                                   | `'1 file'`                                                             | No       | File-count hint when `maxFiles` is 1                                     |
| `maxSizeLabel`       | `string`                                                   | `'Max {size}MB'`                                                       | No       | Max-size hint; `{size}` is replaced with the limit in MB                 |
| `sizeErrorLabel`     | `string`                                                   | `'{count} file(s) exceed the {size}MB limit and were not added.'`      | No       | Error shown when files exceed `maxSize`; `{count}`/`{size}` are substituted |
| `countErrorLabel`    | `string`                                                   | `'Only {max} file(s) allowed. {count} file(s) were not added.'`       | No       | Error shown when more files are picked than `maxFiles`; `{max}`/`{count}` are substituted |

---

## Examples

### Image Upload

```tsx
import { FileUpload } from 'xertica-ui/ui';

<FileUpload
  accept="image/*"
  maxSize={5 * 1024 * 1024} // 5MB
  onFilesChange={files => handleAvatarUpload(files[0])}
/>;
```

### Multiple Document Upload

```tsx
<FileUpload
  accept=".pdf,.doc,.docx"
  multiple
  onFilesChange={files => setSelectedDocuments(files)}
/>
```

### Inside react-hook-form

```tsx
<FormField
  control={form.control}
  name="attachment"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Attachment</FormLabel>
      <FormControl>
        <FileUpload accept=".pdf" onFilesChange={files => field.onChange(files[0])} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>
```

---

## `useFileUpload` Hook

A headless hook that encapsulates all file upload logic — drag state, file validation, error messaging, and the hidden input ref — without rendering any UI. Use it when the `<FileUpload>` component's visual design doesn't fit your needs.

### Props

| Prop            | Type                                                    | Default         | Description                                            |
| --------------- | ------------------------------------------------------- | --------------- | ------------------------------------------------------ |
| `maxFiles`      | `number`                                                | `1`             | Maximum number of files allowed                        |
| `maxSize`       | `number`                                                | `5242880` (5MB) | Maximum file size in bytes                             |
| `onFilesChange` | `(files: File[]) => void`                               | —               | Called with the accepted file list whenever it changes |
| `onError`       | `(rejected: File[], reason: 'size' \| 'count') => void` | —               | Called when files are rejected                         |
| `disabled`      | `boolean`                                               | `false`         | Whether the upload area is disabled                    |
| `sizeErrorLabel`  | `string`                                              | `'{count} file(s) exceed the {size}MB limit and were not added.'` | Error message when files exceed `maxSize`; `{count}`/`{size}` are substituted |
| `countErrorLabel` | `string`                                              | `'Only {max} file(s) allowed. {count} file(s) were not added.'`  | Error message when more files are picked than `maxFiles`; `{max}`/`{count}` are substituted |

### Return Value

| Property         | Type                                         | Description                                          |
| ---------------- | -------------------------------------------- | ---------------------------------------------------- |
| `files`          | `File[]`                                     | Currently accepted files                             |
| `dragActive`     | `boolean`                                    | Whether a drag is active over the drop zone          |
| `errorMessage`   | `string \| null`                             | Inline error message, or `null`                      |
| `inputRef`       | `RefObject<HTMLInputElement>`                | Attach to the hidden `<input type="file">`           |
| `handleFiles`    | `(files: FileList \| null) => void`          | Process a `FileList` from any source                 |
| `handleDrag`     | `(e: DragEvent) => void`                     | Attach to `onDragEnter`, `onDragOver`, `onDragLeave` |
| `handleDrop`     | `(e: DragEvent) => void`                     | Attach to `onDrop`                                   |
| `handleChange`   | `(e: ChangeEvent<HTMLInputElement>) => void` | Attach to the hidden input's `onChange`              |
| `removeFile`     | `(index: number) => void`                    | Remove a file by its index                           |
| `openFileDialog` | `() => void`                                 | Programmatically open the native file picker         |

### Custom Drop Zone Example

```tsx
import { useFileUpload } from 'xertica-ui/ui';
import { UploadCloud } from 'lucide-react';

function CustomDropZone() {
  const {
    files,
    dragActive,
    errorMessage,
    inputRef,
    handleDrag,
    handleDrop,
    handleChange,
    removeFile,
    openFileDialog,
  } = useFileUpload({
    maxFiles: 3,
    maxSize: 10 * 1024 * 1024, // 10MB
    onFilesChange: files => console.log('Files:', files),
  });

  return (
    <div className="space-y-3">
      <div
        onDragEnter={handleDrag}
        onDragOver={handleDrag}
        onDragLeave={handleDrag}
        onDrop={handleDrop}
        onClick={openFileDialog}
        className={cn(
          'flex flex-col items-center justify-center rounded-xl border-2 border-dashed p-10 cursor-pointer transition-colors',
          dragActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'
        )}
      >
        <UploadCloud className="size-8 text-muted-foreground mb-2" />
        <p className="text-sm text-muted-foreground">
          Drop files here or <span className="text-primary font-medium">browse</span>
        </p>
        <input ref={inputRef} type="file" multiple className="hidden" onChange={handleChange} />
      </div>

      {errorMessage && <p className="text-sm text-destructive">{errorMessage}</p>}

      {files.length > 0 && (
        <ul className="space-y-1">
          {files.map((file, i) => (
            <li key={i} className="flex items-center justify-between text-sm">
              <span>{file.name}</span>
              <Button size="sm" variant="ghost" onClick={() => removeFile(i)}>
                Remove
              </Button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

---

## AI Rules

- Do not use native `<input type="file">` — always use `<FileUpload>` or `useFileUpload` from `xertica-ui`.
- `maxSize` is in bytes — calculate using `MB * 1024 * 1024`.
- In forms, use `onFilesChange` to call `field.onChange` — not `{...field}`.
- For avatar uploads, use `accept="image/*"` and `multiple={false}`.
- When using `useFileUpload`, attach `handleDrag` to **all three** drag events: `onDragEnter`, `onDragOver`, and `onDragLeave`.
- Always attach `inputRef` to a hidden `<input type="file">` element — `openFileDialog()` requires it.
- `removeFile(index)` removes by array index, not by file name.
- Use `size="compact"` for dense forms/sidebars instead of building a custom layout with `useFileUpload`.
- All UI text (`uploadLabel`, `dragAndDropLabel`, `multipleFilesLabel`, `singleFileLabel`, `maxSizeLabel`, `sizeErrorLabel`, `countErrorLabel`) is overridable — always localize these when the app isn't English-only. `multipleFilesLabel`/`maxSizeLabel`/`sizeErrorLabel`/`countErrorLabel` support `{count}`/`{size}`/`{max}` placeholders that get substituted automatically.
