{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"code-block","type":"registry:component","title":"Code Block","description":"A code block with a label, a copy button, and light coloring for keywords, strings, comments and numbers, with no highlighter dependency.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/code-block.tsx","type":"registry:component","target":"@components/motion/code-block.tsx","content":"\"use client\";\n// easeui.dev/components/agents/code-block\n\nimport { FileCode } from \"lucide-react\";\nimport { CopyButton } from \"@/components/motion/copy-button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CodeBlockProps {\n  code: string;\n  /** Shown in the header, such as a language name or a filename. Default \"code\". */\n  label?: string;\n  className?: string;\n}\n\nconst KEYWORDS = new Set([\n  \"const\", \"let\", \"var\", \"function\", \"return\", \"if\", \"else\", \"for\", \"while\", \"do\", \"switch\", \"case\",\n  \"break\", \"continue\", \"class\", \"interface\", \"type\", \"extends\", \"implements\", \"import\", \"from\",\n  \"export\", \"default\", \"async\", \"await\", \"new\", \"this\", \"super\", \"try\", \"catch\", \"finally\", \"throw\",\n  \"typeof\", \"instanceof\", \"in\", \"of\", \"true\", \"false\", \"null\", \"undefined\", \"void\", \"yield\", \"static\",\n  \"public\", \"private\", \"protected\", \"readonly\", \"def\", \"elif\", \"lambda\", \"pass\", \"with\", \"as\", \"None\",\n  \"True\", \"False\", \"self\",\n]);\n\ntype TokenType = \"keyword\" | \"string\" | \"comment\" | \"number\" | \"plain\";\ntype Token = { text: string; type: TokenType };\n\nconst TOKEN_PATTERN =\n  /(\\/\\/.*$|#.*$|\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|`(?:[^`\\\\]|\\\\.)*`|\\b\\d+(?:\\.\\d+)?\\b|\\b[A-Za-z_$][\\w$]*\\b)/g;\n\n/** A best-effort tokenizer for keywords, strings, comments and numbers. Not a real parser. */\nfunction tokenize(line: string): Token[] {\n  const tokens: Token[] = [];\n  let last = 0;\n  for (const match of line.matchAll(TOKEN_PATTERN)) {\n    const index = match.index ?? 0;\n    if (index > last) tokens.push({ text: line.slice(last, index), type: \"plain\" });\n    const text = match[0];\n    const type: TokenType =\n      text.startsWith(\"//\") || text.startsWith(\"#\")\n        ? \"comment\"\n        : /^[\"'`]/.test(text)\n          ? \"string\"\n          : /^\\d/.test(text)\n            ? \"number\"\n            : KEYWORDS.has(text)\n              ? \"keyword\"\n              : \"plain\";\n    tokens.push({ text, type });\n    last = index + text.length;\n  }\n  if (last < line.length) tokens.push({ text: line.slice(last), type: \"plain\" });\n  return tokens;\n}\n\nconst TOKEN_CLASS: Record<TokenType, string> = {\n  keyword: \"text-accent\",\n  string: \"text-success\",\n  comment: \"italic text-muted-foreground\",\n  number: \"text-warning\",\n  plain: \"text-foreground\",\n};\n\n/** A code block with a label, a copy button, and light syntax coloring. No highlighter dependency. */\nexport function CodeBlock({ code, label = \"code\", className }: CodeBlockProps) {\n  const lines = code.replace(/\\n$/, \"\").split(\"\\n\");\n\n  return (\n    <figure\n      className={cn(\n        \"overflow-hidden rounded-2xl bg-card font-mono text-[13px] leading-relaxed shadow-[0_0_0_1px_var(--border)]\",\n        className,\n      )}\n    >\n      <figcaption className=\"flex h-11 items-center justify-between gap-3 border-b border-border pl-4 pr-1.5\">\n        <span className=\"flex min-w-0 items-center gap-2 font-sans text-xs text-muted-foreground\">\n          <FileCode aria-hidden=\"true\" className=\"h-3.5 w-3.5 shrink-0\" />\n          <span className=\"truncate\">{label}</span>\n        </span>\n        <CopyButton value={code} />\n      </figcaption>\n      <pre className=\"max-h-96 overflow-x-auto overflow-y-auto py-4\">\n        <code>\n          {lines.map((line, index) => (\n            // biome-ignore lint/suspicious/noArrayIndexKey: lines only append or grow in place while streaming.\n            <div key={index} className=\"px-4\">\n              {line.length === 0\n                ? \" \"\n                : tokenize(line).map((token, tokenIndex) => (\n                    // biome-ignore lint/suspicious/noArrayIndexKey: tokens are recomputed fresh from the line on every render.\n                    <span key={tokenIndex} className={TOKEN_CLASS[token.type]}>\n                      {token.text}\n                    </span>\n                  ))}\n            </div>\n          ))}\n        </code>\n      </pre>\n    </figure>\n  );\n}\n"},{"path":"components/motion/copy-button.tsx","type":"registry:component","target":"@components/motion/copy-button.tsx","content":"\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport { type ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype CopyState = \"idle\" | \"copied\" | \"failed\";\n\nexport interface CopyButtonProps\n  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | \"value\" | \"onCopy\"> {\n  /** Text written to the clipboard. */\n  value: string;\n  /** Visible text. Leave it out for an icon only button. */\n  label?: string;\n  /** Visible text after copying. Default \"Copied\". */\n  copiedLabel?: string;\n  /** How long the copied state stays, in ms. Default 1500. */\n  timeout?: number;\n  /** Called after the text reaches the clipboard. */\n  onCopied?: (value: string) => void;\n}\n\n// Both icons, and both labels, share a grid cell and trade places with a fade and a small\n// scale. Sharing the cell keeps the button the same width in either state.\nconst SWAP =\n  \"col-start-1 row-start-1 transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none\";\nconst SHOWN = \"scale-100 opacity-100\";\nconst ICON_HIDDEN = \"scale-50 opacity-0\";\nconst TEXT_HIDDEN = \"scale-[0.97] opacity-0\";\n\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n  { value, label, copiedLabel = \"Copied\", timeout = 1500, onCopied, className, onClick, ...props },\n  ref,\n) {\n  const [state, setState] = useState<CopyState>(\"idle\");\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current);\n    },\n    [],\n  );\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(value);\n      setState(\"copied\");\n      onCopied?.(value);\n    } catch {\n      // Clipboard access can be blocked, for example in an insecure context.\n      setState(\"failed\");\n    }\n    if (timer.current) clearTimeout(timer.current);\n    timer.current = setTimeout(() => setState(\"idle\"), timeout);\n  };\n\n  const copied = state === \"copied\";\n  const status = copied ? \"Copied to clipboard\" : state === \"failed\" ? \"Could not copy\" : \"\";\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      aria-label={label ?? \"Copy\"}\n      data-state={state}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) void copy();\n      }}\n      className={cn(\n        \"relative inline-flex h-9 shrink-0 touch-manipulation select-none items-center justify-center gap-2 rounded-full bg-card text-sm font-medium text-foreground outline-none\",\n        \"shadow-[0_0_0_1px_var(--border)] transition-[background-color,scale] duration-150 ease-out hover:bg-muted active:scale-[0.97] motion-reduce:active:scale-100\",\n        \"focus-visible:ring-2 focus-visible:ring-foreground/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        label ? \"px-3.5\" : \"w-9 after:absolute after:-inset-1\",\n        className,\n      )}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"grid place-items-center\">\n        <Copy className={cn(SWAP, \"h-4 w-4\", copied ? ICON_HIDDEN : SHOWN)} />\n        <Check className={cn(SWAP, \"h-4 w-4\", copied ? SHOWN : ICON_HIDDEN)} />\n      </span>\n      {label ? (\n        <span aria-hidden=\"true\" className=\"grid whitespace-nowrap\">\n          <span className={cn(SWAP, copied ? TEXT_HIDDEN : SHOWN)}>{label}</span>\n          <span className={cn(SWAP, copied ? SHOWN : TEXT_HIDDEN)}>{copiedLabel}</span>\n        </span>\n      ) : null}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {status}\n      </span>\n    </button>\n  );\n});\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}