{
  "schemaVersion": 1,
  "target": "react",
  "name": "artifact-preview",
  "version": "0.2.0",
  "category": "agent",
  "title": "Artifact Preview",
  "description": "A file an agent produced: name, type, size, status, an inline preview, and where to open it.",
  "tags": [
    "agent",
    "artifact",
    "file",
    "preview",
    "download"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "badge",
      "version": "0.2.0"
    },
    {
      "name": "agent-run",
      "version": "0.1.1"
    }
  ],
  "docsPath": "/components/artifact-preview",
  "props": [
    {
      "name": "name / type",
      "type": "string",
      "description": "File name and MIME type."
    },
    {
      "name": "status",
      "type": "\"generating\" | \"ready\" | \"failed\"",
      "description": "Generating shows a static placeholder and a spinner; failed shows the error."
    },
    {
      "name": "path",
      "type": "string",
      "description": "Where to open or download the file. Images preview from this path."
    },
    {
      "name": "content",
      "type": "string",
      "description": "Text to preview for text and JSON types, rendered as text."
    },
    {
      "name": "sizeBytes / error",
      "type": "number / string",
      "description": "Optional size and failure message."
    }
  ],
  "accessibility": [
    "Status is a text badge in a live region.",
    "The download link names the file for screen readers.",
    "The generating placeholder is decorative and static; only its small spinner turns, and it holds still with reduced motion."
  ],
  "states": [
    "generating",
    "ready",
    "failed"
  ],
  "files": [
    {
      "path": "agent/artifact-preview.tsx",
      "content": "import type { ComponentProps } from \"react\";\nimport { Badge, type BadgeTone } from \"../ui/badge\";\nimport type { ArtifactStatus } from \"./agent-run\";\n\nconst statuses: Record<ArtifactStatus, { label: string; tone: BadgeTone }> = {\n  generating: { label: \"Generating\", tone: \"info\" },\n  ready: { label: \"Ready\", tone: \"success\" },\n  failed: { label: \"Failed\", tone: \"danger\" },\n};\n\nconst enter =\n  \"transition-[opacity,translate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none\";\n\nexport interface ArtifactPreviewProps extends Omit<ComponentProps<\"article\">, \"children\"> {\n  name: string;\n  /** MIME type, e.g. text/markdown, text/csv, application/json, image/png. */\n  type: string;\n  status: ArtifactStatus;\n  /** Where to open or download the artifact once it is ready. */\n  path?: string;\n  /** Text content to preview for text-like types. Rendered as text, never HTML. */\n  content?: string;\n  sizeBytes?: number;\n  error?: string;\n}\n\n/** A file an agent produced: name, type, size, status, a preview, and where to open it. */\nexport function ArtifactPreview({ name, type, status, path, content, sizeBytes, error, className, ...props }: ArtifactPreviewProps) {\n  const meta = statuses[status];\n  const isImage = type.startsWith(\"image/\");\n  const isText = type.startsWith(\"text/\") || type === \"application/json\";\n\n  return (\n    <article\n      data-status={status}\n      aria-busy={status === \"generating\" || undefined}\n      className={[\n        \"overflow-hidden rounded-xl border bg-white text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04)] transition-[border-color] duration-200 motion-reduce:transition-none\",\n        \"dark:bg-black dark:text-white dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]\",\n        \"border-zinc-950/10 dark:border-white/10\",\n        className,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      {...props}\n    >\n      <header className=\"flex items-center gap-3 px-4 py-3\">\n        <FileIcon extension={extensionOf(name)} />\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"truncate text-sm font-medium\">{name}</h3>\n          <p className=\"truncate font-mono text-xs text-zinc-500\">\n            {type}\n            {sizeBytes != null ? ` · ${formatSize(sizeBytes)}` : \"\"}\n          </p>\n        </div>\n        <span role=\"status\" className=\"shrink-0\">\n          <Badge tone={meta.tone}>{meta.label}</Badge>\n        </span>\n      </header>\n\n      <div key={status} className={`border-t border-zinc-950/[0.07] p-4 dark:border-white/[0.08] ${enter}`}>\n        {status === \"generating\" ? (\n          <div className=\"grid gap-3\">\n            <div aria-hidden=\"true\" className=\"grid gap-2\">\n              {[\"w-11/12\", \"w-4/5\", \"w-3/5\"].map((width) => (\n                <div key={width} className={`h-2 rounded-full bg-zinc-950/[0.06] dark:bg-white/[0.08] ${width}`} />\n              ))}\n            </div>\n            <p className=\"flex items-center gap-2 text-xs text-zinc-500\">\n              <Spinner />\n              Writing the file\n            </p>\n          </div>\n        ) : status === \"failed\" ? (\n          <p className=\"flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300\">\n            <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" className=\"mt-0.5 size-4 shrink-0 text-red-600 dark:text-red-400\">\n              <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n              <path d=\"m6.1 6.1 3.8 3.8m0-3.8-3.8 3.8\" />\n            </svg>\n            <span>{error ?? \"The artifact could not be generated.\"}</span>\n          </p>\n        ) : isImage && path ? (\n          <img\n            src={path}\n            alt={`Preview of ${name}`}\n            className=\"max-h-64 rounded-lg border border-zinc-950/[0.07] object-contain dark:border-white/[0.08]\"\n          />\n        ) : isText && content != null ? (\n          <pre\n            className={`max-h-64 overflow-auto rounded-lg ${type === \"text/markdown\" || type === \"text/plain\" ? \"break-words whitespace-pre-wrap\" : \"\"} border border-zinc-950/[0.07] bg-zinc-950/[0.02] px-3 py-2.5 font-mono text-xs leading-relaxed text-zinc-800 dark:border-white/[0.08] dark:bg-transparent dark:text-zinc-300`}\n          >\n            {content}\n          </pre>\n        ) : (\n          <p className=\"text-sm text-zinc-500\">No preview for this file type.</p>\n        )}\n      </div>\n\n      {status === \"ready\" && path ? (\n        <footer className=\"flex items-center justify-between gap-3 border-t border-zinc-950/[0.07] px-4 py-2.5 dark:border-white/[0.08]\">\n          <code className=\"min-w-0 truncate font-mono text-xs text-zinc-500\">{path}</code>\n          <a\n            href={path}\n            download={name}\n            className={[\n              \"inline-flex h-7 shrink-0 items-center gap-1.5 rounded-lg border border-zinc-950/12 px-2.5 text-xs font-medium text-zinc-800\",\n              \"transition-colors duration-150 hover:border-zinc-950/20 hover:bg-zinc-950/[0.03] motion-reduce:transition-none\",\n              \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500\",\n              \"dark:border-white/15 dark:text-zinc-200 dark:hover:border-white/25 dark:hover:bg-white/[0.06]\",\n            ].join(\" \")}\n          >\n            <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-3.5\">\n              <path d=\"M8 2.75v7.5M4.75 7 8 10.25 11.25 7M3 13.25h10\" />\n            </svg>\n            Download<span className=\"sr-only\"> {name}</span>\n          </a>\n        </footer>\n      ) : null}\n    </article>\n  );\n}\n\n/** A file tile: the extension in mono when there is one, otherwise a page outline. */\nfunction FileIcon({ extension }: { extension: string }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"grid size-9 shrink-0 place-items-center rounded-lg border border-zinc-950/10 font-mono text-[11px] font-medium text-zinc-600 dark:border-white/10 dark:text-zinc-400\"\n    >\n      {extension || (\n        <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" className=\"size-5\">\n          <path d=\"M5.5 2.75h6l3 3v11.5h-9z\" />\n          <path d=\"M11.5 2.75v3h3\" />\n        </svg>\n      )}\n    </span>\n  );\n}\n\nfunction Spinner() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" className=\"size-3.5 shrink-0 text-sky-600 dark:text-sky-400\">\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" strokeOpacity=\"0.2\" />\n      <path d=\"M8 1.75A6.25 6.25 0 1 1 1.75 8\" className=\"origin-center motion-safe:animate-spin\" />\n    </svg>\n  );\n}\n\nfunction extensionOf(name: string): string {\n  const match = name.match(/\\.([a-z0-9]{1,4})$/i);\n  return match ? match[1]!.toLowerCase() : \"\";\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n}\n"
    }
  ],
  "example": {
    "content": "import { ArtifactPreview } from \"@/components/seamless/agent/artifact-preview\";\n\nexport default function ArtifactPreviewExample() {\n  return (\n    <ArtifactPreview\n      className=\"w-full max-w-md\"\n      name=\"rate-limits.csv\"\n      type=\"text/csv\"\n      status=\"ready\"\n      path=\"/artifacts/rate-limits.csv\"\n      sizeBytes={58}\n      content={\"plan,requests_per_minute\\nfree,60\\npro,600\"}\n    />\n  );\n}\n"
  }
}
