# Attachment & Dropzone

- Page: <https://kznlabs.github.io/labsUI/components/attachment>

## Attachment & Dropzone (`attachment`, component)

File chips with image thumbnails or type icons, size, a springing upload bar, error state and remove; the list springs chips in and closes the gap. FileDropzone wraps a real file input: click, keyboard and the native picker work, drop and paste are checked against accept and maxSize, and dropped files land in the input so forms and Server Actions receive them.

- Install: `npx @kznlabs/labsui add attachment`
- Docs: <https://kznlabs.github.io/labsUI/components/attachment>
- Brings: field, label, utils
- npm: clsx@^2.1.1, lucide-react@1.63.0, motion@14.7.1, tailwind-merge@^3.7.0

### API

- `formatBytes(bytes: number, locale?: string) => string`: Bytes as text in the given locale: 1536 is "1.5 kB".
- `<Attachment>`: File chip: thumbnail or type icon, name, size, upload progress or error, and remove.
  - `name: string`: File name.
  - `error?: string`: Replaces the size line and turns the chip red.
  - `href?: string`: Makes the name a download link.
  - `locale?: string`: BCP 47 locale for the size.
  - `onRemove?: () => void`: Shows a remove button; called when it is pressed.
  - `progress?: number`: 0 to 100 while uploading; the bar springs along the bottom edge.
  - `size?: number`: Bytes.
  - `src?: string`: Thumbnail source: a URL, or `URL.createObjectURL(file)` for a fresh upload.
  - `type?: string`: MIME type; picks the icon, and image types show `src` as a thumbnail.
- `<AttachmentList>` (+ <ul> props): Attachments that spring in, out and into place when one is removed. Key each child.
- `<FileDropzone>`: Drop area around a real file input: click, Enter/Space and the native picker work as-is, and
dropped files are written back into the input so plain forms and Server Actions get them.
  - `accept?: string`: Same syntax as the input attribute: ".pdf,image/*". Enforced on drop and paste too.
  - `aria-describedby?: string`: Ids of hint or error text; Field fills it in.
  - `aria-invalid?: boolean`: Error styling; Field sets it from `error`.
  - `children?: ReactNode`: Replaces the default "Drop files or browse" content.
  - `disabled?: boolean`: Blocks drops and the picker.
  - `id?: string`: Id of the file input; Field fills it in.
  - `maxSize?: number`: Bytes.
  - `multiple?: boolean`: Accept more than one file.
  - `name?: string`: Form field name: picked and dropped files both submit with the form.
  - `onFiles?: (files: File[]) => void`: Files that passed `accept` and `maxSize`, from a drop or the picker.
  - `onReject?: (rejected: Rejection[]) => void`: Files that failed `accept` or `maxSize`, each with a reason.
  - `required?: boolean`: Form submit needs at least one file.

- `type Rejection = { file: File; reason: "type" | "size" }`

### Usage

```tsx
<FileDropzone name="clips" multiple accept="video/*" maxSize={50 * 1024 * 1024} onFiles={add} onReject={warn} /><AttachmentList>{files.map((f) => <Attachment key={f.id} name={f.name} size={f.size} type={f.type} progress={f.progress} onRemove={() => remove(f.id)} />)}</AttachmentList>
```

### Example: clip-upload.tsx

```tsx
import { FileDropzone } from "@/components/ui/attachment";
import { Button } from "@/components/ui/button";
import { uploadClips } from "./actions"; // "use server": (formData) => formData.getAll("clips")

// Picked and dropped files both land in the input, so the Server Action gets them.
export function ClipUpload() {
  return (
    <form action={uploadClips} className="grid gap-3">
      <FileDropzone name="clips" multiple accept="video/*" maxSize={500 * 1024 * 1024} required />
      <Button type="submit">Upload</Button>
    </form>
  );
}
```

### Example: attachments.tsx

```tsx
"use client";

import { Attachment, AttachmentList } from "@/components/ui/attachment";

type File = { id: string; name: string; size: number; type: string; progress?: number };

export function Attachments({ files, onRemove }: { files: File[]; onRemove: (id: string) => void }) {
  return (
    <AttachmentList aria-label="Attachments">
      {files.map((f) => (
        <Attachment
          key={f.id}
          name={f.name}
          size={f.size}
          type={f.type}
          progress={f.progress}
          onRemove={() => onRemove(f.id)}
        />
      ))}
    </AttachmentList>
  );
}
```
