{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "artifact-panel",
  "title": "Artifact Panel",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A side panel for things an assistant generates: Preview and Code tabs, version stepping, copy and download, a streaming state and a close button, with the rendered result in your own slot.",
  "dependencies": [
    "motion@^12",
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/artifact-panel.tsx",
      "content": "// Ballmac UI: Artifact Panel. https://ui.ballmac.com/components/artifact-panel\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronLeft, ChevronRight, Copy, Download, X } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype ArtifactPanelProps = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\"> & {\n  /** Name of the generated thing, such as \"Pricing table\". */\n  title: string\n  /** Kind of artifact, shown under the title, such as \"React component\". */\n  kind?: string\n  /** Icon in the header tile. */\n  icon?: React.ReactNode\n  /** Source text. Shown in the Code tab and used for Copy and Download. */\n  code?: string\n  /** File name for Download and the code header, such as \"pricing-table.tsx\". */\n  filename?: string\n  /** Language label for the code, such as \"tsx\". */\n  language?: string\n  /** The rendered result. Put an iframe, a component or a document here. */\n  children?: React.ReactNode\n  /** How many versions exist. With more than one, arrows to step through them appear. */\n  versions?: number\n  /** Which version is shown, starting at 1 (controlled). */\n  version?: number\n  /** Called when the version changes. */\n  onVersionChange?: (version: number) => void\n  /** True while the model is still writing. Shows a progress edge and disables copy and download. */\n  streaming?: boolean\n  /** Tab shown first. */\n  defaultTab?: \"preview\" | \"code\"\n  /** Adds a close button. */\n  onClose?: () => void\n  /** Called after the code was copied. */\n  onCopy?: () => void\n  /** Extra header actions, placed before Copy. */\n  actions?: React.ReactNode\n  /** Classes for the preview canvas. */\n  previewClassName?: string\n}\n\nconst iconButton =\n  \"inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-4\"\n\nfunction ArtifactPanel({\n  title,\n  kind,\n  icon,\n  code,\n  filename,\n  language,\n  children,\n  versions = 1,\n  version: versionProp,\n  onVersionChange,\n  streaming = false,\n  defaultTab = \"preview\",\n  onClose,\n  onCopy,\n  actions,\n  previewClassName,\n  className,\n  ...props\n}: ArtifactPanelProps) {\n  const msg = useMessages()\n  const reduce = useReducedMotion()\n  const [internalVersion, setInternalVersion] = React.useState(versions)\n  const version = Math.min(Math.max(versionProp ?? internalVersion, 1), versions)\n  const [copied, setCopied] = React.useState(false)\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n  React.useEffect(() => () => clearTimeout(timer.current), [])\n  const hasCode = code !== undefined\n  const hasPreview = children !== undefined && children !== null\n\n  function go(next: number) {\n    const clamped = Math.min(Math.max(next, 1), versions)\n    setInternalVersion(clamped)\n    onVersionChange?.(clamped)\n  }\n\n  async function copy() {\n    if (!code) return\n    try {\n      await navigator.clipboard.writeText(code)\n    } catch {\n      return\n    }\n    setCopied(true)\n    onCopy?.()\n    clearTimeout(timer.current)\n    timer.current = setTimeout(() => setCopied(false), 1800)\n  }\n\n  function download() {\n    if (!code) return\n    const url = URL.createObjectURL(new Blob([code], { type: \"text/plain;charset=utf-8\" }))\n    const a = document.createElement(\"a\")\n    a.href = url\n    a.download = filename ?? \"artifact.txt\"\n    a.click()\n    URL.revokeObjectURL(url)\n  }\n\n  const tabClass =\n    \"relative inline-flex h-7 items-center rounded-md px-3 text-[13px] font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs\"\n\n  return (\n    <section\n      data-slot=\"artifact-panel\"\n      aria-label={title}\n      aria-busy={streaming || undefined}\n      className={cn(\n        \"relative flex w-full flex-col overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\",\n        className\n      )}\n      {...props}\n    >\n      <TabsPrimitive.Root defaultValue={hasPreview ? defaultTab : \"code\"} className=\"flex min-h-0 flex-1 flex-col\">\n        <header className=\"relative flex flex-wrap items-center gap-x-3 gap-y-2 border-b px-3 py-2.5\">\n          <div className=\"flex min-w-[10rem] flex-1 items-center gap-2.5\">\n            {icon && (\n              <span\n                aria-hidden=\"true\"\n                className=\"flex size-8 shrink-0 items-center justify-center rounded-lg border bg-muted text-foreground [&_svg]:size-4\"\n              >\n                {icon}\n              </span>\n            )}\n            <div className=\"min-w-0\">\n              <h3 className=\"truncate text-sm leading-5 font-semibold text-foreground\">{title}</h3>\n              {(kind || streaming) && (\n                <p className=\"truncate text-xs leading-4 text-muted-foreground\">\n                  {streaming ? \"Writing…\" : kind}\n                </p>\n              )}\n            </div>\n          </div>\n\n          {hasPreview && hasCode && (\n            <TabsPrimitive.List\n              aria-label={msg(\"artifact-panel.view\", \"View\")}\n              className=\"inline-flex items-center gap-0.5 rounded-lg bg-muted p-0.5\"\n            >\n              <TabsPrimitive.Trigger value=\"preview\" className={tabClass}>\n                Preview\n              </TabsPrimitive.Trigger>\n              <TabsPrimitive.Trigger value=\"code\" className={tabClass}>\n                Code\n              </TabsPrimitive.Trigger>\n            </TabsPrimitive.List>\n          )}\n\n          <div className=\"flex items-center gap-0.5\">\n            {versions > 1 && (\n              <span className=\"me-1 inline-flex items-center\" role=\"group\" aria-label={msg(\"artifact-panel.versions\", \"Versions\")}>\n                <button\n                  type=\"button\"\n                  aria-label={msg(\"artifact-panel.previousVersion\", \"Previous version\")}\n                  disabled={version <= 1 || streaming}\n                  onClick={() => go(version - 1)}\n                  className={iconButton}\n                >\n                  <ChevronLeft aria-hidden=\"true\"  className=\"rtl:rotate-180\"/>\n                </button>\n                <span className=\"min-w-12 text-center font-mono text-xs text-muted-foreground tabular-nums\" aria-live=\"polite\">\n                  v{version} <span aria-hidden=\"true\">/ {versions}</span>\n                  <span className=\"sr-only\"> {msg(\"artifact-panel.versionOf\", \"of {total}\", { total: versions })}</span>\n                </span>\n                <button\n                  type=\"button\"\n                  aria-label={msg(\"artifact-panel.nextVersion\", \"Next version\")}\n                  disabled={version >= versions || streaming}\n                  onClick={() => go(version + 1)}\n                  className={iconButton}\n                >\n                  <ChevronRight aria-hidden=\"true\"  className=\"rtl:rotate-180\"/>\n                </button>\n              </span>\n            )}\n            {actions}\n            {hasCode && (\n              <>\n                <button\n                  type=\"button\"\n                  aria-label={copied ? msg(\"artifact-panel.copied\", \"Copied\") : msg(\"artifact-panel.copyCode\", \"Copy code\")}\n                  title={copied ? msg(\"artifact-panel.copied\", \"Copied\") : msg(\"artifact-panel.copyCode\", \"Copy code\")}\n                  disabled={streaming}\n                  onClick={copy}\n                  className={iconButton}\n                >\n                  {copied ? <Check aria-hidden=\"true\" /> : <Copy aria-hidden=\"true\" />}\n                </button>\n                <button\n                  type=\"button\"\n                  aria-label={msg(\"artifact-panel.downloadFile\", \"Download {name}\", { name: filename ?? msg(\"artifact-panel.file\", \"file\") })}\n                  title={msg(\"artifact-panel.download\", \"Download\")}\n                  disabled={streaming}\n                  onClick={download}\n                  className={iconButton}\n                >\n                  <Download aria-hidden=\"true\" />\n                </button>\n              </>\n            )}\n            {onClose && (\n              <button type=\"button\" aria-label={msg(\"artifact-panel.closeArtifact\", \"Close artifact\")} title={msg(\"artifact-panel.close\", \"Close\")} onClick={onClose} className={iconButton}>\n                <X aria-hidden=\"true\" />\n              </button>\n            )}\n          </div>\n          {streaming && (\n            <span aria-hidden=\"true\" className=\"absolute inset-x-0 bottom-[-1px] h-0.5 overflow-hidden\">\n              {reduce ? (\n                <span className=\"block h-full w-full bg-foreground/30\" />\n              ) : (\n                <motion.span\n                  className=\"block h-full w-1/3 bg-foreground/70\"\n                  animate={{ x: [\"-100%\", \"300%\"] }}\n                  transition={{ duration: 1.4, repeat: Infinity, ease: \"easeInOut\" }}\n                />\n              )}\n            </span>\n          )}\n        </header>\n\n        {hasPreview && (\n          <TabsPrimitive.Content\n            value=\"preview\"\n            className={cn(\n              \"min-h-0 flex-1 overflow-auto bg-[radial-gradient(var(--border)_1px,transparent_1px)] [background-size:16px_16px] p-5 outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\",\n              previewClassName\n            )}\n            tabIndex={0}\n          >\n            {children}\n          </TabsPrimitive.Content>\n        )}\n        {hasCode && (\n          <TabsPrimitive.Content\n            value=\"code\"\n            className=\"min-h-0 flex-1 overflow-auto bg-muted/30 outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\"\n            tabIndex={0}\n          >\n            {filename || language ? (\n              <div className=\"sticky top-0 flex h-8 items-center gap-2 border-b bg-muted/60 px-4 font-mono text-[11px] text-muted-foreground backdrop-blur\">\n                {filename}\n                {language && <span className=\"ms-auto uppercase\">{language}</span>}\n              </div>\n            ) : null}\n            <pre dir=\"ltr\" className=\"p-4 font-mono text-xs leading-5 text-foreground\">\n              <code>{code}</code>\n              {streaming && (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"ms-0.5 inline-block h-3.5 w-1.5 translate-y-0.5 animate-pulse bg-foreground motion-reduce:animate-none\"\n                />\n              )}\n            </pre>\n          </TabsPrimitive.Content>\n        )}\n      </TabsPrimitive.Root>\n    </section>\n  )\n}\n\nexport { ArtifactPanel, type ArtifactPanelProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/artifact-panel.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "artifact",
      "canvas",
      "preview",
      "code",
      "panel"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "title, kind, icon, code, filename, language and children as the preview. versions > 1 adds steppers (version/onVersionChange). streaming shows a progress edge and disables copy and download. onClose adds a close button.",
      "whenToUse": [
        "Code, documents or designs a model produced next to the chat",
        "Anything users iterate on in versions"
      ],
      "whenNotToUse": [
        "Showing code in the chat; use code-block",
        "Generic tabs; use tabs"
      ],
      "composesWith": [
        "ai-chat",
        "code-block",
        "resizable"
      ],
      "a11y": [
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Switches between Preview and Code when the tabs have focus"
        },
        {
          "keys": "Tab",
          "action": "Reaches version arrows, copy, download, close, and the scrollable content"
        },
        {
          "keys": "Screen readers",
          "action": "Region named by the title; version changes are announced politely"
        }
      ],
      "customization": [
        "defaultTab",
        "streaming",
        "actions slot",
        "previewClassName",
        "filename and language"
      ]
    },
    "examples": [
      "artifact-panel-demo",
      "artifact-panel-streaming"
    ],
    "url": "https://ui.ballmac.com/components/artifact-panel"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}