{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment-demo",
  "title": "Attachment Chip demo",
  "description": "A message draft with an upload in flight, a finished deck and a failed CSV with Retry.",
  "registryDependencies": [
    "https://eased.dev/r/attachment.json",
    "https://eased.dev/r/eased-demo-frame.json",
    "https://eased.dev/r/button.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/attachment/attachment-demo.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useAttachments, type Attachment, type UploadFn } from \"../_primitives/chat-core/chat-core\";\nimport { Button } from \"../button/button\";\nimport { DemoFrame } from \"../lib/demo-frame\";\nimport { AttachmentList } from \"./attachment-chip\";\n\n/** A small landscape as an SVG data URL, so the demo ships no image files. */\nexport function scene(sky: string, sun: string, far: string, near: string, sunX = 64) {\n  const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 120 120\"><defs><linearGradient id=\"s\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"${sky}\"/><stop offset=\"1\" stop-color=\"${sun}\" stop-opacity=\".55\"/></linearGradient></defs><rect width=\"120\" height=\"120\" fill=\"url(#s)\"/><circle cx=\"${sunX}\" cy=\"52\" r=\"15\" fill=\"${sun}\"/><path d=\"M0 78 Q30 58 58 74 T120 66 V120 H0Z\" fill=\"${far}\"/><path d=\"M0 96 Q40 76 76 92 T120 88 V120 H0Z\" fill=\"${near}\"/></svg>`;\n  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n}\n\n/**\n * A pretend upload: progress in uneven steps over ~1.5–2.5s (like a real network), failing\n * when the name contains \"fail\". Respects the abort signal.\n */\nexport const fakeUpload: UploadFn = (file, { onProgress, signal }) =>\n  new Promise((resolve, reject) => {\n    let p = 0;\n    const id = window.setInterval(() => {\n      p = Math.min(1, p + 0.06 + Math.random() * 0.14);\n      onProgress(p);\n      if (/fail/i.test(file.name) && p > 0.55) {\n        clearInterval(id);\n        reject(new Error(\"Connection lost\"));\n      } else if (p >= 1) {\n        clearInterval(id);\n        resolve({ url: URL.createObjectURL(file) });\n      }\n    }, 220);\n    signal.addEventListener(\"abort\", () => clearInterval(id));\n  });\n\nconst SEED: Attachment[] = [\n  { id: \"a1\", name: \"harbour-dawn.png\", kind: \"image\", mediaType: \"image/png\", previewUrl: scene(\"#f6d6b8\", \"#f08a5d\", \"#7a8fb0\", \"#3e4f6e\", 70), status: \"done\" },\n  { id: \"a2\", name: \"Q3 board deck.pdf\", kind: \"file\", mediaType: \"application/pdf\", size: 4_812_000, status: \"done\" },\n  { id: \"a3\", name: \"dunes.jpg\", kind: \"image\", mediaType: \"image/jpeg\", previewUrl: scene(\"#fbe7c6\", \"#e9a15a\", \"#d8a46c\", \"#a7683a\", 40), status: \"uploading\", progress: 0.42 },\n  { id: \"a4\", name: \"churn-by-cohort.csv\", kind: \"file\", mediaType: \"text/csv\", size: 218_400, status: \"error\", error: \"Connection lost\" },\n];\n\n/** Uploads in flight, a finished deck, a failed CSV with Retry; add your own files to watch the ring. */\nexport function AttachmentChipDemo() {\n  const input = useRef<HTMLInputElement>(null);\n  const { attachments, add, remove, retry, update } = useAttachments({ upload: fakeUpload, defaultValue: SEED, maxSize: 20_000_000 });\n\n  const timers = useRef<number[]>([]);\n  // Seeded chips have no File, so drive their \"upload\" here.\n  const drive = (id: string, from: number) => {\n    update(id, { status: \"uploading\", progress: from, error: undefined });\n    let p = from;\n    const t = window.setInterval(() => {\n      p = Math.min(1, p + 0.08 + Math.random() * 0.1);\n      update(id, p >= 1 ? { status: \"done\", progress: 1 } : { progress: p });\n      if (p >= 1) clearInterval(t);\n    }, 260);\n    timers.current.push(t);\n  };\n  useEffect(() => {\n    const start = window.setTimeout(() => drive(\"a3\", 0.42), 900);\n    const list = timers.current;\n    return () => {\n      clearTimeout(start);\n      list.forEach(clearInterval);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <DemoFrame title=\"New message\" meta={`${attachments.length} ${attachments.length === 1 ? \"file\" : \"files\"}`}>\n      <div className=\"flex min-h-0 flex-1 flex-col gap-4 p-4\">\n        <div className=\"flex flex-col gap-1\">\n          <p className=\"text-ui-sm text-text-muted\">To</p>\n          <p className=\"text-ui text-text\">Finance team</p>\n        </div>\n        <div className=\"min-h-0 flex-1 overflow-y-auto pt-1.5 [scrollbar-width:thin]\">\n          <AttachmentList\n            attachments={attachments}\n            onRemove={remove}\n            onRetry={(id) => (attachments.find((a) => a.id === id)?.file ? retry(id) : drive(id, 0.05))}\n          />\n        </div>\n        <div className=\"flex items-center gap-2 border-t border-line-subtle pt-4\">\n          <p className=\"min-w-0 flex-1 truncate text-ui-sm text-text-muted\">Up to 20&nbsp;MB per file</p>\n          <Button variant=\"secondary\" size=\"sm\" onClick={() => input.current?.click()}>\n            Add files\n          </Button>\n          <input\n            ref={input}\n            type=\"file\"\n            multiple\n            className=\"sr-only\"\n            tabIndex={-1}\n            aria-hidden\n            onChange={(e) => {\n              add(Array.from(e.target.files ?? []));\n              e.target.value = \"\";\n            }}\n          />\n        </div>\n      </div>\n    </DemoFrame>\n  );\n}\n",
      "type": "registry:example",
      "target": "@components/eased/attachment/attachment-demo.tsx"
    }
  ],
  "type": "registry:example"
}