# File Upload

> File selection and drag and drop with validation, progress, retry, and removal.

- Category: Forms
- Kind: component
- Page: https://uaiblocks.dev/components/file-upload
- Registry item: https://uaiblocks.dev/r/file-upload.json
- Source: https://github.com/victor-teles/uai/blob/main/src/registry/uai/components/file-upload.tsx

## Install

```bash
bunx shadcn@latest add https://uaiblocks.dev/r/file-upload.json
```

## Usage

```tsx
"use client";

import { useEffect, useState } from "react";
import {
  FileUpload,
  FileUploadDropzone,
  FileUploadInput,
  FileUploadItem,
  FileUploadList,
  FileUploadProgress,
  FileUploadRemove,
  FileUploadRetry,
  FileUploadTrigger,
  type FileUploadVariant,
} from "@/components/ui/uai/file-upload";

type DemoFile = {
  id: string;
  name: string;
  progress: number;
  status: "uploading" | "complete" | "error";
};
export function FileUploadPreview({ variant = "dropzone" }: { variant?: FileUploadVariant }) {
  const [files, setFiles] = useState<DemoFile[]>([
    { id: "brief", name: "Q3 launch brief.pdf", progress: 100, status: "complete" },
    { id: "deck", name: "pricing-review-deck.pdf", progress: 0, status: "error" },
  ]);
  const uploading = files.some((file) => file.status === "uploading");
  useEffect(() => {
    if (!uploading) return;
    const timer = setInterval(
      () =>
        setFiles((current) =>
          current.map((file) =>
            file.status === "uploading"
              ? {
                  ...file,
                  progress: Math.min(100, file.progress + 20),
                  status: file.progress >= 80 ? "complete" : "uploading",
                }
              : file,
          ),
        ),
      350,
    );
    return () => clearInterval(timer);
  }, [uploading]);
  return (
    <FileUpload
      variant={variant}
      accept=".pdf,image/*"
      maxSize={5 * 1024 * 1024}
      maxFiles={5}
      fileCount={files.length}
      onFilesAccepted={(accepted) =>
        setFiles((current) => [
          ...current,
          ...accepted.map((file) => ({
            id: crypto.randomUUID(),
            name: file.name,
            progress: 0,
            status: "uploading" as const,
          })),
        ])
      }
    >
      <FileUploadDropzone>
        <div
          style={{
            textAlign: variant === "dropzone" ? "center" : "left",
            flex: variant === "dropzone" ? undefined : 1,
          }}
        >
          <strong style={{ fontWeight: 500 }}>Add project files</strong>
          <p style={{ margin: "2px 0 0", fontSize: 12, color: "var(--uai-subtle)" }}>
            Drop PDFs or images · up to 5 MB each · 5 files
          </p>
        </div>
        <FileUploadInput />
        <FileUploadTrigger />
      </FileUploadDropzone>
      <FileUploadList>
        {files.map((file) => (
          <FileUploadItem key={file.id} status={file.status} progress={file.progress}>
            <strong style={{ fontWeight: 500 }}>{file.name}</strong>
            <FileUploadProgress aria-label={`Uploading ${file.name}`} />
            <div style={{ display: "flex", gap: 4 }}>
              <FileUploadRetry
                onClick={() =>
                  setFiles((current) =>
                    current.map((item) =>
                      item.id === file.id ? { ...item, progress: 0, status: "uploading" } : item,
                    ),
                  )
                }
              />
              <FileUploadRemove
                aria-label={`Remove file ${file.name}`}
                onClick={() => setFiles((current) => current.filter((item) => item.id !== file.id))}
              />
            </div>
          </FileUploadItem>
        ))}
      </FileUploadList>
      <p style={{ margin: 0, fontSize: 11.5, color: "var(--uai-subtle)" }}>
        Local demo: progress is simulated. Files are not sent to a server.
      </p>
    </FileUpload>
  );
}
```

## Anatomy

- `FileUpload`
  - `FileUploadDropzone`
    - `FileUploadInput`
    - `FileUploadTrigger`
  - `FileUploadList`
    - `FileUploadItem`
      - `FileUploadProgress`
        - `FileUploadRetry`
        - `FileUploadRemove`

## Accessibility

- Choose files is the keyboard alternative to dropping files. Both routes apply accept, maxSize, and count limits.
- Upload progress is a progressbar that exposes its value; provide a file-specific accessible label.
- The consumer owns upload requests, cancellation, progress, retry, removal, and server validation. fileCount includes existing files. Client checks are usability feedback, not a security boundary.
