{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"code-block","title":"Code Block","description":"A line-numbered listing and a unified diff.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json"],"files":[{"path":"registry/ward/CodeBlock.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useCallback, useState, type ReactNode } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CODE BLOCK\n * A light editor panel with two versions (switch in the card):\n *   · Code — a line-numbered listing\n *   · Diff — a unified diff: old/new gutters, a green/red accent\n *     bar and row tint, plus word-level add/del highlights.\n * Both share syntax coloring, insets, and wrapping behavior.\n * ───────────────────────────────────────────────────────── */\n\nconst FILE = \"plan.ts\";\n\nconst CODE_LINES = [\n  \"export async function planBatch() {\",\n  '  const project = await getProject(\"pistachio\");',\n  \"  const base = await dairy.fetch({ project });\",\n  '  await freezer.store(base, { temp: \"-16C\" });',\n  \"  if (!base.approved) return null;\",\n  \"  return base.gallons;\",\n  \"}\",\n];\n\n/* A single run of code within a diff row; `change` tints it as an add/del. */\nexport type CodePiece = { text: string; change?: \"add\" | \"del\" };\n/* One row of a unified diff: old/new line numbers, its kind, and its pieces. */\nexport type DiffRow = {\n  old: number | null;\n  cur: number | null;\n  type: \"ctx\" | \"add\" | \"del\";\n  pieces: CodePiece[];\n};\n/* Prominent copy strings on the code block. */\nexport type CodeBlockLabels = { copy: string; copied: string };\n\n// Back-compat internal aliases for the local component signatures.\ntype Piece = CodePiece;\ntype Row = DiffRow;\n\nconst DIFF: Row[] = [\n  { old: 1, cur: 1, type: \"ctx\", pieces: [{ text: \"export async function planBatch() {\" }] },\n  { old: 2, cur: 2, type: \"ctx\", pieces: [{ text: '  const project = await getProject(\"pistachio\");' }] },\n  { old: 3, cur: 3, type: \"ctx\", pieces: [{ text: \"  const base = await dairy.fetch({ project });\" }] },\n  { old: 4, cur: null, type: \"del\", pieces: [{ text: \"  await freezer.store(base, { temp: \" }, { text: '\"-14C\"', change: \"del\" }, { text: \" });\" }] },\n  { old: null, cur: 4, type: \"add\", pieces: [{ text: \"  await freezer.store(base, { temp: \" }, { text: '\"-16C\"', change: \"add\" }, { text: \" });\" }] },\n  { old: null, cur: 5, type: \"add\", pieces: [{ text: \"  if (!base.approved) return null;\" }] },\n  { old: 5, cur: 6, type: \"ctx\", pieces: [{ text: \"  return base.gallons;\" }] },\n  { old: 6, cur: 7, type: \"ctx\", pieces: [{ text: \"}\" }] },\n];\n\nconst HATCH = \"repeating-linear-gradient(45deg, var(--red) 0, var(--red) 1.5px, transparent 1.5px, transparent 3px)\";\n\n/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */\nconst KEYWORDS = new Set([\"import\", \"from\", \"export\", \"default\", \"async\", \"function\", \"const\", \"let\", \"var\", \"await\", \"return\", \"if\", \"else\", \"for\", \"while\", \"new\", \"throw\", \"try\", \"catch\", \"null\", \"true\", \"false\", \"undefined\"]);\nconst TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`[^`]*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\\b|[A-Za-z_$][\\w$]*(?=\\s*\\())/g;\n\nfunction highlight(text: string): ReactNode[] {\n  const nodes: ReactNode[] = [];\n  let last = 0;\n  let k = 0;\n  for (const m of text.matchAll(TOKEN)) {\n    const idx = m.index ?? 0;\n    const t = m[0];\n    if (idx > last) nodes.push(<span key={k++}>{text.slice(last, idx)}</span>);\n    let color: string;\n    let weight: number | undefined;\n    if (/^[\"'`]/.test(t) || /^\\d/.test(t)) color = \"var(--orange)\"; // string / number\n    else if (KEYWORDS.has(t)) color = \"var(--highlight-ink)\"; // keyword / import / conditional\n    else { color = \"var(--ink)\"; weight = 500; } // function call\n    nodes.push(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);\n    last = idx + t.length;\n  }\n  if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);\n  return nodes;\n}\n\nfunction Pieces({ pieces }: { pieces: Piece[] }) {\n  return (\n    <>\n      {pieces.map((p, i) => {\n        if (p.change) {\n          const add = p.change === \"add\";\n          return (\n            <span\n              key={i}\n              className=\"rounded-[3px]\"\n              style={{\n                background: `color-mix(in srgb, var(--${add ? \"green\" : \"red\"}) 18%, transparent)`,\n                padding: \"0 2px\",\n                margin: \"0 -1px\",\n                boxDecorationBreak: \"clone\",\n                WebkitBoxDecorationBreak: \"clone\",\n              }}\n            >\n              {highlight(p.text)}\n            </span>\n          );\n        }\n        return <span key={i}>{highlight(p.text)}</span>;\n      })}\n    </>\n  );\n}\n\nfunction FileIcon() {\n  return (\n    <svg aria-hidden width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0 text-ink-3\">\n      <path d=\"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5\" />\n    </svg>\n  );\n}\n\nconst DEFAULT_LABELS: CodeBlockLabels = { copy: \"Copy\", copied: \"Copied\" };\n\nexport type CodeBlockProps = {\n  /** Which view to render — \"Code\" (line-numbered listing) or \"Diff\". */\n  variant?: string;\n  /** The lines shown in the Code view. */\n  lines?: string[];\n  /** Raw text placed on the clipboard by Copy. Defaults to `lines` joined. */\n  code?: string;\n  /** The unified-diff rows shown in the Diff view. */\n  diff?: DiffRow[];\n  /** Filename shown in the header. */\n  filename?: string;\n  /** Prominent copy strings. */\n  labels?: Partial<CodeBlockLabels>;\n  /** Called with the copied text after a successful copy. */\n  onCopy?: (text: string) => void;\n};\n\nexport default function CodeBlock({\n  variant = \"Code\",\n  lines = CODE_LINES,\n  code,\n  diff = DIFF,\n  filename = FILE,\n  labels,\n  onCopy,\n}: CodeBlockProps) {\n  const [copied, setCopied] = useState(false);\n  const isDiff = variant === \"Diff\";\n  const text = { ...DEFAULT_LABELS, ...labels };\n  const raw = code ?? lines.join(\"\\n\");\n\n  const copy = useCallback(() => {\n    navigator.clipboard.writeText(raw).then(() => {\n      setCopied(true);\n      onCopy?.(raw);\n      setTimeout(() => setCopied(false), 1500);\n    });\n  }, [raw, onCopy]);\n\n  const added = diff.filter((r) => r.type === \"add\").length;\n  const removed = diff.filter((r) => r.type === \"del\").length;\n\n  return (\n    <div className=\"w-full max-w-105 overflow-hidden rounded-card bg-card shadow-card\">\n      {/* header — file · (diff stat | copy) */}\n      <div className=\"flex h-11 items-center gap-2 border-b border-line px-4 text-[12.5px]\">\n        <span className=\"inline-flex min-w-0 items-center gap-[7px]\">\n          <FileIcon />\n          <span className=\"truncate font-mono leading-none text-ink\">{filename}</span>\n        </span>\n\n        {isDiff ? (\n          <span className=\"ml-auto inline-flex items-center gap-2 font-mono text-[12px] leading-none tabular-nums\">\n            <span className=\"text-green\">+{added}</span>\n            <span className=\"text-red\">-{removed}</span>\n          </span>\n        ) : (\n          <button\n            type=\"button\"\n            aria-label=\"Copy code\"\n            onClick={copy}\n            className={`-mr-1 ml-auto flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[12px]\n              font-medium transition-colors duration-100 hover:bg-hover\n              ${copied ? \"text-green\" : \"text-ink-3 hover:text-ink\"}`}\n          >\n            {copied ? (\n              <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n            ) : (\n              <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2.5\" /><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" /></svg>\n            )}\n            {copied ? text.copied : text.copy}\n          </button>\n        )}\n      </div>\n\n      {/* body — equal 12px inset on top / left / right; lines wrap */}\n      <div className=\"py-3 font-mono text-[12.5px] leading-[1.65] text-ink-2\">\n        {isDiff ? (\n          <div className=\"relative\">\n            <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-line\" />\n            {diff.map((r, i) => {\n              const add = r.type === \"add\";\n              const del = r.type === \"del\";\n              // one gutter column: removals keep the old number, additions/context show the new one\n              const num = del ? r.old : r.cur;\n              return (\n                <div\n                  key={i}\n                  className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start\n                    ${add ? \"bg-green-tint\" : del ? \"bg-red-tint\" : \"\"}`}\n                >\n                  {(add || del) && (\n                    <span className=\"absolute inset-y-0 left-0 w-[3px]\" style={{ background: add ? \"var(--green)\" : HATCH }} />\n                  )}\n                  <span className={`select-none text-center text-[11px] ${add ? \"text-green\" : del ? \"text-red\" : \"text-ink-3\"}`}>{num ?? \"\"}</span>\n                  <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">\n                    <Pieces pieces={r.pieces} />\n                  </code>\n                </div>\n              );\n            })}\n          </div>\n        ) : (\n          <div className=\"relative\">\n            <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-line\" />\n            {lines.map((line, i) => (\n              <div key={i} className=\"grid grid-cols-[20px_minmax(0,1fr)] items-start\">\n                <span className=\"select-none text-center text-[11px] text-ink-3\">{i + 1}</span>\n                <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">{highlight(line)}</code>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/CodeBlock.tsx"}],"meta":{"variants":["Code","Diff"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/CodeBlock.tsx","access":"free"},"categories":["ui"],"type":"registry:component"}