{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "registryDependencies": [
    "utils",
    "https://turnui.xyz/r/use-attachments.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/attachment.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Structurally compatible with the items returned by useAttachments. */\nexport type AttachmentData = {\n  name: string;\n  size: number;\n  previewUrl?: string;\n  status: \"uploading\" | \"done\" | \"error\";\n  progress: number;\n  error?: string;\n};\n\nexport function formatBytes(bytes: number) {\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;\n  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n}\n\nfunction RemoveButton({ name, onRemove, className }: { name: string; onRemove: () => void; className?: string }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onRemove}\n      aria-label={`Remove ${name}`}\n      className={cn(\n        \"flex size-5 items-center justify-center rounded-sm bg-foreground text-background outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    >\n      <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"size-2.5\"><path d=\"m4 4 8 8M12 4l-8 8\" /></svg>\n    </button>\n  );\n}\n\nfunction Progress({ value, label, className }: { value: number; label: string; className?: string }) {\n  return (\n    <div role=\"progressbar\" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={value} className={cn(\"h-0.5 overflow-hidden rounded-none bg-border\", className)}>\n      <div className=\"h-full bg-primary transition-[width] duration-200 motion-reduce:transition-none\" style={{ width: `${value}%` }} />\n    </div>\n  );\n}\n\nexport function Attachment({\n  item,\n  onRemove,\n  className,\n}: {\n  item: AttachmentData;\n  onRemove?: () => void;\n  className?: string;\n}) {\n  const failed = item.status === \"error\";\n  const uploading = item.status === \"uploading\";\n\n  if (item.previewUrl) {\n    return (\n      <div className={cn(\"group relative size-16 shrink-0\", className)} title={failed ? item.error : item.name}>\n        {/* eslint-disable-next-line @next/next/no-img-element */}\n        <img\n          src={item.previewUrl}\n          alt={item.name}\n          className={cn(\"size-full rounded border object-cover\", uploading && \"opacity-60\", failed && \"border-red-500\")}\n        />\n        {uploading && <Progress value={item.progress} label={`Uploading ${item.name}`} className=\"absolute inset-x-1.5 bottom-1.5\" />}\n        {onRemove && (\n          <RemoveButton name={item.name} onRemove={onRemove} className=\"absolute -right-1.5 -top-1.5 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100\" />\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"group relative flex h-16 w-44 shrink-0 items-center gap-2.5 overflow-hidden rounded border bg-muted/40 px-2.5\",\n        failed && \"border-red-500/60\",\n        className,\n      )}\n    >\n      <div className={cn(\"flex size-9 shrink-0 items-center justify-center rounded-sm bg-muted text-muted-foreground\", failed && \"text-red-500\")}>\n        {(() => {\n          const ext = /\\.([a-z0-9]{1,4})$/i.exec(item.name)?.[1];\n          return ext ? <span aria-hidden className=\"font-mono text-[10px] font-medium uppercase\">{ext}</span> : (<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-4\"><path d=\"M9.5 1.75H4.75a1.5 1.5 0 0 0-1.5 1.5v9.5a1.5 1.5 0 0 0 1.5 1.5h6.5a1.5 1.5 0 0 0 1.5-1.5V5l-3.25-3.25Z\" /><path d=\"M9.5 1.75V5h3.25\" /></svg>);\n        })()}\n      </div>\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"truncate font-mono text-xs\" title={item.name}>{item.name}</div>\n        <div className={cn(\"truncate text-xs\", failed ? \"text-red-500\" : \"text-muted-foreground\")} title={failed ? item.error : undefined} aria-live=\"polite\">\n          {failed ? item.error : uploading ? `${item.progress}%` : formatBytes(item.size)}\n        </div>\n      </div>\n      {uploading && <Progress value={item.progress} label={`Uploading ${item.name}`} className=\"absolute inset-x-0 bottom-0 rounded-none\" />}\n      {onRemove && (\n        <RemoveButton name={item.name} onRemove={onRemove} className=\"absolute right-1.5 top-1.5 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100\" />\n      )}\n    </div>\n  );\n}\n\n/** Horizontal, scrollable row of attachments. */\nexport function AttachmentList({\n  items,\n  onRemove,\n  className,\n}: {\n  items: (AttachmentData & { id: string })[];\n  onRemove?: (id: string) => void;\n  className?: string;\n}) {\n  const listRef = React.useRef<HTMLUListElement>(null);\n  if (!items.length) return null;\n\n  // The focused Remove button disappears with its item, so move focus to a neighbour\n  // (or the message input when the list is empty) instead of dropping it on <body>.\n  function remove(index: number, id: string) {\n    const form = listRef.current?.closest(\"form\");\n    onRemove?.(id);\n    requestAnimationFrame(() => {\n      const buttons = listRef.current?.querySelectorAll<HTMLElement>('button[aria-label^=\"Remove\"]');\n      if (buttons?.length) buttons[Math.min(index, buttons.length - 1)].focus();\n      else form?.querySelector(\"textarea\")?.focus();\n    });\n  }\n\n  return (\n    <ul ref={listRef} className={cn(\"flex gap-2 overflow-x-auto px-1 pb-1 pt-2\", className)} aria-label=\"Attachments\">\n      {items.map((item, i) => (\n        <li key={item.id} className=\"shrink-0\">\n          <Attachment item={item} onRemove={onRemove && (() => remove(i, item.id))} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}