{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "title": "Attachment",
  "author": "Tori Bryan",
  "description": "One added file as a chip or an image thumbnail, with its upload status and buttons to remove or retry.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/attachment.tsx",
      "content": "import * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport {\n  CircleAlertIcon,\n  ImageIcon,\n  LoaderCircleIcon,\n  PaperclipIcon,\n  Redo2Icon,\n  XIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst attachmentVariants = cva(\"relative shrink-0\", {\n  variants: {\n    type: {\n      file: \"flex h-7 max-w-56 items-center gap-1.5 rounded-full border bg-background pr-1 pl-2.5 text-xs text-foreground\",\n      image: \"size-14 overflow-hidden rounded-lg border bg-muted\",\n    },\n    status: {\n      ready: \"border-border\",\n      loading: \"border-border\",\n      error: \"border-destructive\",\n    },\n  },\n  defaultVariants: { type: \"file\", status: \"ready\" },\n})\n\nconst iconButton =\n  \"flex shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring-subtle disabled:pointer-events-none\"\n\ntype AttachmentProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** `file` is a one-line chip with the name and size; `image` is a thumbnail. */\n  type?: \"file\" | \"image\"\n  /** Where the upload is. Whoever owns the upload sets it. */\n  status?: \"ready\" | \"loading\" | \"error\"\n  /** The file name. Also the image's alt text. */\n  name: string\n  /** Size in bytes, shown on a ready file chip. */\n  size?: number\n  /** The preview for `type=\"image\"`, such as an object URL of the picked file. */\n  src?: string\n  /** What went wrong, in a few words. Shown on a file chip, read out on an image. */\n  error?: string\n  /** Called by the remove button. Without it there is no remove button. */\n  onRemove?: () => void\n  /** Called to try a failed upload again. Without it there is no retry. */\n  onRetry?: () => void\n  /** Turns the buttons off, as when the surrounding form is disabled. */\n  disabled?: boolean\n}\n\n/*\n * One file someone has added. It shows an upload but never runs one, so the\n * same part works for a chat draft, a form field or a sent message.\n */\nfunction Attachment({\n  type = \"file\",\n  status = \"ready\",\n  name,\n  size,\n  src,\n  error = \"Upload failed\",\n  onRemove,\n  onRetry,\n  disabled,\n  className,\n  ...props\n}: AttachmentProps) {\n  const remove = onRemove ? (\n    <button\n      type=\"button\"\n      data-slot=\"attachment-remove\"\n      aria-label={`Remove ${name}`}\n      disabled={disabled}\n      onClick={onRemove}\n      className={cn(\n        iconButton,\n        type === \"file\"\n          ? \"size-5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n          : \"absolute top-[3px] right-[3px] z-10 size-[18px] border border-border bg-background text-foreground hover:bg-muted\"\n      )}\n    >\n      <XIcon className={type === \"file\" ? \"size-3\" : \"size-2.5\"} />\n    </button>\n  ) : null\n\n  if (type === \"image\") {\n    return (\n      <div\n        data-slot=\"attachment\"\n        data-type=\"image\"\n        data-status={status}\n        aria-busy={status === \"loading\" || undefined}\n        title={status === \"error\" ? `${name}: ${error}` : name}\n        className={cn(attachmentVariants({ type, status }), className)}\n        {...props}\n      >\n        {src ? (\n          <img src={src} alt={name} className=\"size-full object-cover\" />\n        ) : (\n          <div className=\"flex size-full items-center justify-center text-muted-foreground\">\n            <ImageIcon role=\"img\" aria-label={name} className=\"size-5\" />\n          </div>\n        )}\n        {status === \"loading\" ? (\n          <div className=\"absolute inset-0 flex items-center justify-center bg-popover-overlay text-foreground\">\n            <LoaderCircleIcon className=\"size-4 animate-spin motion-reduce:animate-none\" />\n            <span className=\"sr-only\">Uploading</span>\n          </div>\n        ) : null}\n        {status === \"error\" ? (\n          // The whole tile is the retry target: a third button would not fit 56px.\n          <button\n            type=\"button\"\n            data-slot=\"attachment-retry\"\n            aria-label={`Retry ${name}: ${error}`}\n            disabled={disabled || !onRetry}\n            onClick={onRetry}\n            className=\"absolute inset-0 flex items-center justify-center bg-popover-overlay text-destructive outline-none focus-visible:ring-[3px] focus-visible:ring-ring-subtle focus-visible:ring-inset disabled:cursor-default\"\n          >\n            <CircleAlertIcon className=\"size-4\" />\n          </button>\n        ) : null}\n        {remove}\n      </div>\n    )\n  }\n\n  const Icon =\n    status === \"loading\"\n      ? LoaderCircleIcon\n      : status === \"error\"\n        ? CircleAlertIcon\n        : PaperclipIcon\n  return (\n    <div\n      data-slot=\"attachment\"\n      data-type=\"file\"\n      data-status={status}\n      aria-busy={status === \"loading\" || undefined}\n      className={cn(attachmentVariants({ type, status }), className)}\n      {...props}\n    >\n      <Icon\n        aria-hidden\n        className={cn(\n          \"size-3 shrink-0\",\n          status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n          status === \"loading\" && \"animate-spin motion-reduce:animate-none\"\n        )}\n      />\n      <span className=\"truncate\">{name}</span>\n      {status === \"loading\" ? (\n        <span className=\"shrink-0 text-muted-foreground\">Uploading</span>\n      ) : status === \"error\" ? (\n        <span className=\"shrink-0 text-destructive\">{error}</span>\n      ) : size !== undefined ? (\n        <span className=\"shrink-0 text-muted-foreground\">\n          {formatBytes(size)}\n        </span>\n      ) : null}\n      {status === \"error\" && onRetry ? (\n        <button\n          type=\"button\"\n          data-slot=\"attachment-retry\"\n          aria-label={`Retry ${name}`}\n          disabled={disabled}\n          onClick={onRetry}\n          className={cn(\n            iconButton,\n            \"size-5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n          )}\n        >\n          <Redo2Icon className=\"size-3\" />\n        </button>\n      ) : null}\n      {remove}\n    </div>\n  )\n}\n\n/** Sizes in the units people read them in: 512 B, 12 KB, 3.4 MB. */\nfunction formatBytes(bytes: number) {\n  if (bytes < 1024) return `${bytes} B`\n  const units = [\"KB\", \"MB\", \"GB\"]\n  let size = bytes / 1024\n  let unit = 0\n  while (size >= 1024 && unit < units.length - 1) {\n    size /= 1024\n    unit++\n  }\n  return `${size < 10 ? size.toFixed(1).replace(/\\.0$/, \"\") : Math.round(size)} ${units[unit]}`\n}\n\nexport { Attachment, attachmentVariants, formatBytes, type AttachmentProps }\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "color-popover-overlay": "var(--popover-overlay)",
      "color-ring-subtle": "var(--ring-subtle)",
      "text-color-destructive": "var(--destructive-text)",
      "border-color-destructive": "var(--destructive-border)"
    },
    "light": {
      "popover-overlay": "color-mix(in oklch, var(--color-white) 85%, transparent)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-400) 50%, transparent)",
      "destructive-text": "var(--destructive)",
      "destructive-border": "var(--destructive)"
    },
    "dark": {
      "popover-overlay": "color-mix(in oklch, var(--color-neutral-900) 85%, transparent)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-500) 50%, transparent)",
      "destructive-text": "var(--destructive)",
      "destructive-border": "var(--destructive)"
    }
  },
  "meta": {
    "tier": "base-components",
    "group": "Forms",
    "docs": "https://fibo.toribryan.com/?path=/docs/base-components-forms-attachment--docs",
    "status": "new"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/base-components-forms-attachment--docs",
  "categories": [
    "base-components",
    "forms"
  ],
  "type": "registry:ui"
}