{
  "schemaVersion": 1,
  "name": "attachment",
  "useFor": "Show a file attached to an email, a message or a draft. Set state to idle while it waits, uploading or processing while it moves, error when it failed (say why in AttachmentDescription), and done when it is ready. Add AttachmentTrigger to open the file and AttachmentAction for remove or retry. Lay several out in an AttachmentGroup, which scrolls sideways.",
  "constraints": [
    "A colored card behind the file",
    "Sizes in Inter",
    "A spinner over the whole card",
    "An icon that moves on hover"
  ],
  "documentation": "",
  "source": "Extrapolated from the Inkbox console (Figma 427:2715): the webhook cards (white, field edge, 8px corners) and the pink channel tiles of Create Agent Identity",
  "exports": [
    "Attachment",
    "AttachmentAction",
    "AttachmentActions",
    "AttachmentContent",
    "AttachmentDescription",
    "AttachmentGroup",
    "AttachmentMedia",
    "AttachmentTitle",
    "AttachmentTrigger"
  ],
  "client": false,
  "docsUrl": "https://inkbox-ds.vercel.app/components/attachment",
  "markdownUrl": "https://inkbox-ds.vercel.app/r/attachment.md",
  "examples": [
    {
      "path": "examples/attachment.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowClockwiseIcon, DownloadSimpleIcon, FileCsvIcon, FilePdfIcon, FileTextIcon, ImageIcon, XIcon } from \"@phosphor-icons/react/ssr\";\n\nimport { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger } from \"@/components/ui/attachment\";\n\nexport function Preview() {\n  const [files, setFiles] = React.useState([\n    { name: \"invoice-2026-08.pdf\", size: \"248 KB\", icon: FilePdfIcon },\n    { name: \"contacts-export.csv\", size: \"1.2 MB\", icon: FileCsvIcon },\n    { name: \"agent-card.json\", size: \"3 KB\", icon: FileTextIcon },\n  ]);\n  return (\n    <AttachmentGroup className=\"max-w-full\">\n      {files.map(({ name, size, icon: Icon }) => (\n        <Attachment key={name}>\n          <AttachmentTrigger aria-label={`Open ${name}`} />\n          <AttachmentMedia>\n            <Icon weight=\"fill\" />\n          </AttachmentMedia>\n          <AttachmentContent>\n            <AttachmentTitle>{name}</AttachmentTitle>\n            <AttachmentDescription>{size}</AttachmentDescription>\n          </AttachmentContent>\n          <AttachmentActions>\n            <AttachmentAction aria-label={`Remove ${name}`} onClick={() => setFiles((all) => all.filter((f) => f.name !== name))}>\n              <XIcon />\n            </AttachmentAction>\n          </AttachmentActions>\n        </Attachment>\n      ))}\n    </AttachmentGroup>\n  );\n}\n\nexport function States() {\n  return (\n    <div className=\"flex flex-wrap items-start gap-3\">\n      <Attachment state=\"idle\">\n        <AttachmentMedia>\n          <FileTextIcon weight=\"fill\" />\n        </AttachmentMedia>\n        <AttachmentContent>\n          <AttachmentTitle>quickstart.md</AttachmentTitle>\n          <AttachmentDescription>Waiting</AttachmentDescription>\n        </AttachmentContent>\n      </Attachment>\n      <Attachment state=\"uploading\">\n        <AttachmentMedia>\n          <FilePdfIcon weight=\"fill\" />\n        </AttachmentMedia>\n        <AttachmentContent>\n          <AttachmentTitle>contract-signed.pdf</AttachmentTitle>\n          <AttachmentDescription>62% of 4.8 MB</AttachmentDescription>\n        </AttachmentContent>\n      </Attachment>\n      <Attachment state=\"error\">\n        <AttachmentMedia>\n          <FileCsvIcon weight=\"fill\" />\n        </AttachmentMedia>\n        <AttachmentContent>\n          <AttachmentTitle>leads.csv</AttachmentTitle>\n          <AttachmentDescription>Larger than 25 MB</AttachmentDescription>\n        </AttachmentContent>\n        <AttachmentActions>\n          <AttachmentAction aria-label=\"Retry leads.csv\">\n            <ArrowClockwiseIcon />\n          </AttachmentAction>\n        </AttachmentActions>\n      </Attachment>\n    </div>\n  );\n}\n\nexport function Vertical() {\n  return (\n    <div className=\"flex flex-wrap items-start gap-3\">\n      <Attachment orientation=\"vertical\">\n        <AttachmentMedia variant=\"image\">\n          {/* eslint-disable-next-line @next/next/no-img-element */}\n          <img src=\"/octopus.png\" alt=\"\" />\n        </AttachmentMedia>\n        <AttachmentContent>\n          <AttachmentTitle>octopus.png</AttachmentTitle>\n          <AttachmentDescription>94 KB</AttachmentDescription>\n        </AttachmentContent>\n        <AttachmentActions>\n          <AttachmentAction aria-label=\"Download octopus.png\">\n            <DownloadSimpleIcon />\n          </AttachmentAction>\n        </AttachmentActions>\n      </Attachment>\n      <Attachment orientation=\"vertical\" state=\"processing\">\n        <AttachmentMedia>\n          <ImageIcon weight=\"fill\" />\n        </AttachmentMedia>\n        <AttachmentContent>\n          <AttachmentTitle>screenshot.png</AttachmentTitle>\n          <AttachmentDescription>Scanning</AttachmentDescription>\n        </AttachmentContent>\n      </Attachment>\n    </div>\n  );\n}\n\nexport function Sizes() {\n  return (\n    <div className=\"flex flex-wrap items-center gap-3\">\n      {([\"default\", \"sm\", \"xs\"] as const).map((size) => (\n        <Attachment key={size} size={size}>\n          <AttachmentMedia>\n            <FilePdfIcon weight=\"fill\" />\n          </AttachmentMedia>\n          <AttachmentContent>\n            <AttachmentTitle>invoice.pdf</AttachmentTitle>\n            <AttachmentDescription>248 KB</AttachmentDescription>\n          </AttachmentContent>\n        </Attachment>\n      ))}\n    </div>\n  );\n}\n",
      "exports": [
        "Preview",
        "States",
        "Vertical",
        "Sizes"
      ],
      "requires": [
        "attachment"
      ],
      "packages": [
        "@phosphor-icons/react"
      ],
      "previews": [
        {
          "name": "Preview",
          "url": "https://inkbox-ds.vercel.app/preview/attachment/Preview"
        },
        {
          "name": "States",
          "url": "https://inkbox-ds.vercel.app/preview/attachment/States"
        },
        {
          "name": "Vertical",
          "url": "https://inkbox-ds.vercel.app/preview/attachment/Vertical"
        },
        {
          "name": "Sizes",
          "url": "https://inkbox-ds.vercel.app/preview/attachment/Sizes"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Attachment",
  "description": "A white file card with the field edge: the file in a pink tile, its name in 14px medium ink and its size in IBM Plex Mono.",
  "surface": "site",
  "kinds": [],
  "requires": [
    "theme",
    "button",
    "utils"
  ],
  "packages": [
    "radix-ui",
    "class-variance-authority"
  ],
  "files": [
    {
      "path": "components/ui/attachment.tsx",
      "content": "import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\n// A file attached to a message or an email, drawn as the console's cards (Figma 427:2715): white with the field edge\n// and 8px corners. The file sits in a pink tile with a filled glyph in rose (the channel tiles of Create Agent\n// Identity); its name is 14px medium ink and its size or status 12px muted in IBM Plex Mono. A file still waiting has a\n// dashed edge, one uploading pulses its name softly, one that failed takes the red edge and a red line.\nconst attachmentVariants = cva(\n  \"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-control border border-line-field bg-surface text-fg transition-[background-color,border-color] duration-150 ease-out has-[>[data-slot=attachment-trigger]]:hover:bg-subtle has-[>[data-slot=attachment-trigger]:focus-visible]:outline-2 has-[>[data-slot=attachment-trigger]:focus-visible]:outline-offset-2 has-[>[data-slot=attachment-trigger]:focus-visible]:outline-focus-ring data-[state=error]:border-danger data-[state=idle]:border-dashed motion-reduce:transition-none\",\n  {\n    variants: {\n      size: {\n        default: \"gap-3 text-body has-data-[slot=attachment-content]:px-3 has-data-[slot=attachment-content]:py-2.5 has-data-[slot=attachment-media]:p-2.5\",\n        sm: \"gap-2.5 text-body has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2\",\n        xs: \"gap-2 rounded-sm text-body-sm has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5\",\n      },\n      orientation: {\n        horizontal: \"min-w-48 items-center\",\n        vertical: \"w-28 flex-col has-data-[slot=attachment-content]:w-36\",\n      },\n    },\n    defaultVariants: { size: \"default\", orientation: \"horizontal\" },\n  },\n);\n\ntype AttachmentState = \"idle\" | \"uploading\" | \"processing\" | \"error\" | \"done\";\n\nfunction Attachment({ className, state = \"done\", size = \"default\", orientation = \"horizontal\", ...props }: React.ComponentProps<\"div\"> & VariantProps<typeof attachmentVariants> & { state?: AttachmentState }) {\n  return <div data-slot=\"attachment\" data-state={state} data-size={size} data-orientation={orientation} aria-busy={state === \"uploading\" || state === \"processing\" || undefined} className={cn(attachmentVariants({ size, orientation }), className)} {...props} />;\n}\n\nconst attachmentMediaVariants = cva(\n  \"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-sm bg-pink text-rose group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-xs group-data-[state=error]/attachment:bg-subtle group-data-[state=error]/attachment:text-danger [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-5.5 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-7 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        icon: \"\",\n        // A photo shows itself; it stays faint until it has uploaded.\n        image: \"bg-subtle opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover\",\n      },\n    },\n    defaultVariants: { variant: \"icon\" },\n  },\n);\n\nfunction AttachmentMedia({ className, variant = \"icon\", ...props }: React.ComponentProps<\"div\"> & VariantProps<typeof attachmentMediaVariants>) {\n  return <div data-slot=\"attachment-media\" data-variant={variant} className={cn(attachmentMediaVariants({ variant }), className)} {...props} />;\n}\n\nfunction AttachmentContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"attachment-content\" className={cn(\"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1 group-data-[orientation=vertical]/attachment:pb-1\", className)} {...props} />;\n}\n\nfunction AttachmentTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"attachment-title\"\n      className={cn(\"block max-w-full min-w-0 truncate font-sans font-medium text-fg group-data-[state=processing]/attachment:animate-pulse-soft group-data-[state=uploading]/attachment:animate-pulse-soft\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AttachmentDescription({ className, ...props }: React.ComponentProps<\"span\">) {\n  return <span data-slot=\"attachment-description\" className={cn(\"mt-1 block max-w-full min-w-0 truncate font-mono text-caption leading-tight text-fg-muted group-data-[state=error]/attachment:font-sans group-data-[state=error]/attachment:text-danger\", className)} {...props} />;\n}\n\nfunction AttachmentActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"attachment-actions\" className={cn(\"relative z-20 flex shrink-0 items-center gap-0.5 group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3.5 group-data-[orientation=vertical]/attachment:right-3.5\", className)} {...props} />;\n}\n\n/** A row action on the attachment (remove, retry, download): a ghost glyph button. */\nfunction AttachmentAction({ className, variant, size = \"icon-sm\", ...props }: React.ComponentProps<typeof Button>) {\n  return <Button data-slot=\"attachment-action\" variant={variant ?? \"ghost\"} size={size} className={cn(\"group-data-[orientation=vertical]/attachment:bg-surface group-data-[orientation=vertical]/attachment:shadow-s\", className)} {...props} />;\n}\n\n/** Makes the whole card open the file: a button (or a link with asChild) stretched over it. */\nfunction AttachmentTrigger({ className, asChild = false, type, ...props }: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n  return <Comp data-slot=\"attachment-trigger\" type={asChild ? undefined : (type ?? \"button\")} className={cn(\"absolute inset-0 z-10 cursor-pointer rounded-control outline-none\", className)} {...props} />;\n}\n\nfunction AttachmentGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"attachment-group\"\n      className={cn(\"flex min-w-0 snap-x snap-mandatory scroll-px-1 gap-3 overflow-x-auto overscroll-x-contain p-1 [scrollbar-width:none] *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger };\n"
    }
  ]
}
