# Attachment

File attachment chips with media, title, description, and action slots, plus idle, uploading, processing, error, and done states, three sizes, and horizontal or vertical orientation.

- Category: Forms
- License: MIT
- Page: https://www.saasuji.com/components/attachment
- Install: npx shadcn@latest add https://www.saasuji.com/r/attachment.json

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| state (Attachment) | `"idle" | "uploading" | "processing" | "error" | "done"` | `"done"` | Upload lifecycle state; uploading and processing shimmer the title and error colors the border. |
| size (Attachment) | `"default" | "sm" | "xs"` | `"default"` | Padding and gap scale of the chip. |
| orientation (Attachment) | `"horizontal" | "vertical"` | `"horizontal"` | Stack media and content in a row or a centered column. |
| variant (AttachmentMedia) | `"icon" | "image"` | `"icon"` | Media box style: a square icon tile or a larger image thumbnail. |
| size (AttachmentAction) | `Button size` | `"icon-xs"` | Button size for the action; all other Button props are forwarded. |
| render (AttachmentTrigger) | `React.ReactElement` | — | Renders and clones a custom element as the overlay trigger instead of a <button>. |
| className | `string` | — | Additional classes merged onto the sub-component that receives it. |
| children | `React.ReactNode` | — | Media, content, and action slots inside the attachment. |

## Source

```tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

type AttachmentState = "idle" | "uploading" | "processing" | "error" | "done"

const AttachmentContext = React.createContext<{ state: AttachmentState }>({ state: "done" })

const attachmentVariants = cva(
  "relative flex w-full items-center gap-3 overflow-hidden rounded-lg border bg-card p-3 text-card-foreground transition-colors",
  {
    variants: {
      state: {
        idle: "border-border",
        uploading: "border-border",
        processing: "border-border",
        error: "border-destructive/50 text-destructive dark:border-destructive",
        done: "border-border",
      },
      size: {
        default: "gap-3 p-3",
        sm: "gap-2.5 p-2.5",
        xs: "gap-2 p-2",
      },
      orientation: {
        horizontal: "flex-row",
        vertical: "flex-col items-stretch text-center",
      },
    },
    defaultVariants: {
      state: "done",
      size: "default",
      orientation: "horizontal",
    },
  }
)

function Attachment({
  className,
  state = "done",
  size = "default",
  orientation = "horizontal",
  ...props
}: React.ComponentProps<"div"> &
  VariantProps<typeof attachmentVariants> & { state?: AttachmentState }) {
  return (
    <AttachmentContext.Provider value={{ state: state ?? "done" }}>
      <div
        data-slot="attachment"
        data-state={state}
        data-size={size}
        className={cn(attachmentVariants({ state, size, orientation }), className)}
        {...props}
      />
    </AttachmentContext.Provider>
  )
}

const attachmentMediaVariants = cva(
  "flex shrink-0 items-center justify-center overflow-hidden rounded-md border bg-muted text-muted-foreground",
  {
    variants: {
      variant: {
        icon: "size-10 [&_svg]:size-5",
        image: "size-16 p-0 [&_img]:h-full [&_img]:w-full [&_img]:object-cover",
      },
    },
    defaultVariants: {
      variant: "icon",
    },
  }
)

function AttachmentMedia({
  className,
  variant = "icon",
  ...props
}: React.ComponentProps<"div"> & { variant?: "icon" | "image" }) {
  return (
    <div
      data-slot="attachment-media"
      data-variant={variant}
      className={cn(attachmentMediaVariants({ variant }), className)}
      {...props}
    />
  )
}

function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="attachment-content"
      className={cn("flex min-w-0 flex-1 flex-col items-start gap-0.5 text-left", className)}
      {...props}
    />
  )
}

function AttachmentTitle({ className, ...props }: React.ComponentProps<"div">) {
  const { state } = React.useContext(AttachmentContext)
  const shimmer = state === "uploading" || state === "processing"
  return (
    <div
      data-slot="attachment-title"
      className={cn(
        "w-full truncate text-sm font-medium leading-none",
        shimmer && "animate-pulse text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function AttachmentDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="attachment-description"
      className={cn("w-full truncate text-xs text-muted-foreground", className)}
      {...props}
    />
  )
}

function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="attachment-actions"
      className={cn("flex shrink-0 items-center gap-1", className)}
      {...props}
    />
  )
}

function AttachmentAction({
  size = "icon-xs",
  ...props
}: React.ComponentProps<typeof Button>) {
  return (
    <Button
      data-slot="attachment-action"
      variant="ghost"
      size={size}
      {...props}
    />
  )
}

function AttachmentTrigger({
  className,
  render,
  ...props
}: React.ComponentProps<"button"> & { render?: React.ReactElement }) {
  if (render && React.isValidElement(render)) {
    return React.cloneElement(render as React.ReactElement<{ className?: string }>, {
      ...props,
      className: cn(
        "absolute inset-0 z-0 cursor-pointer rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
        (render.props as { className?: string }).className,
        className
      ),
    })
  }
  return (
    <button
      data-slot="attachment-trigger"
      type="button"
      className={cn(
        "absolute inset-0 z-0 cursor-pointer rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
        className
      )}
      {...props}
    />
  )
}

function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="attachment-group"
      className={cn(
        "flex gap-3 overflow-x-auto pb-1 [scroll-snap-type:x_mandatory] [&>*]:shrink-0 [&>*]:snap-start",
        "[mask-image:linear-gradient(to_right,black_calc(100%-2rem),transparent)]",
        className
      )}
      {...props}
    />
  )
}

export {
  Attachment,
  AttachmentMedia,
  AttachmentContent,
  AttachmentTitle,
  AttachmentDescription,
  AttachmentActions,
  AttachmentAction,
  AttachmentTrigger,
  AttachmentGroup,
}

```
