{
  "schemaVersion": 1,
  "name": "code-block",
  "useFor": "Use to show a payload, headers or a snippet someone will read or copy (Signature headers, Payload example). Give it a CodeBlockHeader with the language and a CodeBlockCopyButton, then CodeBlockContent with the code. Pass wrap for long single values such as signatures.",
  "constraints": [
    "A dark code block",
    "Code in Inter",
    "Highlight colors outside the editor theme"
  ],
  "documentation": "",
  "source": "Figma 427:2715: Webhooks - Event catalog (Signature headers, Payload example)",
  "exports": [
    "CodeBlock",
    "CodeBlockActions",
    "CodeBlockContent",
    "CodeBlockCopyButton",
    "CodeBlockHeader",
    "CodeBlockTitle",
    "tokenize"
  ],
  "client": true,
  "docsUrl": "https://inkbox-ds.vercel.app/components/code-block",
  "markdownUrl": "https://inkbox-ds.vercel.app/r/code-block.md",
  "examples": [
    {
      "path": "examples/code-block.tsx",
      "content": "import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle } from \"@/components/ui/code-block\";\n\nconst HEADERS = `{\n  \"X-Inkbox-Request-ID\": \"b4f64d39-7cf0-4c89-b2fa-3522091e979d\",\n  \"X-Inkbox-Timestamp\": \"1764875501\",\n  \"X-Inkbox-Signature\": \"sha256=2f8d6b7f37b7d6461f7b8a91f4f495eb54f60af8f4a1f9d3edfdc7d2adf237f2\"\n}`;\n\nconst SNIPPET = `import { Inkbox } from \"@inkbox/sdk\";\n\n// Send the agent's first email.\nconst inkbox = new Inkbox({ apiKey: process.env.INKBOX_API_KEY });\nawait inkbox.mail.send({ to: \"you@example.com\", subject: \"Hello\" });`;\n\nexport function Preview() {\n  return (\n    <CodeBlock>\n      <CodeBlockHeader>\n        <CodeBlockTitle language=\"json\">JSON</CodeBlockTitle>\n        <CodeBlockActions>\n          <CodeBlockCopyButton value={HEADERS} />\n        </CodeBlockActions>\n      </CodeBlockHeader>\n      <CodeBlockContent code={HEADERS} language=\"json\" wrap />\n    </CodeBlock>\n  );\n}\n\nexport function Script() {\n  return (\n    <CodeBlock>\n      <CodeBlockHeader>\n        <CodeBlockTitle>send.ts</CodeBlockTitle>\n        <CodeBlockActions>\n          <CodeBlockCopyButton value={SNIPPET} />\n        </CodeBlockActions>\n      </CodeBlockHeader>\n      <CodeBlockContent code={SNIPPET} language=\"ts\" />\n    </CodeBlock>\n  );\n}\n",
      "exports": [
        "Preview",
        "Script"
      ],
      "requires": [
        "code-block"
      ],
      "packages": [
        "@inkbox/sdk"
      ],
      "previews": [
        {
          "name": "Preview",
          "url": "https://inkbox-ds.vercel.app/preview/code-block/Preview"
        },
        {
          "name": "Script",
          "url": "https://inkbox-ds.vercel.app/preview/code-block/Script"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Code Block",
  "description": "The event catalog's code viewer: a gray head with the language and a copy glyph over an off-white body with line numbers.",
  "surface": "site",
  "kinds": [],
  "requires": [
    "theme",
    "utils"
  ],
  "packages": [
    "@phosphor-icons/react"
  ],
  "files": [
    {
      "path": "components/ui/code-block.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { BracketsCurlyIcon, CheckIcon, CopyIcon } from \"@phosphor-icons/react/ssr\";\n\nimport { cn } from \"@/lib/utils\";\n\n// The event catalog's code block (Figma 427:2715, Webhooks: Signature headers, Payload example): 8px corners and the\n// editor's gray edge, a 40px gray head with the language (yellow braces for JSON) and a purple copy glyph, then the\n// off-white body in the system monospace at 14px on 20px lines, line numbers in the editor's teal (the first line's\n// darker), and the Light+ colors: red keys, blue values and braces. Long lines scroll, or wrap with `wrap`.\nfunction CodeBlock({ className, ...props }: React.ComponentProps<\"figure\">) {\n  return <figure data-slot=\"code-block\" className={cn(\"group/code-block m-0 flex w-full min-w-0 flex-col overflow-hidden rounded-control border border-line-code bg-code text-code-text\", className)} {...props} />;\n}\n\nfunction CodeBlockHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"code-block-header\" className={cn(\"flex h-10 min-w-0 shrink-0 items-center gap-2 border-b border-line-code bg-code-head pr-1.5 pl-3\", className)} {...props} />;\n}\n\nfunction CodeBlockTitle({ className, children, language, ...props }: React.ComponentProps<\"figcaption\"> & { language?: string }) {\n  return (\n    <figcaption data-slot=\"code-block-title\" className={cn(\"flex min-w-0 items-center gap-2 font-sans text-body leading-tight text-code-head-fg [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\", className)} {...props}>\n      {language === \"json\" && <BracketsCurlyIcon aria-hidden className=\"text-code-brace\" />}\n      <span className=\"truncate\">{children}</span>\n    </figcaption>\n  );\n}\n\nfunction CodeBlockActions({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"code-block-actions\" className={cn(\"ml-auto flex shrink-0 items-center gap-1\", className)} {...props} />;\n}\n\nfunction CodeBlockCopyButton({ value, className, ...props }: Omit<React.ComponentProps<\"button\">, \"value\" | \"children\"> & { value: string }) {\n  const [state, setState] = React.useState<\"idle\" | \"copied\" | \"failed\">(\"idle\");\n  React.useEffect(() => {\n    if (state === \"idle\") return;\n    const timeout = setTimeout(() => setState(\"idle\"), 2000);\n    return () => clearTimeout(timeout);\n  }, [state]);\n  return (\n    <>\n      <button\n        type=\"button\"\n        data-slot=\"code-block-copy-button\"\n        data-copied={state === \"copied\" || undefined}\n        aria-label={state === \"copied\" ? \"Copied\" : \"Copy code\"}\n        onClick={() => navigator.clipboard.writeText(value).then(() => setState(\"copied\"), () => setState(\"failed\"))}\n        className={cn(\"inline-flex size-8 items-center justify-center rounded-control text-fg-brand outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-surface active:scale-97 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring motion-reduce:transition-none\", className)}\n        {...props}\n      >\n        {state === \"copied\" ? <CheckIcon className=\"size-4.5\" weight=\"bold\" /> : <CopyIcon className=\"size-4.5\" weight=\"duotone\" />}\n      </button>\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {state === \"copied\" ? \"Copied\" : state === \"failed\" ? \"Copy failed. Select the code and copy it.\" : \"\"}\n      </span>\n    </>\n  );\n}\n\ntype Kind = \"key\" | \"string\" | \"punct\" | \"keyword\" | \"comment\" | \"number\" | \"function\" | \"type\" | \"tag\" | \"attr\" | \"plain\";\nconst COLOR: Record<Kind, string | undefined> = {\n  key: \"text-code-key\",\n  string: \"text-code-string\",\n  punct: \"text-code-punct\",\n  keyword: \"text-code-keyword\",\n  comment: \"text-code-comment\",\n  number: \"text-code-number\",\n  function: \"text-code-function\",\n  type: \"text-code-type\",\n  tag: \"text-code-tag\",\n  attr: \"text-code-attr\",\n  plain: undefined,\n};\n\nconst KEYWORDS = /^(?:import|from|export|default|function|return|const|let|var|if|else|for|of|in|new|type|interface|extends|as|async|await|true|false|null|undefined|class|this|throw|try|catch)$/;\n\n/** Splits code into colored runs for the Light+ theme. JSON keys are red and values blue; in scripts strings are red. */\nfunction tokenize(code: string, language: string): { kind: Kind; text: string }[] {\n  const out: { kind: Kind; text: string }[] = [];\n  const push = (kind: Kind, text: string) => (text ? out.push({ kind, text }) : undefined);\n  if (language === \"json\") {\n    for (const m of code.matchAll(/(\"(?:\\\\.|[^\"\\\\])*\")(\\s*:)?|(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)|(true|false|null)|([{}[\\]])|([^\"{}[\\]\\d\\-tfn]+|[\\s\\S])/g)) {\n      if (m[1]) {\n        push(m[2] ? \"key\" : \"string\", m[1]);\n        push(\"plain\", m[2] ?? \"\");\n      } else if (m[3]) push(\"number\", m[3]);\n      else if (m[4]) push(\"keyword\", m[4]);\n      else if (m[5]) push(\"punct\", m[5]);\n      else push(\"plain\", m[0]);\n    }\n    return out;\n  }\n  if (language === \"bash\" || language === \"sh\" || language === \"shell\") {\n    for (const line of code.split(/(\\n)/)) {\n      if (line === \"\\n\") push(\"plain\", line);\n      else if (/^\\s*#/.test(line)) push(\"comment\", line);\n      else\n        for (const [i, m] of [...line.matchAll(/(\"[^\"]*\"|'[^']*')|(\\s--?[\\w-]+)|(\\S+|\\s+)/g)].entries()) {\n          if (m[1]) push(\"string\", m[1]);\n          else if (m[2]) push(\"keyword\", m[2]);\n          else push(i === 0 || (i === 1 && /^\\s/.test(line)) ? \"function\" : \"plain\", m[0]);\n        }\n    }\n    return out;\n  }\n  const pattern = /(\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/)|(\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*'|`(?:\\\\.|[^`\\\\])*`)|(<\\/?)([A-Za-z][\\w.]*)|([A-Za-z_$][\\w-]*)(=)|\\b(\\d+(?:\\.\\d+)?)\\b|([A-Za-z_$][\\w$]*)(?=\\s*\\()|([A-Z][\\w$]*)|([A-Za-z_$][\\w$]*)|([\\s\\S])/g;\n  for (const m of code.matchAll(pattern)) {\n    if (m[1]) push(\"comment\", m[1]);\n    else if (m[2]) push(\"string\", m[2]);\n    else if (m[3]) {\n      push(\"punct\", m[3]);\n      push(\"tag\", m[4]);\n    } else if (m[5]) {\n      push(\"attr\", m[5]);\n      push(\"plain\", m[6]);\n    } else if (m[7]) push(\"number\", m[7]);\n    else if (m[8]) push(KEYWORDS.test(m[8]) ? \"keyword\" : \"function\", m[8]);\n    else if (m[9]) push(\"type\", m[9]);\n    else if (m[10]) push(KEYWORDS.test(m[10]) ? \"keyword\" : \"plain\", m[10]);\n    else push(\"plain\", m[11]);\n  }\n  return out.map((t) => (t.kind === \"string\" ? { ...t, kind: \"key\" as Kind } : t));\n}\n\n/** Draws the editor's indent guides: each two spaces of leading indentation becomes a cell with a hairline in it. */\nfunction withGuides(line: { kind: Kind; text: string }[]): { kind: Kind | \"guide\"; text: string }[] {\n  const first = line[0];\n  const indent = first?.kind === \"plain\" ? (first.text.match(/^ +/)?.[0].length ?? 0) : 0;\n  const levels = Math.floor(indent / 2);\n  if (!levels) return line;\n  const rest = first.text.slice(levels * 2);\n  return [...Array.from({ length: levels }, () => ({ kind: \"guide\" as const, text: \"  \" })), ...(rest ? [{ kind: \"plain\" as Kind, text: rest }] : []), ...line.slice(1)];\n}\n\n/**\n * The code itself. Pass the text as `code` and its `language` (json, tsx, ts, css, bash); line numbers show by default.\n * `wrap` breaks long lines instead of scrolling; `maxHeight` scrolls a long file inside the block.\n */\nfunction CodeBlockContent({ code, language = \"tsx\", lineNumbers = true, wrap = false, className, ...props }: Omit<React.ComponentProps<\"pre\">, \"children\"> & { code: string; language?: string; lineNumbers?: boolean; wrap?: boolean }) {\n  const lines = React.useMemo(() => {\n    const rows: { kind: Kind; text: string }[][] = [[]];\n    for (const token of tokenize(code.replace(/\\n$/, \"\"), language)) {\n      const parts = token.text.split(\"\\n\");\n      parts.forEach((part, i) => {\n        if (i > 0) rows.push([]);\n        if (part) rows[rows.length - 1].push({ kind: token.kind, text: part });\n      });\n    }\n    return rows;\n  }, [code, language]);\n  return (\n    <pre\n      data-slot=\"code-block-content\"\n      tabIndex={0}\n      className={cn(\"m-0 overflow-auto bg-code py-3 font-code text-body leading-5 outline-none [scrollbar-width:thin] [tab-size:2] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring\", className)}\n      {...props}\n    >\n      <code className={cn(\"grid\", wrap ? \"grid-cols-[minmax(0,1fr)]\" : \"min-w-max\")}>\n        {lines.map((line, i) => (\n          <span key={i} className={cn(\"grid\", lineNumbers ? \"grid-cols-[3rem_minmax(0,1fr)]\" : \"grid-cols-[minmax(0,1fr)] pl-4\", wrap && \"min-w-0\")}>\n            {lineNumbers && (\n              <span aria-hidden className={cn(\"pr-4 text-right select-none\", i === 0 ? \"text-code-line-active\" : \"text-code-line\")}>\n                {i + 1}\n              </span>\n            )}\n            <span className={cn(\"pr-4\", wrap ? \"whitespace-pre-wrap [overflow-wrap:anywhere]\" : \"whitespace-pre\")}>\n              {line.length ? withGuides(line).map((t, j) =>\n                t.kind === \"guide\" ? (\n                  <span key={j} className=\"relative inline-block w-[2ch]\">\n                    <span aria-hidden className=\"absolute inset-y-0 left-[1ch] w-px bg-line-code\" />\n                    {\"  \"}\n                  </span>\n                ) : (\n                  <span key={j} className={COLOR[t.kind]}>\n                    {t.text}\n                  </span>\n                ),\n              ) : \"\\u200b\"}\n            </span>\n          </span>\n        ))}\n      </code>\n    </pre>\n  );\n}\n\nexport { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle, tokenize };\n"
    }
  ]
}
