{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachments",
  "title": "Attachments",
  "description": "File and image attachments with progress, failure recovery, metadata, and removal actions.",
  "dependencies": [
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0"
  ],
  "registryDependencies": [
    "button",
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/attachments.tsx",
      "content": "import { FileIcon } from '@solar-icons/react/linear/file'\nimport { FileTextIcon } from '@solar-icons/react/linear/file-text'\nimport { GalleryIcon as ImageIcon } from '@solar-icons/react/linear/gallery'\nimport { RestartIcon as RotateCcwIcon } from '@solar-icons/react/linear/restart'\nimport { CloseIcon as XIcon } from '@solar-icons/react/linear/close'\nimport * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from 'cn'\n\ninterface AttachmentItem {\n  id: string\n  kind?: 'document' | 'file' | 'image' | 'pdf'\n  name: string\n  progress?: number\n  size?: string\n  status?: 'error' | 'ready' | 'uploading'\n  url?: string\n}\n\ninterface AttachmentsProps extends React.ComponentProps<'ul'> {\n  variant?: 'default' | 'uploading' | 'failed'\n  items: AttachmentItem[]\n  onRemove?: (id: string) => void\n  onRetry?: (id: string) => void\n}\n\nfunction Attachments({\n  className,\n  items,\n  onRemove,\n  onRetry,\n  variant = 'default',\n  ...props\n}: AttachmentsProps) {\n  return (\n    <ul\n      aria-label=\"Attachments\"\n      data-slot=\"attachments\"\n      className={cn(\n        'grid gap-3 font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground sm:grid-cols-2',\n        className,\n      )}\n      {...props}\n    >\n      {items.map((attachment) => {\n        const item = {\n          ...attachment,\n          status:\n            attachment.status ??\n            (variant === 'failed' ? 'error' : variant === 'uploading' ? 'uploading' : 'ready'),\n        }\n        return (\n          <li\n            className=\"relative flex min-w-0 items-center gap-3 overflow-hidden rounded-[16px] bg-card p-3 shadow-card transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] starting:scale-[0.97] starting:opacity-0 motion-reduce:starting:scale-100\"\n            key={item.id}\n          >\n            <AttachmentPreview item={item} />\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"truncate card-heading leading-5 text-muted-foreground\">{item.name}</p>\n              <p\n                className={cn(\n                  'mt-0.5 text-xs leading-5 font-[450] text-muted-foreground',\n                  item.status === 'error' && 'text-destructive',\n                )}\n              >\n                {item.status === 'uploading'\n                  ? `${item.progress ?? 0}% uploaded`\n                  : item.status === 'error'\n                    ? 'Upload failed'\n                    : item.size}\n              </p>\n            </div>\n            {item.status === 'error' && (\n              <Button\n                aria-label={`Retry upload: ${item.name}`}\n                type=\"button\"\n                onClick={() => onRetry?.(item.id)}\n                size=\"icon-xs\"\n                variant=\"ghost\"\n              >\n                <RotateCcwIcon />\n              </Button>\n            )}\n            <Button\n              aria-label={`Remove ${item.name}`}\n              type=\"button\"\n              onClick={() => onRemove?.(item.id)}\n              size=\"icon-xs\"\n              variant=\"ghost\"\n            >\n              <XIcon />\n            </Button>\n            {item.status === 'uploading' && (\n              <span className=\"absolute inset-x-0 bottom-0 h-0.5 bg-muted\">\n                <span\n                  className=\"block h-full origin-left bg-foreground transition-[scale] duration-300 ease-linear\"\n                  style={{ scale: `${(item.progress ?? 0) / 100} 1` }}\n                />\n              </span>\n            )}\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nfunction AttachmentPreview({ item }: { item: AttachmentItem }) {\n  if (item.kind === 'image' && item.url) {\n    return (\n      <img\n        alt=\"\"\n        className=\"size-10 shrink-0 rounded-md object-cover\"\n        height={40}\n        src={item.url}\n        width={40}\n      />\n    )\n  }\n\n  const kind = item.kind === 'pdf' || /\\.pdf$/i.test(item.name) ? 'pdf' : (item.kind ?? 'file')\n  const Icon =\n    kind === 'image' ? ImageIcon : kind === 'document' || kind === 'pdf' ? FileTextIcon : FileIcon\n  const colors = {\n    image: 'bg-violet-500/10 text-violet-600 dark:text-violet-400',\n    document: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',\n    pdf: 'bg-red-500/10 text-red-600 dark:text-red-400',\n    file: 'bg-amber-500/10 text-amber-700 dark:text-amber-400',\n  }\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        'flex size-10 shrink-0 flex-col items-center justify-center gap-0.5 rounded-md',\n        colors[kind],\n      )}\n    >\n      <Icon className=\"size-4\" />\n      {kind === 'pdf' && (\n        <span className=\"text-[8px] leading-none font-semibold tracking-wide\">PDF</span>\n      )}\n    </span>\n  )\n}\n\nexport { Attachments }\nexport type { AttachmentItem, AttachmentsProps }\n",
      "type": "registry:component",
      "target": "@components/ai/attachments.tsx"
    }
  ],
  "type": "registry:component"
}