{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Generated-artifact frame whose preview and code panes swap along a shared axis rather than cross-fading.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check, Copy } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst TRAVEL_PX = 24;\nconst COPIED_RESET_MS = 1600;\n\nexport type AIArtifactPane = \"preview\" | \"code\";\n\nexport type AIArtifactProps = {\n  className?: string;\n  /** The raw form — source, JSON, markup. */\n  code?: ReactNode;\n  /** Plain text handed to the clipboard. Omit to hide the copy action. */\n  copyText?: string;\n  /** Which pane to start on. */\n  defaultPane?: AIArtifactPane;\n  /** The rendered form. */\n  preview?: ReactNode;\n  /** Name the artifact so it can be referred to in conversation. */\n  title: string;\n};\n\nconst PANES: AIArtifactPane[] = [\"preview\", \"code\"];\n\n/**\n * A frame around something the model produced.\n *\n * The two panes travel along a single horizontal axis — preview sits to the left\n * of code, always — so the swap has a direction and, after one go, a memory. A\n * cross-fade between them would leave the user with no sense of where the other\n * pane went, and no expectation of where it will come back from.\n */\nconst AIArtifact = ({\n  className,\n  code,\n  copyText,\n  defaultPane = \"preview\",\n  preview,\n  title,\n}: AIArtifactProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [pane, setPane] = useState<AIArtifactPane>(defaultPane);\n  const [hasCopied, setHasCopied] = useState(false);\n  const indicatorId = useId();\n\n  const available = PANES.filter((candidate) =>\n    candidate === \"preview\" ? Boolean(preview) : Boolean(code)\n  );\n\n  useEffect(() => {\n    if (!hasCopied) {\n      return;\n    }\n    const timeout = setTimeout(() => setHasCopied(false), COPIED_RESET_MS);\n    return () => clearTimeout(timeout);\n  }, [hasCopied]);\n\n  const copy = async () => {\n    if (!copyText) {\n      return;\n    }\n    try {\n      await navigator.clipboard.writeText(copyText);\n      setHasCopied(true);\n    } catch {\n      // A blocked clipboard is not worth an error state.\n    }\n  };\n\n  const direction = pane === \"code\" ? 1 : -1;\n\n  return (\n    <div\n      className={cn(\n        \"w-full overflow-hidden rounded-xl border border-border bg-background\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-2 border-border border-b px-2 py-1.5\">\n        <span className=\"min-w-0 truncate px-1 font-medium text-foreground text-xs\">\n          {title}\n        </span>\n\n        {available.length > 1 && (\n          <div className=\"ml-auto flex items-center gap-0.5 rounded-lg bg-muted p-0.5\">\n            {available.map((candidate) => (\n              <button\n                aria-selected={pane === candidate}\n                className={cn(\n                  \"relative cursor-pointer rounded-md px-2 py-1 text-xs capitalize transition-colors\",\n                  pane === candidate\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                )}\n                key={candidate}\n                onClick={() => setPane(candidate)}\n                role=\"tab\"\n                type=\"button\"\n              >\n                {pane === candidate && (\n                  // A single shared element slides between the tabs. Two\n                  // separately styled tabs would just swap colour, which reads\n                  // as two states rather than one selection moving.\n                  <motion.span\n                    className=\"absolute inset-0 rounded-md bg-background shadow-sm\"\n                    layoutId={`${indicatorId}-indicator`}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT\n                    }\n                  />\n                )}\n                <span className=\"relative\">{candidate}</span>\n              </button>\n            ))}\n          </div>\n        )}\n\n        {copyText ? (\n          <button\n            aria-label={hasCopied ? \"Copied\" : \"Copy\"}\n            className={cn(\n              \"cursor-pointer rounded-md p-1.5 transition-colors\",\n              available.length > 1 ? \"\" : \"ml-auto\",\n              hasCopied\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\n            )}\n            onClick={copy}\n            type=\"button\"\n          >\n            {hasCopied ? (\n              <Check aria-hidden=\"true\" size={13} />\n            ) : (\n              <Copy aria-hidden=\"true\" size={13} />\n            )}\n          </button>\n        ) : null}\n      </div>\n\n      <div className=\"relative overflow-hidden\">\n        <AnimatePresence custom={direction} initial={false} mode=\"wait\">\n          <motion.div\n            animate={{ opacity: 1, x: 0 }}\n            custom={direction}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, x: -direction * TRAVEL_PX }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, x: 0 }\n                : { opacity: 0, x: direction * TRAVEL_PX }\n            }\n            key={pane}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.2, ease: EASE_OUT }\n            }\n          >\n            {pane === \"preview\" ? (\n              <div className=\"p-3\">{preview}</div>\n            ) : (\n              <pre className=\"overflow-x-auto p-3 font-mono text-foreground text-xs leading-relaxed\">\n                {code}\n              </pre>\n            )}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n};\n\nexport default AIArtifact;\n","path":"index.tsx","target":"components/smoothui/ai-artifact/index.tsx","type":"registry:ui"}],"name":"ai-artifact","registryDependencies":[],"title":"Ai Artifact","type":"registry:ui"}