{
  "name": "code-reveal",
  "type": "registry:block",
  "description": "Editor window that writes a syntax-highlighted file character by character, then focuses the lines that matter",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "ai-composer-utils",
    "code-syntax",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/code-reveal/index.tsx",
      "type": "registry:block",
      "content": "import { loadFont } from \"@remotion/google-fonts/JetBrainsMono\";\nimport {\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { Caret, stageScale } from \"@/remotion/lib/ai-composer-utils\";\nimport {\n  CODE_THEMES,\n  CodeLine,\n  lineLength,\n  MONO_ADVANCE,\n  tokenizeCode,\n} from \"@/remotion/lib/code-syntax\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\", \"700\"],\n  subsets: [\"latin\"],\n});\n\n/** Reference stage the editor is laid out against, then uniformly scaled. */\nconst REF_WIDTH = 1280;\nconst REF_HEIGHT = 720;\n\nconst WINDOW_WIDTH = 940;\nconst HEADER_HEIGHT = 46;\nconst BODY_PADDING = 24;\nconst ROW_HEIGHT = 34;\nconst FONT_SIZE = 21;\nconst GUTTER_WIDTH = 62;\nconst CODE_INSET = 20;\n/** Longest listing shown at once — anything past this scrolls. */\nconst MAX_VISIBLE_ROWS = 10;\n\n/** Characters per second the code is written at. */\nconst WRITE_CPS = 55;\n/** Beat spent moving to the next line, in seconds. */\nconst NEWLINE_PAUSE = 0.07;\n\nconst DEFAULT_CODE = `import { CodeReveal } from \"@/remotion/scenes/code-reveal\";\n\nexport const Explainer = () => (\n  <CodeReveal\n    title=\"pipeline.ts\"\n    highlightedLines={[4, 5]}\n  />\n);`;\n\nexport type CodeRevealProps = {\n  /** Source shown in the editor. Surrounding blank lines are trimmed. */\n  code?: string;\n  /** 1-based line numbers focused once the listing finishes writing. */\n  highlightedLines?: number[];\n  /** Filename on the editor tab. */\n  title?: string;\n  /** Language badge on the right of the header. */\n  language?: string;\n  /** First line number in the gutter — set it when showing an excerpt. */\n  startLine?: number;\n  /** Hides the gutter entirely when false. */\n  showLineNumbers?: boolean;\n  accentColor?: string;\n  /** Overrides the page background behind the window. */\n  backgroundColor?: string;\n  theme?: \"dark\" | \"light\";\n  /** Animation speed multiplier. */\n  speed?: number;\n};\n\nconst CHROME_LIGHTS = [\"#FF5F57\", \"#FEBC2E\", \"#28C840\"] as const;\n\n/**\n * Code reveal staged as an editor actually writing the file: a write head runs\n * through the listing character by character with the caret riding its tip,\n * then — once the file is complete — attention lands on the lines that matter\n * and the rest of the listing recedes.\n */\nexport const CodeReveal: React.FC<CodeRevealProps> = ({\n  code = DEFAULT_CODE,\n  highlightedLines = [],\n  title = \"explainer.tsx\",\n  language = \"tsx\",\n  startLine = 1,\n  showLineNumbers = true,\n  accentColor = \"#E8B86D\",\n  backgroundColor,\n  theme = \"dark\",\n  speed = 1,\n}) => {\n  const rawFrame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const palette = CODE_THEMES[theme];\n  const page = backgroundColor ?? palette.page;\n  const scale = stageScale(width, height, REF_WIDTH, REF_HEIGHT);\n  const frame = rawFrame * speed;\n\n  const seconds = (value: number) => value * fps;\n  const clamp = {\n    extrapolateLeft: \"clamp\" as const,\n    extrapolateRight: \"clamp\" as const,\n  };\n\n  const lines = code.replace(/\\s+$/, \"\").replace(/^\\n+/, \"\").split(\"\\n\");\n  const tokenLines = tokenizeCode(lines);\n  const focused = new Set(highlightedLines);\n  const hasFocus = highlightedLines.length > 0;\n\n  // --- Write plan ---------------------------------------------------------\n  // Leading indentation is free — editors auto-indent, so only the characters\n  // a person actually types cost time. Each row carries a short newline beat.\n  const writeStart = seconds(0.4);\n  let cursorFrame = writeStart;\n  const plan = tokenLines.map((tokens, index) => {\n    const raw = lines[index];\n    const indent = raw.length - raw.trimStart().length;\n    const cost = Math.max(0, lineLength(tokens) - indent);\n    const startsAt = cursorFrame;\n    const endsAt = startsAt + (cost / WRITE_CPS) * fps;\n    cursorFrame = endsAt + seconds(NEWLINE_PAUSE);\n    return { tokens, indent, cost, startsAt, endsAt };\n  });\n\n  const writeEnd = plan[plan.length - 1]?.endsAt ?? writeStart;\n  const focusAt = writeEnd + seconds(0.34);\n\n  /** Characters of a row written by now, indentation included. */\n  const writtenChars = (row: (typeof plan)[number]) =>\n    row.indent +\n    Math.round(\n      interpolate(\n        frame,\n        [row.startsAt, Math.max(row.endsAt, row.startsAt + 1)],\n        [0, row.cost],\n        clamp,\n      ),\n    );\n\n  // --- Window -------------------------------------------------------------\n  const enter = spring({\n    fps,\n    frame,\n    config: { damping: 18, stiffness: 120, mass: 0.8 },\n  });\n  const chromeIn = interpolate(frame, [seconds(0.1), seconds(0.4)], [0, 1], {\n    easing: EASING.enter,\n    ...clamp,\n  });\n  const focusProgress = hasFocus\n    ? interpolate(frame, [focusAt, focusAt + seconds(0.5)], [0, 1], {\n        easing: EASING.enter,\n        ...clamp,\n      })\n    : 0;\n\n  // --- Caret --------------------------------------------------------------\n  const charWidth = FONT_SIZE * MONO_ADVANCE;\n  const writing = plan.findIndex((row) => frame < row.endsAt);\n  const caretRow = writing === -1 ? plan.length - 1 : writing;\n  const caretPlan = plan[caretRow];\n  const caretChars = caretPlan ? writtenChars(caretPlan) : 0;\n  // While writing the caret is solid; once the file is done it rests and blinks.\n  const caretResting = frame >= writeEnd;\n\n  // --- Scroll -------------------------------------------------------------\n  // The viewport follows the write head fractionally, so a long listing pans\n  // instead of jumping a whole line-height as each row starts.\n  const visibleRows = Math.min(plan.length, MAX_VISIBLE_ROWS);\n  const bodyHeight = visibleRows * ROW_HEIGHT + BODY_PADDING * 2;\n  // Rows count in fractionally as they open, so a long listing pans by that\n  // fraction instead of jumping a whole line-height when a row starts.\n  const openRows = plan.reduce(\n    (total, row) =>\n      total +\n      interpolate(frame, [row.startsAt, row.startsAt + 6], [0, 1], {\n        easing: EASING.enter,\n        ...clamp,\n      }),\n    0,\n  );\n  const scrollY = Math.max(0, openRows - visibleRows) * ROW_HEIGHT;\n  const codeInset = showLineNumbers ? GUTTER_WIDTH + CODE_INSET : BODY_PADDING;\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: page,\n        fontFamily,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        overflow: \"hidden\",\n        position: \"relative\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(ellipse 70% 50% at 50% 38%, ${accentColor}1C, transparent 70%)`,\n          opacity: chromeIn * (0.55 + focusProgress * 0.45),\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background:\n            theme === \"dark\"\n              ? \"radial-gradient(ellipse 120% 90% at 50% 50%, transparent 45%, rgba(0,0,0,0.55) 100%)\"\n              : \"radial-gradient(ellipse 120% 90% at 50% 50%, transparent 55%, rgba(15,18,25,0.07) 100%)\",\n        }}\n      />\n\n      <div\n        style={{\n          width: WINDOW_WIDTH,\n          transform: `scale(${scale * interpolate(enter, [0, 1], [0.965, 1])}) translateY(${interpolate(enter, [0, 1], [26, 0])}px)`,\n          opacity: enter,\n          borderRadius: 16,\n          background: palette.window,\n          border: `1px solid ${palette.border}`,\n          boxShadow: `inset 0 1px 0 ${palette.highlight}, 0 34px 90px ${palette.shadow}`,\n          overflow: \"hidden\",\n          position: \"relative\",\n        }}\n      >\n        {/* Header chrome */}\n        <div\n          style={{\n            height: HEADER_HEIGHT,\n            display: \"flex\",\n            alignItems: \"center\",\n            padding: \"0 18px\",\n            borderBottom: `1px solid ${palette.border}`,\n            background: palette.header,\n            opacity: chromeIn,\n            position: \"relative\",\n          }}\n        >\n          <div style={{ display: \"flex\", gap: 8 }}>\n            {CHROME_LIGHTS.map((color) => (\n              <div\n                key={color}\n                style={{\n                  width: 11,\n                  height: 11,\n                  borderRadius: \"50%\",\n                  background: color,\n                }}\n              />\n            ))}\n          </div>\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              gap: 9,\n              pointerEvents: \"none\",\n            }}\n          >\n            <div\n              style={{\n                width: 7,\n                height: 7,\n                borderRadius: \"50%\",\n                background: accentColor,\n                // The unsaved-changes dot settles once the file is written.\n                opacity: interpolate(\n                  frame,\n                  [writeEnd, writeEnd + seconds(0.4)],\n                  [1, 0.2],\n                  clamp,\n                ),\n              }}\n            />\n            <span\n              style={{\n                color: palette.dim,\n                fontSize: 15,\n                fontWeight: 500,\n                letterSpacing: 0.2,\n              }}\n            >\n              {title}\n            </span>\n          </div>\n          <span\n            style={{\n              marginLeft: \"auto\",\n              color: palette.faint,\n              fontSize: 13,\n              letterSpacing: 0.6,\n              textTransform: \"uppercase\",\n            }}\n          >\n            {language}\n          </span>\n        </div>\n\n        {/* Editor body */}\n        <div\n          style={{\n            height: bodyHeight,\n            overflow: \"hidden\",\n            position: \"relative\",\n          }}\n        >\n          {showLineNumbers ? (\n            <div\n              style={{\n                position: \"absolute\",\n                top: 0,\n                bottom: 0,\n                left: 0,\n                width: GUTTER_WIDTH,\n                background: palette.gutter,\n                borderRight: `1px solid ${palette.border}`,\n              }}\n            />\n          ) : null}\n\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              paddingTop: BODY_PADDING,\n              paddingBottom: BODY_PADDING,\n            }}\n          >\n            <div\n              style={{\n                transform: `translateY(${-scrollY}px)`,\n                fontSize: FONT_SIZE,\n                lineHeight: `${ROW_HEIGHT}px`,\n                position: \"relative\",\n              }}\n            >\n              {plan.map((row, index) => {\n                const started = frame >= row.startsAt;\n                const written = started ? writtenChars(row) : 0;\n                const complete = written >= row.indent + row.cost;\n                const isFocused = focused.has(index + startLine);\n                // Unfocused lines recede rather than vanish — the reader should\n                // still feel the shape of the file around the point.\n                const recede = hasFocus && !isFocused ? focusProgress : 0;\n\n                return (\n                  <div\n                    key={`${index}-${lines[index]}`}\n                    style={{\n                      height: ROW_HEIGHT,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      position: \"relative\",\n                      opacity: started\n                        ? interpolate(recede, [0, 1], [1, 0.34])\n                        : 0,\n                    }}\n                  >\n                    {isFocused ? (\n                      <>\n                        <div\n                          style={{\n                            position: \"absolute\",\n                            inset: 0,\n                            background: `${accentColor}14`,\n                            opacity: focusProgress,\n                          }}\n                        />\n                        <div\n                          style={{\n                            position: \"absolute\",\n                            left: 0,\n                            top: 3,\n                            bottom: 3,\n                            width: 3,\n                            borderRadius: 3,\n                            background: accentColor,\n                            opacity: focusProgress,\n                            transform: `scaleY(${0.35 + focusProgress * 0.65})`,\n                          }}\n                        />\n                      </>\n                    ) : null}\n                    {showLineNumbers ? (\n                      <span\n                        style={{\n                          width: GUTTER_WIDTH,\n                          flexShrink: 0,\n                          paddingRight: 16,\n                          textAlign: \"right\",\n                          color: palette.faint,\n                          fontVariantNumeric: \"tabular-nums\",\n                          fontSize: FONT_SIZE * 0.82,\n                          position: \"relative\",\n                        }}\n                      >\n                        {index + startLine}\n                        {isFocused ? (\n                          // The number only takes the accent as focus lands —\n                          // colouring it up front would give the point away.\n                          <span\n                            style={{\n                              position: \"absolute\",\n                              inset: 0,\n                              paddingRight: 16,\n                              color: accentColor,\n                              opacity: focusProgress,\n                            }}\n                          >\n                            {index + startLine}\n                          </span>\n                        ) : null}\n                      </span>\n                    ) : null}\n                    <span\n                      style={{\n                        paddingLeft: showLineNumbers ? CODE_INSET : BODY_PADDING,\n                        position: \"relative\",\n                      }}\n                    >\n                      <CodeLine\n                        tokens={row.tokens}\n                        theme={palette}\n                        reveal={complete ? undefined : written}\n                      />\n                    </span>\n                  </div>\n                );\n              })}\n\n              {/* Caret rides the write head, then rests at the end of the file */}\n              {frame >= writeStart && caretPlan ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    top: caretRow * ROW_HEIGHT,\n                    left: codeInset + caretChars * charWidth,\n                    height: ROW_HEIGHT,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                  }}\n                >\n                  <Caret\n                    color={accentColor}\n                    width={charWidth * 0.9}\n                    height={FONT_SIZE * 1.15}\n                    radius={2}\n                    blink={caretResting}\n                    blinkPerSecond={1.6}\n                    speed={speed}\n                  />\n                </div>\n              ) : null}\n            </div>\n          </div>\n\n          {/* Fades so scrolled rows dissolve rather than clip */}\n          <div\n            style={{\n              position: \"absolute\",\n              top: 0,\n              left: 0,\n              right: 0,\n              height: BODY_PADDING,\n              background: `linear-gradient(${palette.window}, ${palette.window}00)`,\n              opacity: Math.min(1, scrollY / 10),\n              pointerEvents: \"none\",\n            }}\n          />\n          <div\n            style={{\n              position: \"absolute\",\n              bottom: 0,\n              left: 0,\n              right: 0,\n              height: BODY_PADDING,\n              background: `linear-gradient(${palette.window}00, ${palette.window})`,\n              pointerEvents: \"none\",\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core",
    "tags": [
      "code"
    ]
  }
}