{
  "name": "code-syntax",
  "type": "registry:lib",
  "description": "Syntax tokenizer, editor palettes, and revealable code line renderer for the code scenes",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/bases/default/lib/code-syntax.tsx",
      "type": "registry:lib",
      "content": "/**\n * Minimal syntax tokenizer and editor palette shared by the code scenes\n * (`code-reveal`, `code-accordion`, `code-diff-wipe`).\n *\n * This is deliberately not a real parser. A video shows a handful of lines for\n * a couple of seconds, so the goal is a believable colour rhythm — strings,\n * comments, keywords, types, calls — not correctness on pathological input.\n * Everything runs per frame, so it stays regex-based and allocation-light.\n */\n\nexport type CodeTokenKind =\n  | \"plain\"\n  | \"comment\"\n  | \"string\"\n  | \"keyword\"\n  | \"number\"\n  | \"type\"\n  | \"call\"\n  | \"prop\"\n  | \"punct\"\n  | \"tag\";\n\nexport type CodeToken = {\n  text: string;\n  kind: CodeTokenKind;\n};\n\nexport type CodeTheme = {\n  page: string;\n  window: string;\n  header: string;\n  border: string;\n  highlight: string;\n  gutter: string;\n  fg: string;\n  dim: string;\n  faint: string;\n  band: string;\n  shadow: string;\n  token: Record<CodeTokenKind, string>;\n};\n\n/**\n * JetBrains Mono (and every other mono face worth shipping) advances 0.6em per\n * character. Carets and wipe masks are positioned arithmetically from this\n * rather than measured, so they stay exact in a headless render.\n */\nexport const MONO_ADVANCE = 0.6;\n\nexport const CODE_THEMES: Record<\"dark\" | \"light\", CodeTheme> = {\n  dark: {\n    page: \"#07070B\",\n    window: \"#0B0C11\",\n    header: \"rgba(255,255,255,0.035)\",\n    border: \"rgba(255,255,255,0.09)\",\n    highlight: \"rgba(255,255,255,0.14)\",\n    gutter: \"rgba(255,255,255,0.02)\",\n    fg: \"#D8DCE4\",\n    dim: \"#7A828F\",\n    faint: \"#4A5160\",\n    band: \"rgba(255,255,255,0.05)\",\n    shadow: \"rgba(0,0,0,0.55)\",\n    token: {\n      plain: \"#D8DCE4\",\n      comment: \"#5C6472\",\n      string: \"#9BD4A0\",\n      keyword: \"#C99BE8\",\n      number: \"#E8B86D\",\n      type: \"#7DD3E8\",\n      call: \"#8FB8F0\",\n      prop: \"#E8B86D\",\n      punct: \"#7A828F\",\n      tag: \"#7DD3E8\",\n    },\n  },\n  light: {\n    page: \"#F4F4F2\",\n    window: \"#FFFFFF\",\n    header: \"rgba(0,0,0,0.025)\",\n    border: \"rgba(0,0,0,0.10)\",\n    highlight: \"rgba(255,255,255,0.9)\",\n    gutter: \"rgba(0,0,0,0.018)\",\n    fg: \"#22252B\",\n    dim: \"#6B7280\",\n    faint: \"#A0A6B0\",\n    band: \"rgba(15,18,25,0.045)\",\n    shadow: \"rgba(15,18,25,0.18)\",\n    token: {\n      plain: \"#22252B\",\n      comment: \"#8A93A1\",\n      string: \"#1B7F4B\",\n      keyword: \"#8B3FBF\",\n      number: \"#9A5B12\",\n      type: \"#0E7490\",\n      call: \"#2563A8\",\n      prop: \"#9A5B12\",\n      punct: \"#6B7280\",\n      tag: \"#0E7490\",\n    },\n  },\n};\n\nconst KEYWORDS = new Set([\n  \"import\",\n  \"from\",\n  \"export\",\n  \"default\",\n  \"const\",\n  \"let\",\n  \"var\",\n  \"function\",\n  \"return\",\n  \"async\",\n  \"await\",\n  \"type\",\n  \"interface\",\n  \"class\",\n  \"extends\",\n  \"implements\",\n  \"new\",\n  \"if\",\n  \"else\",\n  \"for\",\n  \"while\",\n  \"switch\",\n  \"case\",\n  \"break\",\n  \"continue\",\n  \"try\",\n  \"catch\",\n  \"finally\",\n  \"throw\",\n  \"typeof\",\n  \"instanceof\",\n  \"in\",\n  \"of\",\n  \"as\",\n  \"null\",\n  \"undefined\",\n  \"true\",\n  \"false\",\n  \"this\",\n  \"void\",\n  \"yield\",\n  \"public\",\n  \"private\",\n  \"readonly\",\n  \"static\",\n  \"def\",\n  \"fn\",\n  \"pub\",\n  \"use\",\n  \"struct\",\n  \"enum\",\n  \"match\",\n  \"impl\",\n]);\n\n/** Ordered scanners — first match at the cursor wins. */\nconst SCANNERS: Array<{ kind: CodeTokenKind; re: RegExp }> = [\n  { kind: \"comment\", re: /^(\\/\\/[^\\n]*|#[^\\n]*)/ },\n  { kind: \"comment\", re: /^\\/\\*[\\s\\S]*?(\\*\\/|$)/ },\n  { kind: \"string\", re: /^([\"'`])(?:\\\\.|(?!\\1)[^\\\\])*(\\1|$)/ },\n  { kind: \"number\", re: /^(0x[\\da-fA-F]+|\\d+(\\.\\d+)?(e[+-]?\\d+)?)\\b/ },\n  { kind: \"plain\", re: /^[A-Za-z_$][\\w$]*/ },\n  { kind: \"punct\", re: /^[^\\sA-Za-z0-9_$]/ },\n  { kind: \"plain\", re: /^\\s+/ },\n];\n\nfunction classifyWord(\n  word: string,\n  before: string,\n  after: string,\n): CodeTokenKind {\n  if (KEYWORDS.has(word)) return \"keyword\";\n  if (/[<\\/]$/.test(before.trimEnd()) && /^[A-Z]/.test(word)) return \"tag\";\n  if (after.startsWith(\"(\")) return \"call\";\n  if (after.startsWith(\"=\") && !after.startsWith(\"==\")) return \"prop\";\n  if (/^[A-Z]/.test(word)) return \"type\";\n  return \"plain\";\n}\n\n/**\n * Split one line into coloured tokens. Block comments opened on an earlier\n * line are handled by the caller passing `inBlockComment` forward.\n */\nexport function tokenizeLine(\n  line: string,\n  inBlockComment = false,\n): { tokens: CodeToken[]; inBlockComment: boolean } {\n  const tokens: CodeToken[] = [];\n  let rest = line;\n  let offset = 0;\n  let block = inBlockComment;\n\n  if (block) {\n    const end = rest.indexOf(\"*/\");\n    if (end === -1) {\n      return { tokens: [{ text: rest, kind: \"comment\" }], inBlockComment: true };\n    }\n    tokens.push({ text: rest.slice(0, end + 2), kind: \"comment\" });\n    offset = end + 2;\n    rest = rest.slice(end + 2);\n    block = false;\n  }\n\n  while (rest.length > 0) {\n    let matched = false;\n\n    for (const scanner of SCANNERS) {\n      const match = scanner.re.exec(rest);\n      if (!match || match[0].length === 0) continue;\n\n      const text = match[0];\n      let kind = scanner.kind;\n\n      if (kind === \"comment\" && text.startsWith(\"/*\") && !text.endsWith(\"*/\")) {\n        block = true;\n      }\n      if (kind === \"plain\" && /^[A-Za-z_$]/.test(text)) {\n        kind = classifyWord(\n          text,\n          line.slice(0, offset),\n          rest.slice(text.length),\n        );\n      }\n\n      tokens.push({ text, kind });\n      offset += text.length;\n      rest = rest.slice(text.length);\n      matched = true;\n      break;\n    }\n\n    // Unreachable for well-formed input, but never spin on an unmatched char.\n    if (!matched) {\n      tokens.push({ text: rest[0], kind: \"plain\" });\n      offset += 1;\n      rest = rest.slice(1);\n    }\n  }\n\n  return { tokens, inBlockComment: block };\n}\n\n/** Tokenize a whole listing, carrying block-comment state across lines. */\nexport function tokenizeCode(lines: string[]): CodeToken[][] {\n  let block = false;\n  return lines.map((line) => {\n    const result = tokenizeLine(line, block);\n    block = result.inBlockComment;\n    return result.tokens;\n  });\n}\n\nexport type CodeLineProps = {\n  tokens: CodeToken[];\n  theme: CodeTheme;\n  /**\n   * Characters of the line to show. `undefined` shows all of it — pass a count\n   * to reveal the line as if it were being written.\n   */\n  reveal?: number;\n  /** Fades the whole line toward the theme's dim colour. */\n  muted?: boolean;\n};\n\n/**\n * One rendered line of code. Characters past `reveal` are kept in the DOM at\n * zero opacity so the line never reflows as it writes in.\n */\nexport const CodeLine: React.FC<CodeLineProps> = ({\n  tokens,\n  theme,\n  reveal,\n  muted = false,\n}) => {\n  let consumed = 0;\n\n  return (\n    <span style={{ whiteSpace: \"pre\" }}>\n      {tokens.map((token, index) => {\n        const start = consumed;\n        consumed += token.text.length;\n        const shown =\n          reveal === undefined\n            ? token.text.length\n            : Math.max(0, Math.min(token.text.length, reveal - start));\n        const color = muted ? theme.faint : theme.token[token.kind];\n\n        if (shown === token.text.length) {\n          return (\n            <span key={index} style={{ color }}>\n              {token.text}\n            </span>\n          );\n        }\n\n        return (\n          <span key={index} style={{ color }}>\n            {token.text.slice(0, shown)}\n            <span style={{ opacity: 0 }}>{token.text.slice(shown)}</span>\n          </span>\n        );\n      })}\n    </span>\n  );\n};\n\n/** Total character count of a tokenized line. */\nexport function lineLength(tokens: CodeToken[]): number {\n  return tokens.reduce((total, token) => total + token.text.length, 0);\n}\n"
    }
  ]
}