# File Upload

> Drag-and-drop file picker with `accept` / `maxSize` / `maxFiles` validation, per-file progress and status, optional folder upload, and an async-cancellable remove hook.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://www.sisyphosui.com/docs/components/file-upload

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { FileUpload } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

```tsx
import { FileUpload } from "@sisyphos-ui/react";

export const Avatar = () => (
  <FileUpload
    label="Profile photo"
    accept="image/*"
    maxSize={2 * 1024 * 1024}
    onChange={(files) => console.log(files)}
  />
);
```

### Vue 3+

```vue
<script setup lang="ts">
import { FileUpload } from "@sisyphos-ui/vue";
</script>

<template>
  <FileUpload
    label="Profile photo"
    accept="image/*"
    :maxSize="2 * 1024 * 1024"
    @change="(files) => console.log(files)"
  />
</template>
```

### Angular 17+

```ts
import { Component } from "@angular/core";
import { FileUpload } from "@sisyphos-ui/angular";

@Component({
  selector: "app-avatar",
  standalone: true,
  imports: [FileUpload],
  template: `
    <sui-file-upload
      label="Profile photo"
      accept="image/*"
      [maxSize]="2 * 1024 * 1024"
      (filesChange)="onFiles($event)"
    />
  `,
})
export class AvatarComponent {
  onFiles(files: unknown) { console.log(files); }
}
```

## Examples

### Default

Always controlled — parent owns the list of files via `value`/`onChange`.

```tsx
import { useState } from "react";
import { FileUpload, type UploadedFile } from "@sisyphos-ui/react";

export function Example() {
  const [files, setFiles] = useState<UploadedFile[]>([]);

  return (
    <div className="w-full max-w-md">
      <FileUpload
        value={files}
        onChange={setFiles}
        accept="image/*"
        multiple
        maxFiles={3}
        supportedFormats={["PNG", "JPG", "WEBP"]}
      />
    </div>
  );
}
```

### Max size with onReject

`maxSize` enforces a byte limit; `onReject` receives the rejection reason so you can show a friendly message.

```tsx
import { useState } from "react";
import { FileUpload, type UploadedFile } from "@sisyphos-ui/react";

export function Example() {
  const [files, setFiles] = useState<UploadedFile[]>([]);
  const [message, setMessage] = useState<string | null>(null);

  return (
    <>
      <FileUpload
        label="Attach logo"
        value={files}
        onChange={setFiles}
        accept="image/*"
        maxSize={2 * 1024 * 1024}
        onReject={(file, reason) => {
          if (reason.kind === "size") {
            setMessage(`"${file.name}" is too large.`);
          }
        }}
      />
      {message && <p>{message}</p>}
    </>
  );
}
```

### Error state

Pair `error` with external error messaging for form validation.

```tsx
import { useState } from "react";
import { FileUpload, type UploadedFile } from "@sisyphos-ui/react";

export function Example() {
  const [files, setFiles] = useState<UploadedFile[]>([]);

  return (
    <>
      <FileUpload label="CV" value={files} onChange={setFiles} error />
      <p>Please attach your CV to continue.</p>
    </>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| **value** (required) | `UploadedFile[]` | — | Controlled list of files, including pre-uploaded items (use `url` instead of `file`). |
| **onChange** (required) | `(files: UploadedFile[]) => void` | — | Called with the next file array after add or remove. |
| label | `string` | — | Field label rendered above the dropzone. |
| accept | `string` | — | MIME types or extensions accepted by the native input. |
| maxSize | `number` | `10 * 1024 * 1024` | Maximum byte size per file. |
| maxFiles | `number` | `1` | Maximum file count. With `1`, new uploads replace the existing entry. |
| multiple | `boolean` | — | Allow multi-select on the native input. Defaults to `maxFiles > 1`. |
| directory | `boolean` | `false` | Accepts an entire folder via `webkitdirectory`. Each picked file's `webkitRelativePath` is preserved. |
| supportedFormats | `string[]` | — | Human-readable formats shown below the dropzone. |
| onReject | `(file: File, reason: RejectReason) => void` | — | Called when a file fails type / size / count validation. |
| onBeforeRemove | `(file: UploadedFile) => boolean \| Promise<boolean>` | — | Called before a file is removed. Returning `false` cancels the removal — useful for confirmations or revoking server-side resources first. |
| renderFile | `(file, handlers) => ReactNode` | — | Custom renderer for each file row. |
| labels | `FileUploadLabels` | — | i18n strings for placeholder, browse button, completed/uploading badges, and remove tooltip. |
| disabled | `boolean` | `false` | Disables the dropzone. |
| error | `boolean` | `false` | Marks the field as invalid for styling and ARIA. |
| errorMessage | `string` | — | Message shown below the dropzone when `error` is true. |

## Keyboard interactions

- **Tab** — Moves focus to the dropzone, then to each per-file remove button.
- **Enter + Space** — Activates the focused control (open browser dialog, remove file).

## Accessibility notes

- Native `<input type="file">` is hidden but accessible via the surrounding label so screen readers announce the field correctly.
- Each file row exposes a per-row remove button labeled via `labels.remove`.
- Drag state is reflected as a class on the dropzone for users with motion-aware themes.

<!-- exports: { "FileUpload": "@sisyphos-ui/react" } -->