{
  "name": "code-accordion",
  "type": "registry:block",
  "description": "Stepped walkthrough where each section opens, writes its code, is checked off, and closes behind the next",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "ai-composer-utils",
    "code-syntax",
    "motion-tokens"
  ],
  "files": [
    {
      "path": "registry/bases/default/scenes/code-accordion/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 { 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 panel is laid out against, then uniformly scaled. */\nconst REF_WIDTH = 1280;\nconst REF_HEIGHT = 720;\n\nconst PANEL_WIDTH = 880;\nconst HEADER_HEIGHT = 64;\nconst CODE_ROW_HEIGHT = 32;\nconst CODE_FONT_SIZE = 19;\nconst CODE_PADDING = 16;\nconst CODE_INSET = 62;\n\n/** Characters per second each step's code is written at. */\nconst WRITE_CPS = 70;\n\nexport type AccordionSection = {\n  /** Step label shown on the collapsed row. */\n  title: string;\n  /** Code revealed while the step is open. */\n  code: string;\n  /** Optional right-aligned hint, e.g. a filename. */\n  meta?: string;\n};\n\nexport type CodeAccordionProps = {\n  /** Steps played in order. */\n  sections?: AccordionSection[];\n  /**\n   * Plays a single step instead of walking the list — it opens and stays open.\n   * Leave it unset to step through every section in order.\n   */\n  activeIndex?: number;\n  /** Label above the steps. */\n  title?: string;\n  /** Seconds an opened step is held before it closes. */\n  holdSeconds?: number;\n  accentColor?: string;\n  /** Overrides the page background behind the panel. */\n  backgroundColor?: string;\n  theme?: \"dark\" | \"light\";\n  /** Animation speed multiplier. */\n  speed?: number;\n};\n\nconst DEFAULT_SECTIONS: AccordionSection[] = [\n  {\n    title: \"Install the scene\",\n    code: \"npx remotion-ui add code-accordion\",\n    meta: \"terminal\",\n  },\n  {\n    title: \"Import it\",\n    code: 'import { CodeAccordion } from \"@/remotion/scenes/code-accordion\";',\n    meta: \"Root.tsx\",\n  },\n  {\n    title: \"Render the steps\",\n    code: \"<CodeAccordion sections={sections} />\",\n    meta: \"Root.tsx\",\n  },\n];\n\n/** Drawn, not typed — a chevron glyph is not guaranteed in the mono face. */\nconst Chevron: React.FC<{ size: number; color: string; turn: number }> = ({\n  size,\n  color,\n  turn,\n}) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    style={{ transform: `rotate(${turn}deg)` }}\n  >\n    <path\n      d=\"M8.5 5.5L15.5 12l-7 6.5\"\n      stroke={color}\n      strokeWidth={2.6}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\n/** Check that strokes itself on as a step is put behind us. */\nconst Check: React.FC<{ size: number; color: string; progress: number }> = ({\n  size,\n  color,\n  progress,\n}) => {\n  const length = 22;\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <path\n        d=\"M4.5 12.5l5 5 10-11\"\n        stroke={color}\n        strokeWidth={2.75}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeDasharray={length}\n        strokeDashoffset={length * (1 - progress)}\n      />\n    </svg>\n  );\n};\n\n/**\n * Code accordion staged as the walkthrough it stands in for: each step opens in\n * turn, writes its code, is checked off, and closes behind the next one — so\n * the viewer reads a sequence of moves rather than one frozen open drawer.\n */\nexport const CodeAccordion: React.FC<CodeAccordionProps> = ({\n  sections = DEFAULT_SECTIONS,\n  activeIndex,\n  title = \"Add it to your project\",\n  holdSeconds = 0.75,\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  // A single pinned step never closes — it is the whole scene.\n  const pinned =\n    activeIndex === undefined\n      ? undefined\n      : Math.min(Math.max(activeIndex, 0), Math.max(sections.length - 1, 0));\n\n  // --- Step plan ----------------------------------------------------------\n  let cursorFrame = seconds(0.45);\n  const plan = sections.map((section, index) => {\n    const lines = section.code.replace(/\\s+$/, \"\").split(\"\\n\");\n    const tokens = tokenizeCode(lines);\n    const chars = tokens.reduce((total, line) => total + lineLength(line), 0);\n    const plays = pinned === undefined || pinned === index;\n\n    if (!plays) {\n      return {\n        section,\n        lines,\n        tokens,\n        plays,\n        opensAt: 0,\n        writeStart: 0,\n        closesAt: 0,\n      };\n    }\n\n    const opensAt = cursorFrame;\n    // Writing waits for the fold to finish opening — text emerging through a\n    // moving clip edge reads as a glitch, not as typing.\n    const writeStart = opensAt + seconds(0.36);\n    const writeEnd = writeStart + (chars / WRITE_CPS) * fps;\n    // Stepping through, a step closes to make room; a pinned one stays open.\n    const closesAt =\n      pinned === undefined\n        ? writeEnd + seconds(holdSeconds)\n        : Number.POSITIVE_INFINITY;\n    cursorFrame = closesAt + seconds(0.22);\n\n    return { section, lines, tokens, plays, opensAt, writeStart, closesAt };\n  });\n\n  const panelEnter = spring({\n    fps,\n    frame,\n    config: { damping: 18, stiffness: 120, mass: 0.8 },\n  });\n  const titleIn = interpolate(frame, [seconds(0.12), seconds(0.5)], [0, 1], {\n    easing: EASING.enter,\n    ...clamp,\n  });\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: titleIn,\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: PANEL_WIDTH,\n          transform: `scale(${scale * interpolate(panelEnter, [0, 1], [0.965, 1])}) translateY(${interpolate(panelEnter, [0, 1], [26, 0])}px)`,\n          opacity: panelEnter,\n        }}\n      >\n        {title ? (\n          <div\n            style={{\n              color: palette.dim,\n              fontSize: 17,\n              fontWeight: 500,\n              letterSpacing: 0.4,\n              marginBottom: 16,\n              paddingLeft: 4,\n              opacity: titleIn,\n            }}\n          >\n            {title}\n          </div>\n        ) : null}\n\n        <div\n          style={{\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          }}\n        >\n          {plan.map((step, index) => {\n            const rowIn = interpolate(\n              frame,\n              [seconds(0.12) + index * 4, seconds(0.42) + index * 4],\n              [0, 1],\n              { easing: EASING.enter, ...clamp },\n            );\n            const open = step.plays\n              ? interpolate(\n                  frame,\n                  [step.opensAt, step.opensAt + seconds(0.32)],\n                  [0, 1],\n                  { easing: EASING.enter, ...clamp },\n                )\n              : 0;\n            // A pinned step's `closesAt` is Infinity — it never closes, and\n            // an infinite input range would throw.\n            const close =\n              step.plays && Number.isFinite(step.closesAt)\n                ? interpolate(\n                    frame,\n                    [step.closesAt, step.closesAt + seconds(0.28)],\n                    [0, 1],\n                    { easing: EASING.exit, ...clamp },\n                  )\n                : 0;\n            const expansion = open * (1 - close);\n            const done = close > 0;\n            const active = expansion > 0.02;\n            const contentHeight =\n              step.lines.length * CODE_ROW_HEIGHT + CODE_PADDING * 2;\n\n            return (\n              <div\n                key={step.section.title}\n                style={{\n                  borderBottom:\n                    index < plan.length - 1\n                      ? `1px solid ${palette.border}`\n                      : \"none\",\n                  opacity: rowIn,\n                  background: active ? palette.gutter : \"transparent\",\n                }}\n              >\n                <div\n                  style={{\n                    height: HEADER_HEIGHT,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    padding: \"0 22px\",\n                    gap: 16,\n                  }}\n                >\n                  <span\n                    style={{\n                      width: 24,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      justifyContent: \"center\",\n                      flexShrink: 0,\n                    }}\n                  >\n                    {done ? (\n                      <Check\n                        size={20}\n                        color={palette.token.string}\n                        progress={close}\n                      />\n                    ) : (\n                      <Chevron\n                        size={18}\n                        color={active ? accentColor : palette.faint}\n                        turn={expansion * 90}\n                      />\n                    )}\n                  </span>\n                  <span\n                    style={{\n                      color: active\n                        ? palette.fg\n                        : done\n                          ? palette.dim\n                          : palette.faint,\n                      fontSize: 21,\n                      fontWeight: 500,\n                      letterSpacing: 0.1,\n                    }}\n                  >\n                    {step.section.title}\n                  </span>\n                  {step.section.meta ? (\n                    <span\n                      style={{\n                        marginLeft: \"auto\",\n                        color: palette.faint,\n                        fontSize: 14,\n                        letterSpacing: 0.4,\n                        opacity: active ? 1 : 0.55,\n                      }}\n                    >\n                      {step.section.meta}\n                    </span>\n                  ) : null}\n                </div>\n\n                <div\n                  style={{\n                    height: contentHeight * expansion,\n                    overflow: \"hidden\",\n                    background: `${accentColor}0A`,\n                  }}\n                >\n                  <div\n                    style={{\n                      paddingTop: CODE_PADDING,\n                      paddingBottom: CODE_PADDING,\n                      paddingLeft: CODE_INSET,\n                      fontSize: CODE_FONT_SIZE,\n                      lineHeight: `${CODE_ROW_HEIGHT}px`,\n                      // The block slides up out of the fold rather than\n                      // stretching, so the type never distorts.\n                      transform: `translateY(${(expansion - 1) * 10}px)`,\n                    }}\n                  >\n                    {step.tokens.map((tokens, lineIndex) => {\n                      const raw = step.lines[lineIndex];\n                      const indent = raw.length - raw.trimStart().length;\n                      const cost = Math.max(0, raw.length - indent);\n                      const before = step.lines\n                        .slice(0, lineIndex)\n                        .reduce((total, line) => total + line.length, 0);\n                      const lineStart =\n                        step.writeStart + (before / WRITE_CPS) * fps;\n                      const written =\n                        indent +\n                        Math.round(\n                          interpolate(\n                            frame,\n                            [lineStart, lineStart + (cost / WRITE_CPS) * fps + 1],\n                            [0, cost],\n                            clamp,\n                          ),\n                        );\n\n                      const complete = written >= raw.length;\n\n                      return (\n                        <div\n                          key={`${lineIndex}-${raw}`}\n                          style={{ display: \"flex\", alignItems: \"center\" }}\n                        >\n                          <span style={{ position: \"relative\" }}>\n                            <CodeLine\n                              tokens={tokens}\n                              theme={palette}\n                              reveal={complete ? undefined : written}\n                            />\n                            {frame >= lineStart && !complete ? (\n                              <span\n                                style={{\n                                  position: \"absolute\",\n                                  top: \"50%\",\n                                  left: written * CODE_FONT_SIZE * MONO_ADVANCE,\n                                  width: CODE_FONT_SIZE * MONO_ADVANCE * 0.9,\n                                  height: CODE_FONT_SIZE * 1.15,\n                                  transform: \"translateY(-50%)\",\n                                  borderRadius: 2,\n                                  background: accentColor,\n                                }}\n                              />\n                            ) : null}\n                          </span>\n                        </div>\n                      );\n                    })}\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "blocks",
    "drive": "time",
    "tier": "core",
    "tags": [
      "code"
    ]
  }
}