# File Upload

URL: https://ark-ui.com/docs/components/file-upload
Source: https://raw.githubusercontent.com/chakra-ui/ark/refs/heads/main/website/src/content/pages/components/file-upload.mdx

A component that is used to upload multiple files.

---

<ComponentPreview id="FileUpload" />

## Anatomy

<Anatomy id="file-upload" />

```tsx
<FileUpload.Root>
  <FileUpload.Label />
  <FileUpload.Dropzone>
    <FileUpload.Trigger />
  </FileUpload.Dropzone>
  <FileUpload.ItemGroup>
    <FileUpload.Item>
      <FileUpload.ItemPreview>
        <FileUpload.ItemPreviewImage />
      </FileUpload.ItemPreview>
      <FileUpload.ItemName />
      <FileUpload.ItemSizeText />
      <FileUpload.ItemDeleteTrigger />
    </FileUpload.Item>
  </FileUpload.ItemGroup>
  <FileUpload.ClearTrigger />
  <FileUpload.HiddenInput />
</FileUpload.Root>
```

## Examples

**Example: basic**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { Paperclip, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component Basic() {
  <FileUpload.Root maxFiles={5} class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'File Upload'}</FileUpload.Label>
    <FileUpload.Trigger class={styles.Trigger}>
      <Paperclip />
      {' Choose file(s)'}
    </FileUpload.Trigger>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.ItemCompact}>
              <FileUpload.ItemName class={styles.ItemName} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Initial Files

Use the `defaultAcceptedFiles` prop to set the initial files in the file upload component.

**Example: initial-files**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { File as FileIcon, Paperclip, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component InitialFiles() {
  <FileUpload.Root
    defaultAcceptedFiles={[
      new File(['Welcome to Ark UI Ripple'], 'README.md', { type: 'text/plain' }),
    ]}
    class={styles.Root}
  >
    <FileUpload.Label class={styles.Label}>{'File Upload'}</FileUpload.Label>
    <FileUpload.Trigger class={styles.Trigger}>
      <Paperclip />
      {' Choose file(s)'}
    </FileUpload.Trigger>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <div class={styles.ItemPreview}>
                <FileIcon />
              </div>
              <FileUpload.ItemName class={styles.ItemName} />
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Clear Trigger

Use the `ClearTrigger` to remove all uploaded files at once.

**Example: clear-trigger**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { Paperclip, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component ClearTrigger() {
  <FileUpload.Root maxFiles={5} accept="image/png,image/jpeg" class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'File Upload'}</FileUpload.Label>
    <div class={styles.Actions}>
      <FileUpload.Trigger class={styles.Trigger}>
        <Paperclip />
        {' Choose file(s)'}
      </FileUpload.Trigger>
      <FileUpload.ClearTrigger class={styles.ClearTrigger}>{'Clear Files'}</FileUpload.ClearTrigger>
    </div>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
              </FileUpload.ItemPreview>
              <FileUpload.ItemName class={styles.ItemName} />
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Dropzone

Use the `Dropzone` to enable drag-and-drop. It exposes a `data-dragging` attribute for styling.

**Example: dropzone**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { File, Upload, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component Dropzone() {
  <FileUpload.Root maxFiles={5} class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'File Upload'}</FileUpload.Label>
    <FileUpload.Dropzone class={styles.Dropzone}>
      <Upload class={styles.DropzoneIcon} />
      <div class={styles.DropzoneContent}>
        <span class={styles.DropzoneTitle}>{'Drag and drop files here'}</span>
        <span class={styles.DropzoneDescription}>{'or click to browse'}</span>
      </div>
    </FileUpload.Dropzone>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
              </FileUpload.ItemPreview>
              <FileUpload.ItemPreview type=".*" class={styles.ItemPreview}>
                <File />
              </FileUpload.ItemPreview>
              <FileUpload.ItemName class={styles.ItemName} />
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Directory Upload

Use the `directory` prop to upload entire folders. Access file paths through `file.webkitRelativePath`.

**Example: directory-upload**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { File, Folder, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component DirectoryUpload() {
  <FileUpload.Root directory class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'Upload Folder'}</FileUpload.Label>
    <FileUpload.Trigger class={styles.Trigger}>
      <Folder style={{ width: '1rem', height: '1rem' }} />
      {' Select Folder'}
    </FileUpload.Trigger>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <div class={styles.ItemPreview}>
                <File />
              </div>
              <FileUpload.ItemName class={styles.ItemName}>
                {file.webkitRelativePath || file.name}
              </FileUpload.ItemName>
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

> When uploading directories with many files, set `maxFiles` to a higher value or remove it entirely to prevent
> rejections.

### Accepted File Types

Use the `accept` prop to restrict file types. Accepts MIME types (`image/png`) or extensions (`.pdf`).

**Example: accepted-file-types**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { CircleAlert, Image, Upload, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component AcceptedFileTypes() {
  <FileUpload.Root accept="image/png,image/jpeg" class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'Upload Images (PNG and JPEG only)'}</FileUpload.Label>
    <FileUpload.Dropzone class={styles.Dropzone}>
      <Upload class={styles.DropzoneIcon} />
      <div class={styles.DropzoneContent}>
        <span class={styles.DropzoneTitle}>{'Drop your images here'}</span>
        <span class={styles.DropzoneDescription}>{'Only PNG and JPEG files'}</span>
      </div>
    </FileUpload.Dropzone>

    <FileUpload.Context>
      component children({ context }) {
        if (@context.acceptedFiles.length > 0) {
          <FileUpload.ItemGroup class={styles.ItemGroup}>
            for (const file of @context.acceptedFiles; key file.name) {
              <FileUpload.Item {file} class={styles.Item}>
                <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                  <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
                </FileUpload.ItemPreview>
                <FileUpload.ItemPreview type=".*" class={styles.ItemPreview}>
                  <Image />
                </FileUpload.ItemPreview>
                <FileUpload.ItemName class={styles.ItemName} />
                <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                  <X />
                </FileUpload.ItemDeleteTrigger>
              </FileUpload.Item>
            }
          </FileUpload.ItemGroup>
        }

        if (@context.rejectedFiles.length > 0) {
          <FileUpload.ItemGroup class={styles.ItemGroup}>
            for (const fileRejection of @context.rejectedFiles; key fileRejection.file.name) {
              <FileUpload.Item file={fileRejection.file} class={styles.Item} data-rejected>
                <div class={styles.ItemPreview}>
                  <CircleAlert />
                </div>
                <FileUpload.ItemName class={styles.ItemName} />
                <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                <div class={styles.ErrorList}>
                  for (const error of fileRejection.errors; key error) {
                    <div class={styles.ErrorItem}>{error}</div>
                  }
                </div>
              </FileUpload.Item>
            }
          </FileUpload.ItemGroup>
        }
      }
    </FileUpload.Context>

    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Error Handling

Set constraints with `maxFiles`, `maxFileSize`, `minFileSize`, and `accept`. Rejected files include error codes like
`TOO_MANY_FILES`, `FILE_INVALID_TYPE`, `FILE_TOO_LARGE`, or `FILE_EXISTS`.

**Example: error-handling**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import type { FileUploadFileError } from 'ark-ripple/file-upload';
import { CircleAlert, CircleCheck, File, Upload, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

const errorMessages: Record<FileUploadFileError, string> = {
  TOO_MANY_FILES: 'Too many files selected (max 3 allowed)',
  FILE_INVALID_TYPE: 'Invalid file type (only images and PDFs allowed)',
  FILE_TOO_LARGE: 'File too large (max 1MB)',
  FILE_TOO_SMALL: 'File too small (min 1KB)',
  FILE_INVALID: 'Invalid file',
  FILE_EXISTS: 'File already exists',
};

export component ErrorHandling() {
  <FileUpload.Root
    maxFiles={3}
    maxFileSize={1024 * 1024}
    minFileSize={1024}
    accept="image/*,application/pdf"
    class={styles.Root}
  >
    <FileUpload.Label class={styles.Label}>{'Upload Documents'}</FileUpload.Label>
    <FileUpload.Dropzone class={styles.Dropzone}>
      <Upload class={styles.DropzoneIcon} />
      <div class={styles.DropzoneContent}>
        <span class={styles.DropzoneTitle}>{'Drop files here'}</span>
        <span class={styles.DropzoneDescription}>{'Images and PDFs, max 1MB each'}</span>
      </div>
    </FileUpload.Dropzone>

    <FileUpload.Context>
      component children({ context }) {
        if (@context.acceptedFiles.length > 0) {
          <div class={styles.Section}>
            <div class={styles.SectionTitle} data-status="accepted">
              <CircleCheck
                style={{
                  width: '0.875rem',
                  height: '0.875rem',
                  display: 'inline',
                  marginRight: '0.25rem',
                  verticalAlign: 'middle',
                }}
              />
              {'Accepted Files'}
            </div>
            <FileUpload.ItemGroup class={styles.ItemGroup}>
              for (const file of @context.acceptedFiles; key file.name) {
                <FileUpload.Item {file} class={styles.Item}>
                  <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                    <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
                  </FileUpload.ItemPreview>
                  <FileUpload.ItemPreview type="application/pdf" class={styles.ItemPreview}>
                    <File />
                  </FileUpload.ItemPreview>
                  <FileUpload.ItemName class={styles.ItemName} />
                  <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                  <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                    <X />
                  </FileUpload.ItemDeleteTrigger>
                </FileUpload.Item>
              }
            </FileUpload.ItemGroup>
          </div>
        }

        if (@context.rejectedFiles.length > 0) {
          <div class={styles.Section}>
            <div class={styles.SectionTitle} data-status="rejected">
              <CircleAlert
                style={{
                  width: '0.875rem',
                  height: '0.875rem',
                  display: 'inline',
                  marginRight: '0.25rem',
                  verticalAlign: 'middle',
                }}
              />
              {'Rejected Files'}
            </div>
            <FileUpload.ItemGroup class={styles.ItemGroup}>
              for (const fileRejection of @context.rejectedFiles; key fileRejection.file.name) {
                <FileUpload.Item file={fileRejection.file} class={styles.Item} data-rejected>
                  <div class={styles.ItemPreview}>
                    <CircleAlert />
                  </div>
                  <FileUpload.ItemName class={styles.ItemName} />
                  <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                  <div class={styles.ErrorList}>
                    for (const error of fileRejection.errors; key error) {
                      <div class={styles.ErrorItem}>
                        {errorMessages[error as FileUploadFileError] || error}
                      </div>
                    }
                  </div>
                </FileUpload.Item>
              }
            </FileUpload.ItemGroup>
          </div>
        }
      }
    </FileUpload.Context>

    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### File Transformations

Use `transformFiles` to process files before they're added. Useful for image compression, format conversion, or
resizing.

**Example: transform-files**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { compressAccurately } from 'image-conversion';
import { Image, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

const transformFiles = async (files: File[]) => {
  return Promise.all(
    files.map(async (file) => {
      if (file.type.startsWith('image/')) {
        try {
          const blob = await compressAccurately(file, 200);
          return new File([blob], file.name, { type: blob.type });
        } catch (error) {
          console.error('Compression failed for:', file.name, error);
          return file;
        }
      }
      return file;
    }),
  );
};

export component TransformFiles() {
  <FileUpload.Root accept="image/*" maxFiles={5} {transformFiles} class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'Upload with Compression'}</FileUpload.Label>
    <FileUpload.Trigger class={styles.Trigger}>
      <Image style={{ width: '1rem', height: '1rem' }} />
      {' Choose Images'}
    </FileUpload.Trigger>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
              </FileUpload.ItemPreview>
              <FileUpload.ItemName class={styles.ItemName} />
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Field

Use `Field` to add helper text and error handling.

**Example: with-field**

```ripple
import { Field } from 'ark-ripple/field';
import { FileUpload } from 'ark-ripple/file-upload';
import { File, Upload, X } from 'lucide-ripple';
import field from 'styles/field.module.css';
import styles from 'styles/file-upload.module.css';

export component WithField() {
  <Field.Root class={field.Root}>
    <FileUpload.Root maxFiles={5} class={styles.Root}>
      <FileUpload.Label class={styles.Label}>{'Attachments'}</FileUpload.Label>
      <FileUpload.Dropzone class={styles.Dropzone}>
        <Upload class={styles.DropzoneIcon} />
        <div class={styles.DropzoneContent}>
          <span class={styles.DropzoneTitle}>{'Drop files here'}</span>
          <span class={styles.DropzoneDescription}>{'or click to browse'}</span>
        </div>
      </FileUpload.Dropzone>
      <FileUpload.ItemGroup class={styles.ItemGroup}>
        <FileUpload.Context>
          component children({ context }) {
            for (const file of @context.acceptedFiles; key file.name) {
              <FileUpload.Item {file} class={styles.Item}>
                <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                  <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
                </FileUpload.ItemPreview>
                <FileUpload.ItemPreview type=".*" class={styles.ItemPreview}>
                  <File />
                </FileUpload.ItemPreview>
                <FileUpload.ItemName class={styles.ItemName} />
                <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                  <X />
                </FileUpload.ItemDeleteTrigger>
              </FileUpload.Item>
            }
          }
        </FileUpload.Context>
      </FileUpload.ItemGroup>
      <FileUpload.HiddenInput data-testid="input" />
    </FileUpload.Root>
    <Field.HelperText class={field.HelperText}>{'Upload up to 5 files'}</Field.HelperText>
    <Field.ErrorText class={field.ErrorText}>{'Please upload at least one file'}</Field.ErrorText>
  </Field.Root>
}

```

### Root Provider

An alternative way to control the file upload is to use the `RootProvider` component and the `useFileUpload` hook. This
way you can access the state and methods from outside the component.

**Example: root-provider**

```ripple
import { FileUpload, useFileUpload } from 'ark-ripple/file-upload';
import { File, Upload, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/file-upload.module.css';

export component RootProvider() {
  const fileUpload = useFileUpload({ maxFiles: 5 });

  <div class="stack">
    <button class={button.Root} onClick={() => @fileUpload.clearFiles()}>{'Clear Files'}</button>
    <FileUpload.RootProvider value={fileUpload} class={styles.Root}>
      <FileUpload.Label class={styles.Label}>{'File Upload'}</FileUpload.Label>
      <FileUpload.Dropzone class={styles.Dropzone}>
        <Upload class={styles.DropzoneIcon} />
        <div class={styles.DropzoneContent}>
          <span class={styles.DropzoneTitle}>{'Drop files here'}</span>
          <span class={styles.DropzoneDescription}>{'or click to browse'}</span>
        </div>
      </FileUpload.Dropzone>
      <FileUpload.ItemGroup class={styles.ItemGroup}>
        <FileUpload.Context>
          component children({ context }) {
            for (const file of @context.acceptedFiles; key file.name) {
              <FileUpload.Item {file} class={styles.Item}>
                <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                  <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
                </FileUpload.ItemPreview>
                <FileUpload.ItemPreview type=".*" class={styles.ItemPreview}>
                  <File />
                </FileUpload.ItemPreview>
                <FileUpload.ItemName class={styles.ItemName} />
                <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                  <X />
                </FileUpload.ItemDeleteTrigger>
              </FileUpload.Item>
            }
          }
        </FileUpload.Context>
      </FileUpload.ItemGroup>
      <FileUpload.HiddenInput />
    </FileUpload.RootProvider>
  </div>
}

```

### Pasting Files

Use `setClipboardFiles` to enable pasting images from the clipboard.

**Example: pasting-files**

```ripple
import { FileUpload, useFileUpload } from 'ark-ripple/file-upload';
import { Clipboard, X } from 'lucide-ripple';
import field from 'styles/field.module.css';
import styles from 'styles/file-upload.module.css';

export component PastingFiles() {
  const fileUpload = useFileUpload({ maxFiles: 3, accept: 'image/*' });

  <FileUpload.RootProvider value={fileUpload} class={styles.Root}>
    <FileUpload.Label class={styles.Label}>
      <Clipboard
        style={{
          width: '1rem',
          height: '1rem',
          display: 'inline',
          marginRight: '0.25rem',
          verticalAlign: 'middle',
        }}
      />
      {'Upload with Paste'}
    </FileUpload.Label>
    <textarea
      class={field.Textarea}
      placeholder="Paste an image here (Ctrl/Cmd + V)"
      onPaste={(e) => {
        @fileUpload.setClipboardFiles((e as ClipboardEvent).clipboardData!);
      }}
    />
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      for (const file of @fileUpload.acceptedFiles; key file.name) {
        <FileUpload.Item {file} class={styles.Item}>
          <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
            <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
          </FileUpload.ItemPreview>
          <FileUpload.ItemName class={styles.ItemName} />
          <FileUpload.ItemSizeText class={styles.ItemSizeText} />
          <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
            <X />
          </FileUpload.ItemDeleteTrigger>
        </FileUpload.Item>
      }
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.RootProvider>
}

```

### Media Capture

Use `capture` to access the device camera. Set to `"environment"` for back camera or `"user"` for front camera.

**Example: media-capture**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { Camera, File, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component MediaCapture() {
  <FileUpload.Root capture="environment" class={styles.Root}>
    <FileUpload.Label class={styles.Label}>{'Capture Photo'}</FileUpload.Label>
    <FileUpload.Trigger class={styles.Trigger}>
      <Camera style={{ width: '1rem', height: '1rem' }} />
      {' Open Camera'}
    </FileUpload.Trigger>
    <FileUpload.ItemGroup class={styles.ItemGroup}>
      <FileUpload.Context>
        component children({ context }) {
          for (const file of @context.acceptedFiles; key file.name) {
            <FileUpload.Item {file} class={styles.Item}>
              <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
              </FileUpload.ItemPreview>
              <FileUpload.ItemPreview type=".*" class={styles.ItemPreview}>
                <File />
              </FileUpload.ItemPreview>
              <FileUpload.ItemName class={styles.ItemName}>
                {file.webkitRelativePath || file.name}
              </FileUpload.ItemName>
              <FileUpload.ItemSizeText class={styles.ItemSizeText} />
              <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                <X />
              </FileUpload.ItemDeleteTrigger>
            </FileUpload.Item>
          }
        }
      </FileUpload.Context>
    </FileUpload.ItemGroup>
    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

### Rejected Files

Access `rejectedFiles` from the context to display validation errors.

**Example: rejected-files**

```ripple
import { FileUpload } from 'ark-ripple/file-upload';
import { CircleAlert, CircleCheck, Upload, X } from 'lucide-ripple';
import styles from 'styles/file-upload.module.css';

export component RejectedFiles() {
  <FileUpload.Root
    maxFiles={2}
    class={styles.Root}
    onFileReject={(details) => {
      console.log('Rejected files:', details);
    }}
  >
    <FileUpload.Label class={styles.Label}>{'Upload Files (Max 2)'}</FileUpload.Label>
    <FileUpload.Dropzone class={styles.Dropzone}>
      <Upload class={styles.DropzoneIcon} />
      <div class={styles.DropzoneContent}>
        <span class={styles.DropzoneTitle}>{'Drop files here'}</span>
        <span class={styles.DropzoneDescription}>{'Maximum 2 files allowed'}</span>
      </div>
    </FileUpload.Dropzone>

    <FileUpload.Context>
      component children({ context }) {
        if (@context.acceptedFiles.length > 0) {
          <div class={styles.Section}>
            <div class={styles.SectionTitle} data-status="accepted">
              <CircleCheck
                style={{
                  width: '0.875rem',
                  height: '0.875rem',
                  display: 'inline',
                  marginRight: '0.25rem',
                  verticalAlign: 'middle',
                }}
              />
              {'Accepted Files'}
            </div>
            <FileUpload.ItemGroup type="accepted" class={styles.ItemGroup}>
              for (const file of @context.acceptedFiles; key file.name) {
                <FileUpload.Item {file} class={styles.Item}>
                  <FileUpload.ItemPreview type="image/*" class={styles.ItemPreview}>
                    <FileUpload.ItemPreviewImage class={styles.ItemPreviewImage} />
                  </FileUpload.ItemPreview>
                  <FileUpload.ItemName class={styles.ItemName} />
                  <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                  <FileUpload.ItemDeleteTrigger class={styles.ItemDeleteTrigger}>
                    <X />
                  </FileUpload.ItemDeleteTrigger>
                </FileUpload.Item>
              }
            </FileUpload.ItemGroup>
          </div>
        }

        if (@context.rejectedFiles.length > 0) {
          <div class={styles.Section}>
            <div class={styles.SectionTitle} data-status="rejected">
              <CircleAlert
                style={{
                  width: '0.875rem',
                  height: '0.875rem',
                  display: 'inline',
                  marginRight: '0.25rem',
                  verticalAlign: 'middle',
                }}
              />
              {'Rejected Files'}
            </div>
            <FileUpload.ItemGroup type="rejected" class={styles.ItemGroup}>
              for (const { file, errors } of @context.rejectedFiles; key file.name) {
                <FileUpload.Item {file} class={styles.Item} data-rejected>
                  <div class={styles.ItemPreview}>
                    <CircleAlert />
                  </div>
                  <FileUpload.ItemName class={styles.ItemName} />
                  <FileUpload.ItemSizeText class={styles.ItemSizeText} />
                  <div class={styles.ErrorList}>
                    for (const error of errors; key error) {
                      <span class={styles.ErrorItem}>{error}</span>
                    }
                  </div>
                </FileUpload.Item>
              }
            </FileUpload.ItemGroup>
          </div>
        }
      }
    </FileUpload.Context>

    <FileUpload.HiddenInput />
  </FileUpload.Root>
}

```

## Guides

### File Previews

Use `ItemPreview` with type matching to show appropriate previews based on file format.

- `type="image/*"`: Shows image thumbnails using `ItemPreviewImage`
- `type="video/*"`: For video file previews
- `type="application/pdf"`: For PDF files
- `type=".*"`: Generic fallback for any file type

```tsx
<FileUpload.ItemPreview type="image/*">
  <FileUpload.ItemPreviewImage />
</FileUpload.ItemPreview>

<FileUpload.ItemPreview type="video/*">
  <VideoIcon />
</FileUpload.ItemPreview>

<FileUpload.ItemPreview type="application/pdf">
  <PdfIcon />
</FileUpload.ItemPreview>

<FileUpload.ItemPreview type=".*">
  <FileIcon />
</FileUpload.ItemPreview>
```

### Disable Dropzone

To disable drag-and-drop functionality, set `allowDrop` to `false`.

```tsx
<FileUpload.Root allowDrop={false}>{/* ... */}</FileUpload.Root>
```

### Prevent Document Drop

By default, we prevent accidental navigation when files are dropped outside the dropzone. Set `preventDocumentDrop` to
`false` to disable this.

```tsx
<FileUpload.Root preventDocumentDrop={false}>{/* ... */}</FileUpload.Root>
```

### Prevent Double Open

Use `disableClick` on `Dropzone` when delegating clicks to a nested `Trigger`. This prevents the file picker from
opening twice.

```tsx
<FileUpload.Dropzone disableClick>
  <FileUpload.Trigger>Choose Files</FileUpload.Trigger>
  Drag files here
</FileUpload.Dropzone>
```

## API Reference

### Props

**Component API Reference**

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `accept` | `Record<string, string[]> | FileMimeType | FileMimeType[]` | No | The accept file types |
| `acceptedFiles` | `File[]` | No | The controlled accepted files |
| `allowDrop` | `boolean` | No | Whether to allow drag and drop in the dropzone element |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `capture` | `'user' | 'environment'` | No | The default camera to use when capturing media |
| `defaultAcceptedFiles` | `File[]` | No | The default accepted files when rendered.
Use when you don't need to control the accepted files of the input. |
| `directory` | `boolean` | No | Whether to accept directories, only works in webkit browsers |
| `disabled` | `boolean` | No | Whether the file input is disabled |
| `ids` | `Partial<{
  root: string
  dropzone: string
  hiddenInput: string
  trigger: string
  label: string
  item: (id: string) => string
  itemName: (id: string) => string
  itemSizeText: (id: string) => string
  itemPreview: (id: string) => string
  itemDeleteTrigger: (id: string) => string
}>` | No | The ids of the elements. Useful for composition. |
| `invalid` | `boolean` | No | Whether the file input is invalid |
| `locale` | `string` | No | The current locale. Based on the BCP 47 definition. |
| `maxFiles` | `number` | No | The maximum number of files |
| `maxFileSize` | `number` | No | The maximum file size in bytes |
| `minFileSize` | `number` | No | The minimum file size in bytes |
| `name` | `string` | No | The name of the underlying file input |
| `onFileAccept` | `(details: FileAcceptDetails) => void` | No | Function called when the file is accepted |
| `onFileChange` | `(details: FileChangeDetails) => void` | No | Function called when the value changes, whether accepted or rejected |
| `onFileReject` | `(details: FileRejectDetails) => void` | No | Function called when the file is rejected |
| `preventDocumentDrop` | `boolean` | No | Whether to prevent the drop event on the document |
| `readOnly` | `boolean` | No | Whether the file input is read-only |
| `required` | `boolean` | No | Whether the file input is required |
| `transformFiles` | `(files: File[]) => Promise<File[]>` | No | Function to transform the accepted files to apply transformations |
| `translations` | `IntlTranslations` | No | The localized messages to use. |
| `validate` | `(file: File, details: FileValidateDetails) => FileError[] | null` | No | Function to validate a file |


**Root Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | root |
| `[data-disabled]` | Present when disabled |
| `[data-readonly]` | Present when read-only |
| `[data-dragging]` | Present when in the dragging state |


**ClearTrigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ClearTrigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | clear-trigger |
| `[data-disabled]` | Present when disabled |
| `[data-readonly]` | Present when read-only |


**Dropzone Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `disableClick` | `boolean` | No | Whether to disable the click event on the dropzone |


**Dropzone Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | dropzone |
| `[data-invalid]` | Present when invalid |
| `[data-disabled]` | Present when disabled |
| `[data-readonly]` | Present when read-only |
| `[data-dragging]` | Present when in the dragging state |


**HiddenInput Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemDeleteTrigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemDeleteTrigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-delete-trigger |
| `[data-disabled]` | Present when disabled |
| `[data-readonly]` | Present when read-only |
| `[data-type]` | The type of the item |


**ItemGroup Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `type` | `ItemType` | No |  |


**ItemGroup Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-group |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**ItemName Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemName Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-name |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**ItemPreviewImage Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemPreviewImage Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-preview-image |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**ItemPreview Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `type` | `string` | No | The file type to match against. Matches all file types by default. |


**ItemPreview Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-preview |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**Item Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `file` | `File` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Item Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**ItemSizeText Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemSizeText Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | item-size-text |
| `[data-disabled]` | Present when disabled |
| `[data-type]` | The type of the item |


**Label Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Label Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | label |
| `[data-disabled]` | Present when disabled |
| `[data-required]` | Present when required |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `UseFileUploadReturn` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Trigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Trigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | file-upload |
| `[data-part]` | trigger |
| `[data-disabled]` | Present when disabled |
| `[data-readonly]` | Present when read-only |
| `[data-invalid]` | Present when invalid |


### Context

**API:**

| Property | Type | Description |
|----------|------|-------------|
| `dragging` | `boolean` | Whether the user is dragging something over the root element |
| `focused` | `boolean` | Whether the user is focused on the dropzone element |
| `disabled` | `boolean` | Whether the file input is disabled |
| `readOnly` | `boolean` | Whether the file input is in read-only mode |
| `transforming` | `boolean` | Whether files are currently being transformed via `transformFiles` |
| `maxFilesReached` | `boolean` | Whether the maximum number of files has been reached |
| `remainingFiles` | `number` | The number of files that can still be added |
| `openFilePicker` | `VoidFunction` | Function to open the file dialog |
| `deleteFile` | `(file: File, type?: ItemType) => void` | Function to delete the file from the list |
| `acceptedFiles` | `File[]` | The accepted files that have been dropped or selected |
| `rejectedFiles` | `FileRejection[]` | The files that have been rejected |
| `setFiles` | `(files: File[]) => void` | Sets the accepted files |
| `clearFiles` | `VoidFunction` | Clears the accepted files |
| `clearRejectedFiles` | `VoidFunction` | Clears the rejected files |
| `getFileSize` | `(file: File) => string` | Returns the formatted file size (e.g. 1.2MB) |
| `createFileUrl` | `(file: File, cb: (url: string) => void) => VoidFunction` | Returns the preview url of a file.
Returns a function to revoke the url. |
| `setClipboardFiles` | `(dt: DataTransfer) => boolean` | Sets the clipboard files
Returns `true` if the clipboard data contains files, `false` otherwise. |
