## Composer Attachments (S—483)

The attachment strip for AI chat composers: progress rings that draw around each tile, retry, previews and drag and drop.

- URL: https://sundaeui.dev/components/composer-attachments
- Category: UI components · Art direction: Refined
- Access: Sundae Pro (authenticated install)
- Stack: React 18+, TypeScript, plain CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/composer-attachments.json`
- Tags: upload, chat, agent, llm, progress, drag-and-drop, file-upload, attachments, composer, ai

Files dropped or picked into a chat composer land in a strip above the input, one after another. Images get real thumbnails; documents, spreadsheets, PDFs, slides, code and archives get a typed file glyph with the name, extension and size.
While a file uploads, an accent ring draws clockwise around the tile’s own outline and a percentage counts up in the corner. At 100% the ring flashes green and fades, and the percentage blurs out as the dismiss button blurs in on the same spot. Failed uploads turn red with the reason and a retry button; files over your size limit fail without uploading.
It ships with useAttachmentUploads, a hook that takes real File objects and your own upload function (fetch, S3 presigned URLs, Supabase Storage, anything that reports progress), uploads them one at a time, cancels on remove and frees image thumbnails when they go. AttachmentDropZone turns any composer into a drop target.

### Usage

```tsx
import { AttachmentDropZone, ComposerAttachments, useAttachmentUploads } from '@/components/sundae/composer-attachments'

function Composer() {
  const files = useAttachmentUploads({
    maxSize: 25 * 1024 * 1024,
    // Your uploader: report progress, honour the abort signal.
    upload: (file, { onProgress, signal }) =>
      new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.open('POST', '/api/upload')
        xhr.upload.onprogress = (e) => onProgress(e.loaded / e.total)
        xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error('Upload failed')))
        xhr.onerror = () => reject(new Error('Network error'))
        signal.addEventListener('abort', () => xhr.abort())
        xhr.send(file)
      }),
  })

  return (
    <AttachmentDropZone onFiles={files.addFiles}>
      <ComposerAttachments items={files.items} onRemove={files.remove} onRetry={files.retry} />
      <textarea placeholder="Ask anything" />
      <input type="file" multiple onChange={(e) => e.target.files && files.addFiles(e.target.files)} />
    </AttachmentDropZone>
  )
}
```

### Props

- `items` (`AttachmentItem[]`, required): Files to show: name, size, type, previewUrl, status (queued, uploading, done, error), progress 0–1, error.
- `onRemove` (`(id: string) => void`, required): Remove a file (and cancel its upload).
- `onRetry` (`(id: string) => void`): Retry a failed upload. Without it, failed tiles only offer remove.
- `preview` (`boolean`, default `true`): Open images in a lightbox when their tile is pressed.
- `summary` (`boolean`, default `true`): Show the "3 files · 4.2 MB" line and upload progress under the strip.
- `theme` (`'auto' | 'light' | 'dark'`, default `'auto'`): auto follows a .dark ancestor.
- `useAttachmentUploads({ upload, concurrency, maxSize, maxFiles })` (`hook`): Manages real File objects: { items, addFiles, remove, retry, clear, uploading }. concurrency defaults to 1 (sequential).
- `<AttachmentDropZone onFiles label>` (`component`): Wrap the composer to accept dropped files, with an outline and label while dragging.

### When to use

- Chat and agent composers that accept files, images and documents.
- Any form where several files upload in the background while people keep typing.

### When not to use

- A dedicated upload page with large files and folders: use Upload Queue.
- Single profile-photo uploads: use Avatar Upload.

### Accessibility

- Every tile control is a real button with a specific label: “Remove report.pdf”, “Cancel upload of report.pdf”, “Retry report.pdf”, “Preview photo.jpg”.
- Upload completion and failure are announced through a polite status region per tile; progress is not announced on every tick.
- The image preview is a native modal dialog: focus moves in, Escape closes it, and focus returns to the tile.
- Reduced motion removes the entrance, ring, blur and settle animations; states still change instantly.

### FAQ

**Does it upload anything by itself?**
No. useAttachmentUploads calls the upload function you pass, with the File, an onProgress callback and an AbortSignal, so it works with any backend.

**Can several files upload at once?**
Yes: pass concurrency (default 1, which uploads them one after another).

