{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "artifact-card",
  "type": "registry:ui",
  "title": "Artifact Card",
  "description": "Card for something an agent made — document, code, web page, image, sheet or diagram — with generating/updating progress, versions and open-in-panel.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/icon-button.json",
    "https://kuro.abstergo.fyi/r/shimmer-text.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/artifact-card/artifact-card.tsx",
      "type": "registry:ui",
      "target": "components/kura/artifact-card/artifact-card.tsx",
      "content": "\"use client\";\n\nimport {\n  AppWindow,\n  ChevronLeft,\n  ChevronRight,\n  File,\n  FileCode2,\n  FileText,\n  Image,\n  RotateCw,\n  Table2,\n  Workflow,\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\nimport { IconButton } from \"@/components/kura/icon-button\";\nimport { ShimmerText } from \"@/components/kura/shimmer-text\";\n\nexport type ArtifactKind = \"document\" | \"code\" | \"web\" | \"image\" | \"data\" | \"diagram\" | \"file\";\nexport type ArtifactStatus = \"generating\" | \"updating\" | \"ready\" | \"error\";\n\nexport interface ArtifactCardProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n  /** The artifact's name, e.g. \"Q3 launch plan\" or \"pricing-table.tsx\". */\n  title: string;\n  /** What the agent made. Picks the icon, the spoken label and the preview placeholder. Default \"document\". */\n  kind?: ArtifactKind;\n  /** Lifecycle. generating = first write; updating = an edit to an existing artifact. Default \"ready\". */\n  status?: ArtifactStatus;\n  /** Secondary line when ready, e.g. \"Markdown · 1,240 words\". Default: the kind's label. */\n  meta?: React.ReactNode;\n  /** Live line while generating/updating, e.g. \"Writing section 3 of 5…\". Default \"Generating…\" / \"Updating…\". */\n  statusText?: React.ReactNode;\n  /** Message shown when status=\"error\". Default \"Couldn't finish\". */\n  error?: React.ReactNode;\n  /** Completion 0–1 while busy. Omit for an indeterminate sweep. */\n  progress?: number;\n  /** Replaces the kind icon. */\n  icon?: React.ReactNode;\n  /** `compact` = a one-row chip for message text (default); `preview` = a card with a thumbnail area. */\n  variant?: \"compact\" | \"preview\";\n  /** Thumbnail content for variant=\"preview\" (a text excerpt, an image, a CodeBlock…). Decorative: it is hidden from screen readers. */\n  preview?: React.ReactNode;\n  /** Number of versions. Shows a ‹ v2/3 › switcher when greater than 1. */\n  versions?: number;\n  /** Shown version, 1-based (controlled). */\n  version?: number;\n  /** Initial version when uncontrolled. Default: the latest (`versions`). */\n  defaultVersion?: number;\n  /** Called with the new 1-based version from the switcher. */\n  onVersionChange?: (version: number) => void;\n  /** Open the artifact (e.g. in a side panel). Makes the whole card a button. */\n  onOpen?: () => void;\n  /** Open the artifact by navigating instead. Makes the whole card a link. */\n  href?: string;\n  /** The artifact is the one currently open, e.g. in the side panel. */\n  selected?: boolean;\n  /** Shows a Retry button when status=\"error\". */\n  onRetry?: () => void;\n  /** Extra buttons at the end of the row, e.g. Copy / Download IconButtons (size \"sm\"). */\n  actions?: React.ReactNode;\n}\n\nconst KINDS: Record<ArtifactKind, { label: string; icon: React.ReactNode }> = {\n  document: { label: \"Document\", icon: <FileText /> },\n  code: { label: \"Code\", icon: <FileCode2 /> },\n  web: { label: \"Web page\", icon: <AppWindow /> },\n  image: { label: \"Image\", icon: <Image /> },\n  data: { label: \"Spreadsheet\", icon: <Table2 /> },\n  diagram: { label: \"Diagram\", icon: <Workflow /> },\n  file: { label: \"File\", icon: <File /> },\n};\n\n/** Spoken label for a kind, e.g. \"Web page\". */\nexport function artifactKindLabel(kind: ArtifactKind): string {\n  return KINDS[kind].label;\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\n/**\n * A card for something an agent made — a document, a file of code, a web page, an image, a sheet\n * or a diagram. Shows what it is, whether it is still being written, which version you are on, and\n * opens it (usually into a side panel) on click.\n */\nexport function ArtifactCard({\n  title,\n  kind = \"document\",\n  status = \"ready\",\n  meta,\n  statusText,\n  error,\n  progress,\n  icon,\n  variant = \"compact\",\n  preview,\n  versions = 1,\n  version: versionProp,\n  defaultVersion,\n  onVersionChange,\n  onOpen,\n  href,\n  selected = false,\n  onRetry,\n  actions,\n  className,\n  ...props\n}: ArtifactCardProps) {\n  const [version, setVersion] = useControllableState({\n    value: versionProp,\n    defaultValue: defaultVersion ?? versions,\n    onChange: onVersionChange,\n  });\n  const k = KINDS[kind];\n  const busy = status === \"generating\" || status === \"updating\";\n  const compact = variant === \"compact\";\n  const openable = onOpen != null || href != null;\n  const pct = progress == null ? null : Math.round(clamp01(progress) * 100);\n  const shownVersion = Math.min(Math.max(1, version), Math.max(1, versions));\n\n  const announcement =\n    status === \"generating\"\n      ? `Generating ${title}`\n      : status === \"updating\"\n        ? `Updating ${title}`\n        : status === \"error\"\n          ? `${title} failed`\n          : `${title} ready`;\n\n  const titleClass =\n    \"min-w-0 truncate text-left font-medium text-kura-fg text-sm leading-5 outline-none\";\n  // The open control stretches over the whole card (after: overlay), so the card is one target\n  // while the version switcher and actions stay separately clickable above it.\n  const stretch =\n    \"after:absolute after:inset-0 after:z-0 after:rounded-[inherit] after:content-['']\";\n  const spoken = (\n    <span className=\"sr-only\">\n      , {k.label}\n      {busy ? `, ${status === \"generating\" ? \"generating\" : \"updating\"}` : \"\"}\n      {status === \"error\" ? \", failed\" : \"\"}\n    </span>\n  );\n\n  let titleNode: React.ReactNode;\n  if (href != null) {\n    titleNode = (\n      <a\n        href={href}\n        data-slot=\"artifact-card-open\"\n        aria-current={selected ? \"true\" : undefined}\n        className={cn(titleClass, stretch)}\n      >\n        {title}\n        {spoken}\n      </a>\n    );\n  } else if (onOpen != null) {\n    titleNode = (\n      <button\n        type=\"button\"\n        data-slot=\"artifact-card-open\"\n        aria-current={selected ? \"true\" : undefined}\n        onClick={onOpen}\n        className={cn(titleClass, stretch, \"cursor-pointer\")}\n      >\n        {title}\n        {spoken}\n      </button>\n    );\n  } else {\n    titleNode = (\n      <span data-slot=\"artifact-card-title\" className={titleClass}>\n        {title}\n        {spoken}\n      </span>\n    );\n  }\n\n  const progressBar = busy && (\n    <div\n      data-slot=\"artifact-card-progress\"\n      role=\"progressbar\"\n      aria-label={`${status === \"generating\" ? \"Generating\" : \"Updating\"} ${title}`}\n      aria-valuemin={pct == null ? undefined : 0}\n      aria-valuemax={pct == null ? undefined : 100}\n      aria-valuenow={pct ?? undefined}\n      className=\"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-0.5 overflow-hidden bg-kura-accent-subtle\"\n    >\n      {pct == null ? (\n        <div\n          className=\"h-full animate-kura-shimmer [background-size:250%_100%] motion-reduce:animate-none motion-reduce:bg-none\"\n          style={\n            {\n              \"--kura-shimmer-duration\": \"1.4s\",\n              backgroundImage:\n                \"linear-gradient(90deg, transparent 30%, var(--kura-accent) 50%, transparent 70%)\",\n            } as React.CSSProperties\n          }\n        />\n      ) : (\n        <div\n          className=\"h-full origin-left bg-kura-accent transition-transform duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-none\"\n          style={{ transform: `scaleX(${pct / 100})` }}\n        />\n      )}\n    </div>\n  );\n\n  return (\n    <div\n      data-slot=\"artifact-card\"\n      data-kind={kind}\n      data-status={status}\n      data-variant={variant}\n      data-selected={selected || undefined}\n      aria-busy={busy || undefined}\n      className={cn(\n        \"group/artifact relative isolate flex min-w-0 flex-col overflow-hidden rounded-kura-xl bg-kura-surface font-kura-sans shadow-kura-sm\",\n        \"transition-[box-shadow,transform] duration-(--kura-duration-fast) ease-kura-out motion-reduce:transition-none\",\n        compact ? \"w-fit max-w-full\" : \"w-full\",\n        openable &&\n          \"hover:shadow-kura-md has-[[data-slot=artifact-card-open]:active]:scale-[0.99] motion-reduce:has-[[data-slot=artifact-card-open]:active]:scale-100\",\n        selected &&\n          \"shadow-[0_0_0_1px_var(--kura-border-strong),0_0_0_4px_var(--kura-hover)] hover:shadow-[0_0_0_1px_var(--kura-border-strong),0_0_0_4px_var(--kura-hover)]\",\n        \"has-[[data-slot=artifact-card-open]:focus-visible]:shadow-kura-focus\",\n        className,\n      )}\n      {...props}\n    >\n      {!compact && (\n        <div className=\"relative\">\n          <div\n            data-slot=\"artifact-card-preview\"\n            aria-hidden\n            inert\n            className=\"relative h-36 overflow-hidden bg-kura-surface-sunken shadow-[inset_0_-1px_0_var(--kura-border-subtle)]\"\n          >\n            <div\n              className={cn(\n                \"absolute inset-0 origin-top [mask-image:linear-gradient(to_bottom,black_62%,transparent)]\",\n                \"transition-[transform,opacity] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-opacity\",\n                openable &&\n                  \"group-hover/artifact:scale-[1.015] motion-reduce:group-hover/artifact:scale-100\",\n                status === \"updating\" && \"opacity-60\",\n              )}\n            >\n              {preview ?? <PreviewPlaceholder kind={kind} busy={busy} />}\n            </div>\n          </div>\n          {progressBar}\n        </div>\n      )}\n\n      <div\n        data-slot=\"artifact-card-header\"\n        className={cn(\"flex min-w-0 items-center gap-3\", compact ? \"p-2 pr-2.5\" : \"px-3 py-2.5\")}\n      >\n        <span\n          aria-hidden\n          data-slot=\"artifact-card-icon\"\n          className={cn(\n            \"relative grid size-9 shrink-0 place-items-center rounded-kura-md bg-kura-gray-3 text-kura-fg-muted shadow-[inset_0_0_0_1px_var(--kura-border-subtle)] [&_svg]:size-4\",\n            \"transition-colors duration-(--kura-duration-base) ease-kura-out\",\n            busy && \"bg-kura-accent-subtle text-kura-accent-text\",\n            status === \"error\" && \"bg-kura-danger-subtle text-kura-danger-text\",\n          )}\n        >\n          {icon ?? k.icon}\n        </span>\n\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          {titleNode}\n          <span\n            data-slot=\"artifact-card-meta\"\n            className=\"flex min-w-0 items-center gap-1.5 text-kura-fg-muted text-xs leading-5\"\n          >\n            {busy ? (\n              <>\n                <ShimmerText className=\"truncate\">\n                  {statusText ?? (status === \"generating\" ? \"Generating…\" : \"Updating…\")}\n                </ShimmerText>\n                {pct != null && (\n                  <span className=\"shrink-0 font-kura-mono text-[11px] text-kura-fg-subtle tabular-nums\">\n                    {pct}%\n                  </span>\n                )}\n              </>\n            ) : status === \"error\" ? (\n              <span className=\"truncate text-kura-danger-text\">{error ?? \"Couldn't finish\"}</span>\n            ) : (\n              <span className=\"truncate\">{meta ?? k.label}</span>\n            )}\n          </span>\n        </div>\n\n        {versions > 1 && (\n          <div\n            data-slot=\"artifact-card-versions\"\n            className=\"relative z-10 flex shrink-0 items-center text-kura-fg-subtle\"\n          >\n            <IconButton\n              size=\"xs\"\n              label=\"Previous version\"\n              icon={<ChevronLeft />}\n              disabled={shownVersion <= 1 || busy}\n              onClick={() => setVersion(shownVersion - 1)}\n            />\n            <span className=\"min-w-[4.5ch] text-center font-kura-mono text-[11px] tabular-nums\">\n              <span className=\"sr-only\">Version </span>v{shownVersion}\n              <span className=\"text-kura-fg-subtle/70\">\n                <span aria-hidden>/</span>\n                <span className=\"sr-only\"> of </span>\n                {versions}\n              </span>\n            </span>\n            <IconButton\n              size=\"xs\"\n              label=\"Next version\"\n              icon={<ChevronRight />}\n              disabled={shownVersion >= versions || busy}\n              onClick={() => setVersion(shownVersion + 1)}\n            />\n          </div>\n        )}\n\n        {((status === \"error\" && onRetry) || actions != null) && (\n          <div\n            data-slot=\"artifact-card-actions\"\n            className=\"relative z-10 flex shrink-0 items-center gap-0.5\"\n          >\n            {status === \"error\" && onRetry && (\n              <IconButton size=\"sm\" label=\"Retry\" icon={<RotateCw />} onClick={onRetry} />\n            )}\n            {actions}\n          </div>\n        )}\n\n        {openable && compact && actions == null && versions <= 1 && status !== \"error\" && (\n          <ChevronRight\n            aria-hidden\n            className=\"size-4 shrink-0 text-kura-fg-subtle transition-transform duration-(--kura-duration-fast) ease-kura-out group-hover/artifact:translate-x-0.5 motion-reduce:transition-none motion-reduce:group-hover/artifact:translate-x-0\"\n          />\n        )}\n      </div>\n\n      {compact && progressBar}\n\n      <span className=\"sr-only\" role=\"status\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n\nconst LINES: Record<ArtifactKind, number[]> = {\n  document: [46, 0, 94, 88, 91, 62, 0, 90, 84],\n  code: [38, 62, 54, 0, 70, 48, 66, 40, 0, 30],\n  web: [],\n  image: [],\n  data: [],\n  diagram: [],\n  file: [72, 88, 64, 80, 56],\n};\n\n/** Drawn when no `preview` is given: a quiet sketch of the kind (and a shimmer while it is written). */\nfunction PreviewPlaceholder({ kind, busy }: { kind: ArtifactKind; busy: boolean }) {\n  const bar = cn(\n    \"h-1.5 rounded-full bg-kura-gray-5\",\n    busy &&\n      \"animate-kura-shimmer [background-size:250%_100%] motion-reduce:animate-none motion-reduce:bg-none\",\n  );\n  const shimmer: React.CSSProperties | undefined = busy\n    ? {\n        backgroundImage:\n          \"linear-gradient(90deg, var(--kura-gray-5) 35%, var(--kura-gray-7) 50%, var(--kura-gray-5) 65%)\",\n      }\n    : undefined;\n\n  if (kind === \"web\") {\n    return (\n      <div className=\"flex h-full flex-col gap-3 p-4\">\n        <div className=\"flex items-center gap-1\">\n          {[0, 1, 2].map((i) => (\n            <span key={i} className=\"size-1.5 rounded-full bg-kura-gray-6\" />\n          ))}\n          <span className=\"ml-2 h-1.5 w-24 rounded-full bg-kura-gray-5\" />\n        </div>\n        <div className={cn(bar, \"h-3 w-1/2\")} style={shimmer} />\n        <div className=\"grid flex-1 grid-cols-3 gap-2\">\n          {[0, 1, 2].map((i) => (\n            <div key={i} className=\"rounded-kura-sm bg-kura-gray-4\" />\n          ))}\n        </div>\n      </div>\n    );\n  }\n  if (kind === \"data\") {\n    return (\n      <div className=\"grid grid-cols-4 gap-px p-4 opacity-80\">\n        {Array.from({ length: 20 }, (_, i) => (\n          <span key={i} className={cn(\"h-4 bg-kura-gray-4\", i < 4 && \"bg-kura-gray-6\")} />\n        ))}\n      </div>\n    );\n  }\n  if (kind === \"image\" || kind === \"diagram\") {\n    return (\n      <div className=\"grid h-full place-items-center text-kura-fg-subtle/60 [&_svg]:size-10 [&_svg]:stroke-[1.25]\">\n        {KINDS[kind].icon}\n      </div>\n    );\n  }\n  return (\n    <div className=\"flex flex-col gap-2 p-5\">\n      {LINES[kind].map((w, i) =>\n        w === 0 ? (\n          <span key={i} className=\"h-1\" />\n        ) : (\n          <span\n            key={i}\n            className={cn(bar, i === 0 && kind === \"document\" && \"h-2.5 bg-kura-gray-6\")}\n            style={{\n              ...shimmer,\n              width: `${w}%`,\n              marginLeft: kind === \"code\" && i % 3 === 1 ? \"1rem\" : undefined,\n            }}\n          />\n        ),\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/kura/artifact-card/index.ts",
      "type": "registry:ui",
      "target": "components/kura/artifact-card/index.ts",
      "content": "export {\n  ArtifactCard,\n  type ArtifactCardProps,\n  type ArtifactKind,\n  type ArtifactStatus,\n  artifactKindLabel,\n} from \"./artifact-card\";\n"
    }
  ],
  "categories": [
    "conversation",
    "artifact",
    "canvas",
    "document",
    "file"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/artifact-card · Markdown for agents: https://kuro.abstergo.fyi/components/artifact-card.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "An assistant message produced a standalone deliverable (a document, a code file, a web page, an image, a CSV, a diagram) that the user opens in a side panel or another page.",
        "Show an artifact while the agent is still writing it: status=\"generating\" with a live statusText and optional progress, then flip to \"ready\".",
        "An artifact the user asked to change: status=\"updating\" plus a ‹ v2/3 › version switcher (versions/version/onVersionChange).",
        "A list of everything the agent made in a session, e.g. a files/outputs sidebar (compact variant, selected marks the one that is open).",
        "A visual card with a thumbnail of the artifact under the title (variant=\"preview\" with a `preview` node: a text excerpt, an <img>, a headerless CodeBlock)."
      ],
      "whenNotToUse": [
        "Code the user should read inline in the answer — use code-block directly.",
        "A file the user uploaded or attached to the prompt — use attachment.",
        "One tool invocation with arguments and a result (read_file, run_command) — use tool-call.",
        "A link to an external web source the answer relies on — use citation.",
        "A multi-step plan of work — use agent-timeline."
      ]
    }
  }
}
