# File Attachment Chips — React File Component

> Attachment chips with a type-coloured icon and size; new chips pop in, removed ones collapse, and the rest reflow with spring layout animation.

- Category: [Files & Uploads](https://pixelpapa.com/components/files.md)
- Tags: attachment, chips, files, remove, composer
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/files/file-attachment-chips

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/file-attachment-chips.json"
```

The file is written to `components/pixelpapa/file-attachment-chips.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ArrowUp, FileSpreadsheet, FileText, Film, Image as ImageIcon, Paperclip, X, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

type Kind = "image" | "pdf" | "sheet" | "video";
export type Attachment = { id: string; name: string; size: string; kind: Kind };

const kinds: Record<Kind, { icon: LucideIcon; tone: string }> = {
  image: { icon: ImageIcon, tone: "bg-violet-500/15 text-violet-600 dark:text-violet-400" },
  pdf: { icon: FileText, tone: "bg-rose-500/15 text-rose-600 dark:text-rose-400" },
  sheet: { icon: FileSpreadsheet, tone: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" },
  video: { icon: Film, tone: "bg-sky-500/15 text-sky-600 dark:text-sky-400" },
};

/** Keeps the extension visible when a long name is shortened. */
function middle(name: string, max = 22) {
  if (name.length <= max) return name;
  const dot = name.lastIndexOf(".");
  const ext = dot > 0 ? name.slice(dot) : "";
  return `${name.slice(0, max - ext.length - 4)}…${name.slice(dot - 3, dot)}${ext}`;
}

type AttachmentChipsProps = {
  items: Attachment[];
  onRemove: (id: string) => void;
  className?: string;
};

export function AttachmentChips({ items, onRemove, className }: AttachmentChipsProps) {
  return (
    <ul aria-label="Attachments" className={cn("flex flex-wrap gap-2", className)}>
      <AnimatePresence mode="popLayout" initial={false}>
        {items.map((a) => {
          const { icon: Icon, tone } = kinds[a.kind];
          return (
            <motion.li
              key={a.id}
              layout
              initial={{ opacity: 0, scale: 0.6 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.15 } }}
              transition={{ type: "spring", stiffness: 480, damping: 30 }}
              className="group flex items-center gap-2 rounded-xl border border-border bg-background py-1 pr-1 pl-1 shadow-xs"
            >
              <span className={cn("flex size-8 items-center justify-center rounded-lg", tone)}>
                <Icon className="size-4" />
              </span>
              <span className="leading-tight">
                <span title={a.name} className="block text-xs font-medium whitespace-nowrap text-foreground">
                  {middle(a.name)}
                </span>
                <span className="block text-[11px] text-muted-foreground">{a.size}</span>
              </span>
              <button
                type="button"
                aria-label={`Remove ${a.name}`}
                onClick={() => onRemove(a.id)}
                className="flex size-6 items-center justify-center rounded-full text-muted-foreground/70 outline-none transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:ring-2 focus-visible:ring-ring/60"
              >
                <X className="size-3.5" />
              </button>
            </motion.li>
          );
        })}
      </AnimatePresence>
    </ul>
  );
}

const pool: Attachment[] = [
  { id: "a1", name: "fleet-dashboard-wireframes-final.png", size: "2.4 MB", kind: "image" },
  { id: "a2", name: "halden-msa.pdf", size: "412 KB", kind: "pdf" },
  { id: "a3", name: "route-costs-october.xlsx", size: "204 KB", kind: "sheet" },
  { id: "a4", name: "depot-walkthrough.mp4", size: "38 MB", kind: "video" },
  { id: "a5", name: "driver-survey-results.pdf", size: "96 KB", kind: "pdf" },
];

export default function FileAttachmentChipsDemo() {
  const [items, setItems] = useState(pool.slice(0, 3));
  const next = pool.find((p) => !items.some((i) => i.id === p.id));

  return (
    <div className="w-full max-w-lg rounded-2xl border border-border bg-card p-3 shadow-sm transition-shadow focus-within:ring-2 focus-within:ring-ring/25">
      <label htmlFor="file-attachment-chips-message" className="sr-only">
        Message
      </label>
      <textarea
        id="file-attachment-chips-message"
        rows={2}
        defaultValue="Wireframes and the signed MSA are attached. Costs are in the sheet, tab two."
        className="block w-full resize-none bg-transparent px-2 pt-1.5 text-sm leading-relaxed text-foreground outline-none placeholder:text-muted-foreground"
      />
      <motion.div layout className="mt-2 px-1">
        <AttachmentChips items={items} onRemove={(id) => setItems((prev) => prev.filter((i) => i.id !== id))} />
      </motion.div>
      <motion.div layout className="mt-3 flex items-center justify-between">
        <button
          type="button"
          disabled={!next}
          onClick={() => next && setItems((prev) => [...prev, next])}
          className="inline-flex h-8 items-center gap-1.5 rounded-full px-2.5 text-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-40"
        >
          <Paperclip className="size-4" />
          Attach
          <span className="rounded-full bg-muted px-1.5 text-[11px] tabular-nums">{items.length}/5</span>
        </button>
        <button
          type="button"
          aria-label="Send message"
          className="flex size-8 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm shadow-primary/30 outline-none hover:brightness-110 focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-card"
        >
          <ArrowUp className="size-4" strokeWidth={2.5} />
        </button>
      </motion.div>
    </div>
  );
}
```

## AI prompt

```text
Add the "File Attachment Chips" component from PixelPapa UI to this project.

What it is: Attachment chips with a type-coloured icon and size; new chips pop in, removed ones collapse, and the rest reflow with spring layout animation.
Category: Files & Uploads. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/file-attachment-chips.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/file-attachment-chips.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/file-attachment-chips" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More files & uploads

- [File List Progress](https://pixelpapa.com/components/files/file-list-progress.md) (Pro): An upload queue with pause, resume, cancel and retry: two files transfer at a time, queued ones start on their own, and finished rows settle with a drawn check.
- [File Preview Grid](https://pixelpapa.com/components/files/file-preview-grid.md): A grid of file tiles colour-coded by type, each with a drawn preview; tiles lift on hover to reveal actions and a selection count rolls in the header.
- [File Tree Explorer](https://pixelpapa.com/components/files/file-tree-explorer.md) (Pro): A file tree with spring-animated folders, indent guides and full arrow-key navigation; the selection highlight slides between rows.
- [File Upload Dropzone](https://pixelpapa.com/components/files/file-upload-dropzone.md): A dropzone that lifts and glows while files hover over it, then lists each file with its own simulated progress bar, a drawn check on completion and a remove button.
