{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shieldcn-plus",
  "title": "shieldcn — Introducing shieldcn Plus",
  "description": "remocn demo composition \"shieldcn — Introducing shieldcn Plus\" — installs the full Remotion composition. Generated with AI from the prompt in demos/shieldcn-plus/prompt.md.",
  "dependencies": [
    "@paper-design/shaders-react",
    "@remotion/google-fonts",
    "@remotion/transitions",
    "culori",
    "remotion"
  ],
  "registryDependencies": [
    "https://remocn.dev/r/blur-in.json",
    "https://remocn.dev/r/caret.json",
    "https://remocn.dev/r/line-by-line-slide.json",
    "https://remocn.dev/r/remocn-ui.json",
    "https://remocn.dev/r/rolling-number.json",
    "https://remocn.dev/r/shader-warp.json",
    "https://remocn.dev/r/short-slide-right.json"
  ],
  "files": [
    {
      "path": "src/components/remocn/claude-chat.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadBodyFont } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { Caret } from \"@/components/remocn/caret\";\nimport { useTypewriter } from \"@/lib/remocn-ui\";\n\nconst { fontFamily: SANS_FAMILY } = loadBodyFont();\n\nexport interface ClaudeChatProps {\n  greeting?: string;\n  placeholder?: string;\n  prompt?: string;\n  modelName?: string;\n  modelTier?: string;\n  accentColor?: string;\n  speed?: number;\n}\n\ninterface Theme {\n  page: string;\n  cardBg: string;\n  cardBorder: string;\n  fg: string;\n  fgMuted: string;\n  placeholder: string;\n  iconBtnBorder: string;\n}\n\nexport const THEMES: Record<\"light\" | \"dark\", Theme> = {\n  light: {\n    page: \"#F5F4EF\",\n    cardBg: \"#FFFFFF\",\n    cardBorder: \"#E8E5DD\",\n    fg: \"#1F1E1D\",\n    fgMuted: \"#73726C\",\n    placeholder: \"#A3A097\",\n    iconBtnBorder: \"#E0DDD4\",\n  },\n  dark: {\n    page: \"#262624\",\n    cardBg: \"#1F1E1D\",\n    cardBorder: \"#3A3936\",\n    fg: \"#F0EEE6\",\n    fgMuted: \"#9B9892\",\n    placeholder: \"#73726C\",\n    iconBtnBorder: \"#3A3936\",\n  },\n};\n\nexport const TYPING_START_FRAME = 42;\n\nexport const TYPING_CPS = 22;\n\nexport function morphProgressAt(\n  frame: number,\n  opts: { startFrame?: number; fps: number; speed: number },\n): number {\n  const startFrame = opts.startFrame ?? TYPING_START_FRAME;\n  const value = spring({\n    fps: opts.fps,\n    frame: frame * opts.speed - startFrame,\n    config: { damping: 14, stiffness: 200, mass: 0.6 },\n  });\n  return Math.max(0, Math.min(value, 1));\n}\n\nexport function introBounceIn(\n  frame: number,\n  fps: number,\n): { translateY: number; scale: number } {\n  const s = spring({\n    fps,\n    frame,\n    config: { damping: 14, stiffness: 110, mass: 0.7 },\n  });\n  const translateY = interpolate(s, [0, 1], [28, 0]);\n  const scale = interpolate(s, [0, 1], [0.97, 1]);\n  return { translateY, scale };\n}\n\nexport function fadeUpAt(\n  frame: number,\n  range: [number, number],\n): { opacity: number; translateY: number } {\n  const opts = {\n    extrapolateLeft: \"clamp\" as const,\n    extrapolateRight: \"clamp\" as const,\n  };\n  return {\n    opacity: interpolate(frame, range, [0, 1], opts),\n    translateY: interpolate(frame, range, [12, 0], opts),\n  };\n}\n\nfunction ChevronDown({ size, color }: { size: number; color: string }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <title>Expand</title>\n      <path\n        d=\"M6 9l6 6 6-6\"\n        stroke={color}\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction PlusIcon({ size, color }: { size: number; color: string }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <title>Add</title>\n      <path\n        d=\"M12 5v14M5 12h14\"\n        stroke={color}\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction MicIcon({ size, color }: { size: number; color: string }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <title>Voice input</title>\n      <rect\n        x={9}\n        y={3}\n        width={6}\n        height={11}\n        rx={3}\n        stroke={color}\n        strokeWidth={1.8}\n      />\n      <path\n        d=\"M5.5 11a6.5 6.5 0 0013 0M12 17.5V21M9 21h6\"\n        stroke={color}\n        strokeWidth={1.8}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction WaveformIcon({ size, color }: { size: number; color: string }) {\n  const bars = [\n    { x: 4, h: 8 },\n    { x: 9, h: 16 },\n    { x: 14, h: 12 },\n    { x: 19, h: 20 },\n  ];\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <title>Voice</title>\n      {bars.map((bar) => (\n        <rect\n          key={bar.x}\n          x={bar.x - 1}\n          y={(24 - bar.h) / 2}\n          width={2.4}\n          height={bar.h}\n          rx={1.2}\n          fill={color}\n        />\n      ))}\n    </svg>\n  );\n}\n\nfunction SendIcon({ size }: { size: number }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\">\n      <title>Send</title>\n      <path\n        d=\"M12 19V5M12 5l-6 6M12 5l6 6\"\n        stroke=\"#FFFFFF\"\n        strokeWidth={2.2}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction IconButton({\n  size,\n  border,\n  children,\n}: {\n  size: number;\n  border: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div\n      style={{\n        width: size,\n        height: size,\n        borderRadius: \"100%\",\n        border: `1px solid ${border}`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexShrink: 0,\n      }}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function ClaudeChat({\n  placeholder = \"Try: draft an email · summarize a doc · plan your week\",\n  prompt = \"Draft a launch tweet for our new release\",\n  modelName = \"Opus 4.8\",\n  modelTier = \"Max\",\n  accentColor = \"#D97757\",\n  speed = 1,\n}: ClaudeChatProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const t = THEMES.dark;\n\n  const refW = 1280;\n  const refH = 720;\n  const stageScale = Math.min(width / refW, height / refH);\n\n  const tw = useTypewriter(prompt, {\n    cps: TYPING_CPS,\n    speed,\n    startFrame: TYPING_START_FRAME,\n  });\n  const visibleText = tw.text;\n  const showText = tw.count > 0;\n  const morph = morphProgressAt(frame, { fps, speed });\n\n  const intro = introBounceIn(frame * speed, fps);\n  const cardFade = fadeUpAt(frame * speed, [6, 22]);\n\n  const cardWidth = 860;\n  const cardLeft = (refW - cardWidth) / 2;\n  const iconBtnSize = 36;\n  const morphSize = 40;\n\n  return (\n    <AbsoluteFill style={{ background: \"transparent\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: refW,\n          height: refH,\n          transform: `translate(-50%, -50%) scale(${stageScale})`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: cardLeft,\n            top: 300,\n            width: cardWidth,\n            background: t.cardBg,\n            border: `1px solid ${t.cardBorder}`,\n            borderRadius: 24,\n            boxShadow: \"0 8px 30px -12px rgba(31,30,29,0.12)\",\n            opacity: cardFade.opacity,\n            transform: `translateY(${cardFade.translateY + intro.translateY}px) scale(${intro.scale})`,\n            transformOrigin: \"center top\",\n          }}\n        >\n          <div\n            style={{\n              padding: \"26px 28px\",\n              minHeight: 58,\n              fontFamily: SANS_FAMILY,\n              fontSize: 21,\n              lineHeight: 1.3,\n              display: \"flex\",\n              alignItems: \"center\",\n            }}\n          >\n            {showText ? (\n              <span style={{ color: t.fg }}>\n                {visibleText}\n                <Caret\n                  color={t.fg}\n                  blink={!tw.typing}\n                  speed={speed}\n                  height={24}\n                  radius={0}\n                  marginLeft={1}\n                  style={{\n                    verticalAlign: \"text-bottom\",\n                    transform: \"translateY(3px)\",\n                  }}\n                />\n              </span>\n            ) : (\n              <span\n                style={{\n                  color: t.placeholder,\n                  display: \"inline-flex\",\n                  alignItems: \"center\",\n                }}\n              >\n                <Caret\n                  color={t.fg}\n                  blink={!tw.typing}\n                  speed={speed}\n                  height={24}\n                  radius={0}\n                  marginLeft={1}\n                  style={{\n                    verticalAlign: \"text-bottom\",\n                    transform: \"translateY(3px)\",\n                  }}\n                />\n                <span style={{ marginLeft: 2 }}>{placeholder}</span>\n              </span>\n            )}\n          </div>\n\n          <div\n            style={{\n              padding: \"14px 18px\",\n              display: \"flex\",\n              justifyContent: \"space-between\",\n              alignItems: \"center\",\n            }}\n          >\n            <IconButton size={iconBtnSize} border={t.iconBtnBorder}>\n              <PlusIcon size={20} color={t.fg} />\n            </IconButton>\n\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 14,\n              }}\n            >\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  gap: 7,\n                }}\n              >\n                <span\n                  style={{\n                    fontFamily: SANS_FAMILY,\n                    fontSize: 18,\n                    fontWeight: 500,\n                    color: t.fg,\n                  }}\n                >\n                  {modelName}\n                </span>\n                <span\n                  style={{\n                    fontFamily: SANS_FAMILY,\n                    fontSize: 18,\n                    fontWeight: 400,\n                    color: t.fgMuted,\n                  }}\n                >\n                  {modelTier}\n                </span>\n                <ChevronDown size={16} color={t.fgMuted} />\n              </div>\n\n              <IconButton size={iconBtnSize} border={t.iconBtnBorder}>\n                <MicIcon size={20} color={t.fg} />\n              </IconButton>\n\n              <div\n                style={{\n                  position: \"relative\",\n                  width: morphSize,\n                  height: morphSize,\n                  flexShrink: 0,\n                }}\n              >\n                <div\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                    borderRadius: \"100%\",\n                    border: `1px solid ${t.iconBtnBorder}`,\n                    opacity: 1 - morph,\n                    transform: `scale(${1 - 0.1 * morph})`,\n                  }}\n                >\n                  <WaveformIcon size={22} color={t.fg} />\n                </div>\n                <div\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                    borderRadius: 10,\n                    background: accentColor,\n                    opacity: morph,\n                    transform: `scale(${0.8 + 0.2 * morph})`,\n                  }}\n                >\n                  <SendIcon size={22} />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/claude-chat.tsx"
    },
    {
      "path": "src/components/remocn/rgb-glitch-text.tsx",
      "content": "\"use client\";\n\nimport { random, useCurrentFrame } from \"remotion\";\n\nexport interface RGBGlitchTextProps {\n  text: string;\n  fontSize?: number;\n  color?: string;\n  fontWeight?: number;\n  glitchAt?: number;\n  glitchDuration?: number;\n  intensity?: number;\n  seed?: string;\n  speed?: number;\n  className?: string;\n}\n\nexport function RGBGlitchText({\n  text,\n  fontSize = 96,\n  color = \"#171717\",\n  fontWeight = 700,\n  glitchAt = 20,\n  glitchDuration = 8,\n  intensity = 6,\n  seed = \"glitch\",\n  speed = 1,\n  className,\n}: RGBGlitchTextProps) {\n  const frame = useCurrentFrame() * speed;\n\n  const isGlitching = frame >= glitchAt && frame < glitchAt + glitchDuration;\n\n  const offset = (axisSeed: string, scale: number) =>\n    (random(`${seed}-${axisSeed}-${frame}`) * 2 - 1) * scale;\n\n  const rX = isGlitching ? offset(\"r-x\", intensity) : 0;\n  const rY = isGlitching ? offset(\"r-y\", intensity * 0.4) : 0;\n  const gX = isGlitching ? offset(\"g-x\", intensity) : 0;\n  const gY = isGlitching ? offset(\"g-y\", intensity * 0.4) : 0;\n  const bX = isGlitching ? offset(\"b-x\", intensity) : 0;\n  const bY = isGlitching ? offset(\"b-y\", intensity * 0.4) : 0;\n\n  const copyOpacity = isGlitching ? 1 : 0;\n\n  const baseStyle: React.CSSProperties = {\n    position: \"absolute\",\n    top: 0,\n    left: 0,\n    fontSize,\n    fontWeight,\n    letterSpacing: \"-0.03em\",\n    fontFamily:\n      \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n    whiteSpace: \"pre\",\n    mixBlendMode: \"screen\",\n  };\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: \"transparent\",\n      }}\n    >\n      <div\n        className={className}\n        style={{ position: \"relative\", display: \"inline-block\" }}\n      >\n        <span\n          style={{\n            position: \"relative\",\n            fontSize,\n            fontWeight,\n            color,\n            letterSpacing: \"-0.03em\",\n            fontFamily:\n              \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n            whiteSpace: \"pre\",\n          }}\n        >\n          {text}\n        </span>\n        <span\n          style={{\n            ...baseStyle,\n            color: \"#ff0040\",\n            opacity: copyOpacity,\n            transform: `translateX(${rX}px) translateY(${rY}px)`,\n          }}\n        >\n          {text}\n        </span>\n        <span\n          style={{\n            ...baseStyle,\n            color: \"#00ff80\",\n            opacity: copyOpacity,\n            transform: `translateX(${gX}px) translateY(${gY}px)`,\n          }}\n        >\n          {text}\n        </span>\n        <span\n          style={{\n            ...baseStyle,\n            color: \"#0080ff\",\n            opacity: copyOpacity,\n            transform: `translateX(${bX}px) translateY(${bY}px)`,\n          }}\n        >\n          {text}\n        </span>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/rgb-glitch-text.tsx"
    },
    {
      "path": "src/components/remocn/shimmer-sweep.tsx",
      "content": "\"use client\";\n\nimport { interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface ShimmerSweepProps {\n  text: string;\n  baseColor?: string;\n  shineColor?: string;\n  fontSize?: number;\n  fontWeight?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport function ShimmerSweep({\n  text,\n  baseColor = \"#3f3f46\",\n  shineColor = \"#fafafa\",\n  fontSize = 96,\n  fontWeight = 700,\n  speed = 1,\n  className,\n}: ShimmerSweepProps) {\n  const frame = useCurrentFrame() * speed;\n  const { durationInFrames } = useVideoConfig();\n\n  const position = interpolate(\n    frame,\n    [0, durationInFrames * 0.8],\n    [200, -100],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n\n  const textStyle: React.CSSProperties = {\n    fontSize,\n    fontWeight,\n    letterSpacing: \"-0.03em\",\n    fontFamily:\n      \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n    margin: 0,\n    lineHeight: 1,\n  };\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: \"transparent\",\n      }}\n    >\n      <div style={{ position: \"relative\", display: \"inline-block\" }}>\n        <span style={{ ...textStyle, color: baseColor }}>{text}</span>\n        <span\n          className={className}\n          style={{\n            ...textStyle,\n            position: \"absolute\",\n            inset: 0,\n            color: \"transparent\",\n            backgroundClip: \"text\",\n            WebkitBackgroundClip: \"text\",\n            backgroundImage: `linear-gradient(110deg, transparent 30%, ${shineColor} 50%, transparent 70%)`,\n            backgroundSize: \"200% 100%\",\n            backgroundPosition: `${position}% 50%`,\n          }}\n        >\n          {text}\n        </span>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/shimmer-sweep.tsx"
    },
    {
      "path": "src/demos/shieldcn-plus/index.tsx",
      "content": "import React, { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  Img,\n  Sequence,\n  Series,\n  continueRender,\n  delayRender,\n  interpolate,\n  interpolateColors,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { demoAsset } from \"@/lib/demo-assets\";\nimport {\n  TransitionSeries,\n  linearTiming,\n  type TransitionPresentation,\n  type TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport { loadFont as loadSora } from \"@remotion/google-fonts/Sora\";\n\nimport { RemocnUIProvider } from \"@/lib/remocn-ui\";\nimport { ShortSlideRight } from \"@/components/remocn/short-slide-right\";\nimport { ClaudeChat } from \"@/components/remocn/claude-chat\";\nimport { ShimmerSweep } from \"@/components/remocn/shimmer-sweep\";\nimport { LineByLineSlide } from \"@/components/remocn/line-by-line-slide\";\nimport { RGBGlitchText } from \"@/components/remocn/rgb-glitch-text\";\nimport { RollingNumber } from \"@/components/remocn/rolling-number\";\nimport { BlurIn } from \"@/components/remocn/blur-in\";\nimport { useBlurInTransition } from \"@/components/remocn/use-blur-in-transition\";\nimport { ShaderWarp } from \"@/components/remocn/shader-warp\";\n\n// The whole video is set in Sora — one family for headlines, URLs, params and\n// repo names alike. Nothing goes above weight 500 (reserved for the lockups\n// and the rolling stat); everything else is regular 400. The remocn text\n// components read var(--font-geist-sans), so feeding Sora into that variable\n// re-fonts them without touching library files.\nconst { fontFamily: SORA_FAMILY } = loadSora(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"500\"],\n});\n\nconst SANS =\n  \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\nconst MONO = \"var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace\";\n\n// The shieldcn register: zinc canvas, ink, and the badge-value green. The\n// violet and amber accents exist ONLY inside the brand-morph scene, and only\n// as real renders fetched from shieldcn.dev with color= params.\nconst BG = \"#09090b\";\nconst INK = \"#fafafa\";\nconst MUTED = \"#a1a1aa\";\nconst FAINT = \"rgba(250,250,250,0.45)\";\nconst BORDER = \"rgba(255,255,255,0.10)\";\n// Card surfaces are OPAQUE — the warp backdrop must never bleed through a\n// card, only live between them.\nconst CARD = \"#101014\";\nconst GREEN = \"#22c55e\";\nconst GREEN_SOFT = \"#4ade80\";\nconst VIOLET = \"#8b5cf6\";\nconst AMBER = \"#f59e0b\";\n\nconst clampOpts = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\n// ---------------------------------------------------------------------------\n// Scene timings (frames @ 30fps), one per beat. Transitions overlap.\n// ---------------------------------------------------------------------------\nconst S_FRONTDOOR = 150; // the README assembles itself under a camera glide\n// \"This is shieldcn.\" then the category montage — Badges / Charts / Headers,\n// each label backed by a REAL shieldcn render, hard-cut one into the next.\nconst WHAT_INTRO = 54;\nconst CAT_BADGES = 62;\nconst CAT_CHARTS = 58;\nconst CAT_HEADERS = 60;\nconst S_WHAT = WHAT_INTRO + CAT_BADGES + CAT_CHARTS + CAT_HEADERS;\nconst S_NEWS = 96; //      now it has accounts — with sync\nconst S_MEET = 132; //     Meet shieldcn Plus (shield draws on)\nconst S_SYNC = 138; //     75 saved READMEs, synced\nconst S_LIBRARY = 126; //  the saved badges library\nconst S_MIGRATE = 132; //  mass migration PR cascade\n// The kinetic build itself takes ~81f (6 words: 10f first + 5 × 13f pushes,\n// entering at frame 6) — the beat holds the assembled line for ~20f before\n// the squeeze takes it, so the cut never lands mid-build.\nconst S_AI_TITLE = 118; // \"AI generates and polishes your READMEs\" (kinetic build + hold)\n// AI writes the README — the dark Claude chat types the prompt, then a\n// parallax hand-off pushes the chat DOWN while \"Thinking\" flies UP out of\n// it (a gentle initial kick, long deceleration), and the result lands.\nconst AI_PUSH_AT = 88; //  the chat is pushed away right after typing settles\nconst AI_PUSH_DUR = 18;\nconst AI_THINK = 56;\nconst AI_XFADE = 12; //    thinking → result cross-dissolve overlap\nconst AI_RESULT = 88;\nconst S_AI = AI_PUSH_AT + AI_THINK + AI_RESULT;\nconst S_BRAND = 240; //    one managed brand — the crown\nconst S_OFFER = 84; //     20% off your first 6 months (line-by-line)\nconst S_CTA = 170; //      the closing shieldcn lockup\n// Post-credits stinger — a hard cut after the lockup (the shared warp\n// backdrop carries through, so it reads as a beat after the credits), then\n// the promo code catches signal.\nconst STING_IN = 10; //     black beat before the code flickers on\nconst STING_CAPTION = 34; // \"promo code\" label settles after the burst\nconst STING_EXIT = 96; //   glitch-out burst + upward throw\nconst S_STINGER = 122;\n\nconst T_X = 14; //    crossfade\nconst T_SQ = 16; //   squeeze — mechanical collapse hidden under a blur envelope\nconst T_ZOOM = 18; // zoom-blur at section turns\nconst T_IRIS = 22; // pill-shaped iris reveal into the CTA\n\nexport const SHIELDCN_PLUS_DURATION =\n  S_FRONTDOOR +\n  S_WHAT +\n  S_NEWS +\n  S_MEET +\n  S_SYNC +\n  S_LIBRARY +\n  S_MIGRATE +\n  S_AI_TITLE +\n  S_AI +\n  S_BRAND +\n  S_OFFER +\n  S_CTA +\n  S_STINGER -\n  (T_SQ +\n    T_ZOOM +\n    T_ZOOM +\n    T_SQ +\n    T_X +\n    T_SQ +\n    T_X +\n    T_SQ +\n    T_ZOOM +\n    T_X +\n    T_IRIS);\n\n// ---------------------------------------------------------------------------\n// Reveal — blur-in wrapper driven by useBlurInTransition.\n// ---------------------------------------------------------------------------\nconst Reveal: React.FC<{\n  children: ReactNode;\n  delay?: number;\n  distance?: number;\n  blur?: number;\n  duration?: number;\n  display?: React.CSSProperties[\"display\"];\n}> = ({\n  children,\n  delay = 0,\n  distance = 16,\n  blur = 10,\n  duration = 20,\n  display = \"block\",\n}) => {\n  const style = useBlurInTransition(\n    [{ at: delay, state: \"revealed\", duration }],\n    { direction: \"up\", distance, blur },\n  );\n  return (\n    <BlurIn style={style} display={display}>\n      {children}\n    </BlurIn>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// SlideLine — a short-slide-right line stacked inside a flex column.\n// SlideRich — the same motion for rich content (marker highlights etc.).\n// ---------------------------------------------------------------------------\nconst SlideLine: React.FC<{\n  text: string;\n  at: number;\n  fontSize: number;\n  color?: string;\n  fontWeight?: number;\n}> = ({ text, at, fontSize, color = INK, fontWeight = 400 }) => (\n  <div\n    style={{\n      position: \"relative\",\n      width: \"100%\",\n      height: Math.round(fontSize * 1.35),\n    }}\n  >\n    <Sequence from={at} layout=\"none\">\n      <ShortSlideRight\n        text={text}\n        fontSize={fontSize}\n        color={color}\n        fontWeight={fontWeight}\n      />\n    </Sequence>\n  </div>\n);\n\nconst SlideRich: React.FC<{ at: number; children: ReactNode }> = ({\n  at,\n  children,\n}) => {\n  const frame = useCurrentFrame();\n  const easing = Easing.bezier(0.2, 0.8, 0.2, 1);\n  const t = frame - at;\n  const x = interpolate(t, [0, 16], [-24, 0], { ...clampOpts, easing });\n  const opacity = interpolate(t, [0, 10], [0, 1], { ...clampOpts, easing });\n  const blurVal = interpolate(t, [0, 16], [1.2, 0], { ...clampOpts, easing });\n  return (\n    <div\n      style={{\n        transform: `translateX(${x}px)`,\n        opacity,\n        filter: `blur(${blurVal}px)`,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// ShieldMark — the REAL shieldcn icon path from the repo\n// (packages/core/src/icons/shieldcn.svg), stroke-drawing itself on before the\n// fill settles in.\n// ---------------------------------------------------------------------------\nconst SHIELD_PATH_A =\n  \"M148.02,363.76c-4.48,0-8.64-2.42-10.86-6.32l-54.29-95.68c-2.15-3.8-2.15-8.52,0-12.32l54.29-95.68c2.21-3.9,6.37-6.32,10.86-6.32h18.51c4.44,0,8.45,2.28,10.73,6.09,2.27,3.82,2.37,8.43.25,12.33l-42.23,77.99c-3.98,7.36-3.98,16.14,0,23.49l22.22,41.02c4.25,7.85,12.43,12.8,21.36,12.92,0,0,45.08.61,45.11.61,8.68,0,16.83-4.64,21.26-12.12l24.87-41.99c2.23-3.77,6.34-6.11,10.72-6.12l19.47-.04c4.48,0,8.49,2.29,10.76,6.12,2.27,3.83,2.35,8.45.21,12.35l-42.2,77.17c-2.19,4-6.39,6.49-10.95,6.49h-110.08Z\";\nconst SHIELD_PATH_B =\n  \"M346.7,363.69c-4.44,0-8.45-2.28-10.73-6.09-2.27-3.82-2.37-8.43-.25-12.33l42.23-77.99c3.98-7.35,3.98-16.14,0-23.49l-22.22-41.02c-4.25-7.85-12.44-12.8-21.36-12.92,0,0-46.51-.63-46.53-.63-8.88,0-17.12,4.81-21.48,12.54l-23.35,41.36c-2.2,3.9-6.36,6.34-10.84,6.35l-19.21.04c-4.48,0-8.49-2.29-10.76-6.12-2.27-3.83-2.35-8.45-.22-12.36l42.2-77.17c2.19-4.01,6.39-6.5,10.95-6.5h110.08c4.48,0,8.64,2.42,10.86,6.32l54.29,95.68c2.16,3.8,2.16,8.52,0,12.32l-54.29,95.68c-2.21,3.9-6.37,6.32-10.86,6.32h-18.51Z\";\n\nconst ShieldMark: React.FC<{ size: number; at?: number }> = ({\n  size,\n  at = 0,\n}) => {\n  const frame = useCurrentFrame();\n  const draw = (start: number) =>\n    interpolate(frame, [at + start, at + start + 26], [1, 0], {\n      ...clampOpts,\n      easing: Easing.inOut(Easing.cubic),\n    });\n  const fillOpacity = interpolate(frame, [at + 20, at + 38], [0, 1], {\n    ...clampOpts,\n    easing: Easing.inOut(Easing.quad),\n  });\n  const strokeOpacity = interpolate(\n    frame,\n    [at + 30, at + 44],\n    [1, 0],\n    clampOpts,\n  );\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 512 512\">\n      {[SHIELD_PATH_A, SHIELD_PATH_B].map((d, i) => (\n        <path\n          key={i}\n          d={d}\n          pathLength={1}\n          strokeDasharray={1}\n          strokeDashoffset={draw(i * 6)}\n          stroke={INK}\n          strokeWidth={10}\n          strokeOpacity={strokeOpacity}\n          fill={INK}\n          fillOpacity={fillOpacity}\n        />\n      ))}\n    </svg>\n  );\n};\n\n// ===========================================================================\n// Scene 1 — The front door, alive. A README assembles itself from REAL\n// shieldcn renders while the camera pushes in and glides down the page.\n// ===========================================================================\nconst PAGE_W = 720;\nconst PAGE_INNER = PAGE_W - 64;\n\nconst ProseLine: React.FC<{ width: string; at: number }> = ({ width, at }) => {\n  const frame = useCurrentFrame();\n  const p = interpolate(frame, [at, at + 12], [0, 1], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <div\n      style={{\n        width,\n        height: 10,\n        borderRadius: 5,\n        background: \"rgba(255,255,255,0.08)\",\n        transform: `scaleX(${p})`,\n        transformOrigin: \"left center\",\n      }}\n    />\n  );\n};\n\nconst FrontDoorScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { durationInFrames } = useVideoConfig();\n  // One continuous camera move: push in while gliding down the page.\n  const glide = interpolate(frame, [0, durationInFrames], [26, -470], {\n    ...clampOpts,\n    easing: Easing.inOut(Easing.quad),\n  });\n  const zoom = interpolate(frame, [0, durationInFrames], [1.04, 1.17], {\n    ...clampOpts,\n    easing: Easing.inOut(Easing.quad),\n  });\n  // The page itself materializes first; the content assembles onto it.\n  const pageIn = interpolate(frame, [0, 16], [0, 1], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <AbsoluteFill style={{ alignItems: \"center\" }}>\n      <div style={{ transform: `scale(${zoom}) translateY(${glide}px)` }}>\n        <div\n          style={{\n            width: PAGE_W,\n            borderRadius: 16,\n            border: `1px solid ${BORDER}`,\n            background: \"#0c0c0e\",\n            padding: 32,\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: 18,\n            opacity: pageIn,\n            transform: `translateY(${(1 - pageIn) * 26}px) scale(${0.98 + pageIn * 0.02})`,\n          }}\n        >\n          {/* The real graph header lands first */}\n          <Reveal delay={10} distance={18} blur={12} duration={16}>\n            <Img\n              src={demoAsset(\"shieldcn/header-graph.svg\")}\n              style={{ width: PAGE_INNER, height: \"auto\", display: \"block\" }}\n            />\n          </Reveal>\n          {/* Three real xs badges snap in one after another */}\n          <div style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n            {[\"stars-shieldcn-xs\", \"license-xs\", \"npm-react-xs\"].map(\n              (name, i) => (\n                <Reveal\n                  key={name}\n                  delay={20 + i * 5}\n                  distance={8}\n                  blur={6}\n                  duration={10}\n                >\n                  <Img\n                    src={demoAsset(`shieldcn/${name}.svg`)}\n                    style={{ height: 22, width: \"auto\", display: \"block\" }}\n                  />\n                </Reveal>\n              ),\n            )}\n          </div>\n          {/* Prose skeleton draws on */}\n          <div style={{ display: \"flex\", flexDirection: \"column\", gap: 9 }}>\n            <ProseLine width=\"94%\" at={32} />\n            <ProseLine width=\"100%\" at={37} />\n            <ProseLine width=\"72%\" at={42} />\n          </div>\n          {/* The real star-history chart slides into place */}\n          <Reveal delay={42} distance={26} blur={14} duration={18}>\n            <Img\n              src={demoAsset(\"shieldcn/chart-stars.svg\")}\n              style={{ width: PAGE_INNER, height: \"auto\", display: \"block\" }}\n            />\n          </Reveal>\n          {/* The real sponsor wall settles at the bottom */}\n          <Reveal delay={58} distance={26} blur={14} duration={18}>\n            <Img\n              src={demoAsset(\"shieldcn/sponsors.svg\")}\n              style={{ width: PAGE_INNER, height: \"auto\", display: \"block\" }}\n            />\n          </Reveal>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 2 — What this is. Two beats on the shared Z axis.\n// ===========================================================================\nconst MicroPhase: React.FC<{ children: ReactNode; outAt?: number }> = ({\n  children,\n  outAt,\n}) => {\n  const frame = useCurrentFrame();\n  const easing = Easing.bezier(0.32, 0.72, 0, 1);\n  const opacityIn = interpolate(frame, [0, 18], [0, 1], {\n    ...clampOpts,\n    easing,\n  });\n  const scale = interpolate(frame, [0, 18], [0.96, 1], {\n    ...clampOpts,\n    easing,\n  });\n  const opacityOut =\n    outAt === undefined\n      ? 1\n      : interpolate(frame, [outAt, outAt + 10], [1, 0], {\n          ...clampOpts,\n          easing: Easing.in(Easing.quad),\n        });\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        opacity: opacityIn * opacityOut,\n        transform: `scale(${scale})`,\n      }}\n    >\n      {children}\n    </AbsoluteFill>\n  );\n};\n\n// Shared beat frame for the category montage: label on top, the real render\n// beneath, both entering fast — hard cuts between beats do the rest.\nconst CategoryFrame: React.FC<{ label: string; children: ReactNode }> = ({\n  label,\n  children,\n}) => {\n  const frame = useCurrentFrame();\n  const labelOpacity = interpolate(frame, [0, 7], [0, 1], clampOpts);\n  const labelY = interpolate(frame, [0, 8], [10, 0], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  const labelBlur = interpolate(frame, [0, 7], [6, 0], clampOpts);\n  const uiIn = interpolate(frame, [4, 16], [0, 1], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n      <span\n        style={{\n          fontFamily: SANS,\n          fontWeight: 400,\n          fontSize: 44,\n          color: INK,\n          opacity: labelOpacity,\n          transform: `translateY(${labelY}px)`,\n          filter: `blur(${labelBlur}px)`,\n        }}\n      >\n        {label}\n      </span>\n      <div\n        style={{\n          marginTop: 28,\n          opacity: uiIn,\n          transform: `translateY(${(1 - uiIn) * 24}px) scale(${0.97 + uiIn * 0.03})`,\n        }}\n      >\n        {children}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// Badges — a wall of REAL shieldcn badges cascading in.\nconst BADGE_WALL: string[][] = [\n  [\"npm-react\", \"stars-nextjs\", \"views-shieldcn\", \"license-shieldcn\"],\n  [\"dw-react\", \"npm-react-outline\", \"npm-typescript\", \"vercel-oss\"],\n  [\n    \"npm-react-secondary\",\n    \"stars-react\",\n    \"last-commit\",\n    \"npm-react-destructive\",\n  ],\n];\n\nconst BadgesBeat: React.FC = () => (\n  <CategoryFrame label=\"Badges\">\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 12,\n        alignItems: \"center\",\n      }}\n    >\n      {BADGE_WALL.map((row, r) => (\n        <div key={r} style={{ display: \"flex\", gap: 12 }}>\n          {row.map((name, i) => (\n            <CardPop key={name} at={8 + (r * 4 + i) * 2}>\n              <Img\n                src={demoAsset(`shieldcn/${name}.svg`)}\n                style={{ height: 30, width: \"auto\", display: \"block\" }}\n              />\n            </CardPop>\n          ))}\n        </div>\n      ))}\n    </div>\n  </CategoryFrame>\n);\n\n// Charts — the real star-history chart wiping on left to right, so the\n// curve draws itself across the beat.\nconst ChartsBeat: React.FC = () => {\n  const frame = useCurrentFrame();\n  const wipe = interpolate(frame, [8, 44], [100, 0], {\n    ...clampOpts,\n    easing: Easing.inOut(Easing.cubic),\n  });\n  return (\n    <CategoryFrame label=\"Charts\">\n      <div\n        style={{\n          border: `1px solid ${BORDER}`,\n          background: CARD,\n          borderRadius: 14,\n          padding: \"14px 18px\",\n        }}\n      >\n        <div style={{ clipPath: `inset(0 ${wipe}% 0 0)` }}>\n          <Img\n            src={demoAsset(\"shieldcn/chart-stars.svg\")}\n            style={{ width: 600, height: \"auto\", display: \"block\" }}\n          />\n        </div>\n      </div>\n    </CategoryFrame>\n  );\n};\n\n// Headers — the real graph header, holding under a slow push-in.\nconst HeadersBeat: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { durationInFrames } = useVideoConfig();\n  const zoomP = interpolate(frame, [0, durationInFrames], [1, 1.03], clampOpts);\n  return (\n    <CategoryFrame label=\"Headers\">\n      <div\n        style={{\n          borderRadius: 14,\n          overflow: \"hidden\",\n          border: `1px solid ${BORDER}`,\n          transform: `scale(${zoomP})`,\n        }}\n      >\n        <Img\n          src={demoAsset(\"shieldcn/header-graph.svg\")}\n          style={{ width: 640, height: \"auto\", display: \"block\" }}\n        />\n      </div>\n    </CategoryFrame>\n  );\n};\n\nconst WhatScene: React.FC = () => (\n  <AbsoluteFill>\n    <Series>\n      <Series.Sequence durationInFrames={WHAT_INTRO} layout=\"none\">\n        <MicroPhase outAt={42}>\n          <span\n            style={{\n              fontFamily: SANS,\n              fontWeight: 400,\n              fontSize: 60,\n              color: INK,\n            }}\n          >\n            This is Shieldcn\n          </span>\n        </MicroPhase>\n      </Series.Sequence>\n      <Series.Sequence durationInFrames={CAT_BADGES} layout=\"none\">\n        <BadgesBeat />\n      </Series.Sequence>\n      <Series.Sequence durationInFrames={CAT_CHARTS} layout=\"none\">\n        <ChartsBeat />\n      </Series.Sequence>\n      <Series.Sequence durationInFrames={CAT_HEADERS} layout=\"none\">\n        <HeadersBeat />\n      </Series.Sequence>\n    </Series>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 3 — The news. The announcement lands, and a green sync arc draws\n// itself once beside the key phrase. One rotation of ink, then stillness.\n// ===========================================================================\nconst SyncGlyph: React.FC<{ size: number; at: number }> = ({ size, at }) => {\n  const frame = useCurrentFrame();\n  const draw = (start: number) =>\n    interpolate(frame, [at + start, at + start + 20], [1, 0], {\n      ...clampOpts,\n      easing: Easing.inOut(Easing.cubic),\n    });\n  const tip = (start: number) =>\n    interpolate(frame, [at + start + 16, at + start + 24], [0, 1], clampOpts);\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 48 48\">\n      {/* top arc, clockwise right → left */}\n      <path\n        d=\"M40 24 A16 16 0 0 1 8 24\"\n        pathLength={1}\n        strokeDasharray={1}\n        strokeDashoffset={draw(0)}\n        stroke={GREEN}\n        strokeWidth={4}\n        strokeLinecap=\"round\"\n        fill=\"none\"\n      />\n      {/* bottom arc, clockwise left → right */}\n      <path\n        d=\"M8 24 A16 16 0 0 1 40 24\"\n        pathLength={1}\n        strokeDasharray={1}\n        strokeDashoffset={draw(6)}\n        stroke={GREEN}\n        strokeWidth={4}\n        strokeLinecap=\"round\"\n        fill=\"none\"\n      />\n      {/* arrowheads fade in as each arc completes */}\n      <path\n        d=\"M8 24 L3.5 18 M8 24 L14.5 20.5\"\n        stroke={GREEN}\n        strokeWidth={4}\n        strokeLinecap=\"round\"\n        fill=\"none\"\n        opacity={tip(0)}\n      />\n      <path\n        d=\"M40 24 L44.5 30 M40 24 L33.5 27.5\"\n        stroke={GREEN}\n        strokeWidth={4}\n        strokeLinecap=\"round\"\n        fill=\"none\"\n        opacity={tip(6)}\n      />\n    </svg>\n  );\n};\n\nconst NEWS_GLYPH_AT = 16;\n\nconst NewsScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  // Once the arcs have finished drawing, the glyph makes one full\n  // confirming turn — the sync visibly happens.\n  const spin = interpolate(\n    frame,\n    [NEWS_GLYPH_AT + 36, NEWS_GLYPH_AT + 66],\n    [0, 360],\n    { ...clampOpts, easing: Easing.inOut(Easing.cubic) },\n  );\n  const word: React.CSSProperties = {\n    fontFamily: SANS,\n    fontWeight: 400,\n    fontSize: 46,\n    color: INK,\n  };\n  return (\n    <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n      <div style={{ display: \"flex\", alignItems: \"center\", gap: 18 }}>\n        <SlideRich at={0}>\n          <span style={word}>Now it has accounts</span>\n        </SlideRich>\n        <span\n          style={{\n            display: \"inline-flex\",\n            transform: `rotate(${spin}deg)`,\n          }}\n        >\n          <SyncGlyph size={40} at={NEWS_GLYPH_AT} />\n        </span>\n        <SlideRich at={18}>\n          <span style={word}>with sync</span>\n        </SlideRich>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 5 — Meet shieldcn Plus. The real shield mark draws itself on, the\n// wordmark settles beside it, and \"Plus\" lands in green.\n// ===========================================================================\n// The lockup assembles in three moves: the shield draws itself alone in the\n// center, then the wordmark drives in from the right and pushes the mark\n// left into the lockup, and the subtitle settles tight beneath the pair.\n// The shield glyph fills only ~42% of its 512 viewBox height, so the box is\n// sized up until the VISIBLE glyph matches the wordmark's height (~61px at\n// 78px Sora); the gap compensates for the box's transparent padding.\nconst MEET_MARK_W = 144;\nconst MEET_WORD_W = 330; // \"shieldcn\" at 78px Sora 500, measured from stills\nconst MEET_GAP = 4;\nconst MEET_LOCKUP_W = MEET_MARK_W + MEET_GAP + MEET_WORD_W;\nconst MEET_MARK_X = -(MEET_LOCKUP_W / 2) + MEET_MARK_W / 2;\nconst MEET_WORD_X = MEET_LOCKUP_W / 2 - MEET_WORD_W / 2;\nconst MEET_SLIDE_AT = 40; // the name arrives once the shield has drawn\n// At the final lockup the word's center sits exactly this far right of the\n// mark's; the push preserves that offset from the moment of contact, so the\n// pair moves as one rigid body and an overlap is geometrically impossible.\nconst MEET_PUSH_DIST = MEET_WORD_X - MEET_MARK_X;\n\nconst MeetPlusScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  // One aggressive curve drives the whole move: the name flies in from the\n  // right, makes contact early, PUSHES the mark through most of the flight,\n  // and the hard deceleration parks them — a small impact pop sells the hit.\n  const wordX = interpolate(\n    frame,\n    [MEET_SLIDE_AT, MEET_SLIDE_AT + 18],\n    [MEET_WORD_X + 300, MEET_WORD_X],\n    { ...clampOpts, easing: Easing.bezier(0.55, 0, 0.15, 1) },\n  );\n  // The mark's position is DERIVED from the word's: it holds the center\n  // until the word reaches contact distance, then rides exactly one\n  // lockup-offset ahead of it.\n  const markX = Math.max(MEET_MARK_X, Math.min(0, wordX - MEET_PUSH_DIST));\n  const wordOpacity = interpolate(\n    frame,\n    [MEET_SLIDE_AT, MEET_SLIDE_AT + 7],\n    [0, 1],\n    clampOpts,\n  );\n  const wordBlur = interpolate(\n    frame,\n    [MEET_SLIDE_AT, MEET_SLIDE_AT + 14],\n    [8, 0],\n    clampOpts,\n  );\n  // Impact pop on landing — the lockup compresses a hair and settles.\n  const landPop = interpolate(\n    frame,\n    [MEET_SLIDE_AT + 18, MEET_SLIDE_AT + 23, MEET_SLIDE_AT + 34],\n    [0, 1, 0],\n    clampOpts,\n  );\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n        gap: 8,\n      }}\n    >\n      <div\n        style={{\n          position: \"relative\",\n          width: \"100%\",\n          height: 100,\n          transform: `scale(${1 + landPop * 0.016})`,\n        }}\n      >\n        {/* The shield draws alone in the center, then yields left */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"50%\",\n            transform: `translate(-50%, -50%) translateX(${markX}px)`,\n          }}\n        >\n          <ShieldMark size={MEET_MARK_W} />\n        </div>\n        {/* The wordmark drives in and takes its slot */}\n        <span\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"50%\",\n            transform: `translate(-50%, -50%) translateX(${wordX}px)`,\n            opacity: wordOpacity,\n            filter: wordBlur > 0 ? `blur(${wordBlur}px)` : undefined,\n            fontFamily: SANS,\n            fontWeight: 500,\n            fontSize: 78,\n            color: INK,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          shieldcn\n        </span>\n      </div>\n      <Reveal delay={MEET_SLIDE_AT + 24} distance={10} blur={8}>\n        <span\n          style={{\n            fontFamily: SANS,\n            fontWeight: 400,\n            fontSize: 22,\n            color: MUTED,\n          }}\n        >\n          For maintainers who live in their README\n        </span>\n      </Reveal>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 6 — 75 READMEs, synced. A dashboard library fills with miniature\n// README cards while the count rolls up to 75.\n// ===========================================================================\nconst MiniReadme: React.FC = () => (\n  <div\n    style={{\n      width: 128,\n      height: 86,\n      borderRadius: 10,\n      border: `1px solid ${BORDER}`,\n      background: CARD,\n      padding: 12,\n      display: \"flex\",\n      flexDirection: \"column\",\n      gap: 7,\n    }}\n  >\n    <div\n      style={{\n        width: \"62%\",\n        height: 12,\n        borderRadius: 4,\n        background: \"rgba(255,255,255,0.12)\",\n      }}\n    />\n    <div style={{ display: \"flex\", gap: 4 }}>\n      <div\n        style={{\n          width: 22,\n          height: 7,\n          borderRadius: 4,\n          background: GREEN,\n          opacity: 0.85,\n        }}\n      />\n      <div\n        style={{\n          width: 22,\n          height: 7,\n          borderRadius: 4,\n          background: \"rgba(255,255,255,0.16)\",\n        }}\n      />\n      <div\n        style={{\n          width: 22,\n          height: 7,\n          borderRadius: 4,\n          background: \"rgba(255,255,255,0.16)\",\n        }}\n      />\n    </div>\n    <div\n      style={{\n        width: \"92%\",\n        height: 7,\n        borderRadius: 4,\n        background: \"rgba(255,255,255,0.07)\",\n      }}\n    />\n    <div\n      style={{\n        width: \"74%\",\n        height: 7,\n        borderRadius: 4,\n        background: \"rgba(255,255,255,0.07)\",\n      }}\n    />\n  </div>\n);\n\nconst CardPop: React.FC<{ children: ReactNode; at: number }> = ({\n  children,\n  at,\n}) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const s = spring({\n    frame: frame - at,\n    fps,\n    config: { damping: 14, mass: 0.7 },\n  });\n  const opacity = interpolate(frame, [at, at + 7], [0, 1], clampOpts);\n  return (\n    <div style={{ opacity, transform: `scale(${0.82 + s * 0.18})` }}>\n      {children}\n    </div>\n  );\n};\n\nconst SyncScene: React.FC = () => (\n  <AbsoluteFill\n    style={{\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      flexDirection: \"column\",\n      gap: 26,\n    }}\n  >\n    <div\n      style={{\n        display: \"grid\",\n        gridTemplateColumns: \"repeat(5, 128px)\",\n        gap: 12,\n      }}\n    >\n      {Array.from({ length: 10 }, (_, i) => (\n        <CardPop key={i} at={4 + i * 3}>\n          <MiniReadme />\n        </CardPop>\n      ))}\n    </div>\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-end\",\n        gap: 18,\n        fontFamily: SANS,\n      }}\n    >\n      {/* RollingNumber's root is an AbsoluteFill — it needs a positioned,\n          sized box or the digits center on the whole frame. It also hardcodes\n          its own mono family at weight 800; the video rule is Sora ≤ 500, so\n          a scoped !important rule outbids the inline styles. */}\n      <div\n        className=\"sora-rolling\"\n        style={{\n          position: \"relative\",\n          width: 96,\n          height: 72 * 1.1,\n          overflow: \"hidden\",\n        }}\n      >\n        <style>{`.sora-rolling span { font-family: '${SORA_FAMILY}', sans-serif !important; font-weight: 500 !important; }`}</style>\n        <RollingNumber to={75} fontSize={72} color={INK} speed={1} />\n      </div>\n      <span style={{ fontWeight: 400, fontSize: 34, color: MUTED }}>\n        saved READMEs\n      </span>\n      <Reveal delay={64} distance={8} blur={6} display=\"inline-block\">\n        <span\n          style={{\n            fontWeight: 400,\n            fontSize: 20,\n            color: FAINT,\n            display: \"inline-block\",\n            paddingBottom: 4,\n          }}\n        >\n          synced across devices\n        </span>\n      </Reveal>\n    </div>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 7 — The saved badges library. Real badges assemble into a shelf; the\n// source badge gets a green ring, and copies land in three README rows.\n// ===========================================================================\nconst SHELF_ROW_1 = [\"npm-react\", \"stars-nextjs\", \"views-shieldcn\"];\nconst SHELF_ROW_2 = [\"license-shieldcn\", \"npm-typescript\", \"npm-react-outline\"];\nconst REUSED = \"stars-nextjs\";\n\nconst LibraryScene: React.FC = () => {\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n        gap: 18,\n      }}\n    >\n      <SlideLine\n        text=\"A saved badges library\"\n        at={0}\n        fontSize={40}\n        color={INK}\n      />\n      {/* The shelf */}\n      <Reveal delay={10} distance={16} blur={10}>\n        <div\n          style={{\n            border: `1px solid ${BORDER}`,\n            background: CARD,\n            borderRadius: 14,\n            padding: \"16px 20px\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: 10,\n          }}\n        >\n          {[SHELF_ROW_1, SHELF_ROW_2].map((row, r) => (\n            <div key={r} style={{ display: \"flex\", gap: 10 }}>\n              {row.map((name, i) => (\n                <CardPop key={name} at={14 + (r * 3 + i) * 3}>\n                  <Img\n                    src={demoAsset(`shieldcn/${name}.svg`)}\n                    style={{ height: 28, width: \"auto\", display: \"block\" }}\n                  />\n                </CardPop>\n              ))}\n            </div>\n          ))}\n        </div>\n      </Reveal>\n      {/* Three README rows receive the same badge */}\n      <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n        {[0, 1, 2].map((i) => (\n          <Reveal key={i} delay={30 + i * 5} distance={10} blur={7}>\n            <div\n              style={{\n                width: 520,\n                height: 42,\n                borderRadius: 10,\n                border: `1px solid ${BORDER}`,\n                background: CARD,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 12,\n                padding: \"0 14px\",\n              }}\n            >\n              <span style={{ fontFamily: MONO, fontSize: 13.5, color: FAINT }}>\n                readme-{i + 1}.md\n              </span>\n              <div\n                style={{\n                  flex: 1,\n                  height: 7,\n                  borderRadius: 4,\n                  background: \"rgba(255,255,255,0.06)\",\n                }}\n              />\n              <CardPop at={52 + i * 10}>\n                <Img\n                  src={demoAsset(`shieldcn/${REUSED}.svg`)}\n                  style={{ height: 24, width: \"auto\", display: \"block\" }}\n                />\n              </CardPop>\n            </div>\n          </Reveal>\n        ))}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 8 — Mass migration. Six repos, six PRs, one cascade.\n// ===========================================================================\nconst REPOS = [\n  \"acme/app\",\n  \"acme/docs\",\n  \"acme/ui\",\n  \"acme/cli\",\n  \"acme/site\",\n  \"acme/sdk\",\n];\n\nconst CheckGlyph: React.FC<{ at: number }> = ({ at }) => {\n  const frame = useCurrentFrame();\n  const off = interpolate(frame, [at, at + 10], [1, 0], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  return (\n    <svg width={18} height={18} viewBox=\"0 0 20 20\">\n      <path\n        d=\"M3.5 10.5 L8 15 L16.5 5.5\"\n        pathLength={1}\n        strokeDasharray={1}\n        strokeDashoffset={off}\n        stroke={GREEN}\n        strokeWidth={2.6}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        fill=\"none\"\n      />\n    </svg>\n  );\n};\n\nconst MigrationScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexDirection: \"column\",\n        gap: 8,\n      }}\n    >\n      <SlideLine text=\"Mass migration\" at={0} fontSize={44} color={INK} />\n      <SlideLine\n        text=\"open PRs across all your repos at once\"\n        at={12}\n        fontSize={20}\n        color={FAINT}\n        fontWeight={400}\n      />\n      <div style={{ height: 10 }} />\n      <div\n        style={{\n          width: 600,\n          border: `1px solid ${BORDER}`,\n          background: CARD,\n          borderRadius: 14,\n          padding: \"8px 0\",\n          display: \"flex\",\n          flexDirection: \"column\",\n        }}\n      >\n        {REPOS.map((repo, i) => {\n          const rowAt = 18 + i * 4;\n          const prAt = 52 + i * 7;\n          const rowIn = interpolate(frame, [rowAt, rowAt + 10], [0, 1], {\n            ...clampOpts,\n            easing: Easing.out(Easing.cubic),\n          });\n          const prIn = interpolate(frame, [prAt + 4, prAt + 12], [0, 1], {\n            ...clampOpts,\n            easing: Easing.out(Easing.cubic),\n          });\n          return (\n            <div\n              key={repo}\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 12,\n                padding: \"9px 18px\",\n                borderBottom:\n                  i < REPOS.length - 1 ? `1px solid ${BORDER}` : undefined,\n                opacity: rowIn,\n                transform: `translateX(${(1 - rowIn) * -14}px)`,\n              }}\n            >\n              <CheckGlyph at={prAt} />\n              <span style={{ fontFamily: MONO, fontSize: 17, color: MUTED }}>\n                {repo}\n              </span>\n              <div style={{ flex: 1 }} />\n              <span\n                style={{\n                  fontFamily: MONO,\n                  fontSize: 14,\n                  color: GREEN_SOFT,\n                  opacity: prIn,\n                  transform: `translateX(${(1 - prIn) * 10}px)`,\n                  display: \"inline-block\",\n                }}\n              >\n                PR opened\n              </span>\n            </div>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 9 — AI writes it. A prompt types itself; the artifacts snap in fast.\n// ===========================================================================\n// ---------------------------------------------------------------------------\n// SoraKineticBuild — kinetic-center-build's exact motion, re-implemented\n// locally because the library component measures word widths with a SYSTEM\n// font stack; Sora renders wider than the measurement, so the gaps collapse\n// and the words collide. This variant measures with the loaded Sora face and\n// re-measures once document.fonts is ready (gated with delayRender so a\n// still can never capture the pre-measure layout).\n// ---------------------------------------------------------------------------\nconst KB_GAP = 10;\nconst KB_FIRST_DUR = 10;\nconst KB_PUSH_DUR = 13;\nconst KB_ENTRY_OFFSET = 88;\nconst KB_ENTRY_SCALE = 0.992;\nconst KB_ENTRY_BLUR = 3.5;\nconst KB_REFLOW_BLUR = 0.8;\nconst KB_FIRST_WORD_Y = 6;\nconst KB_EASING = Easing.bezier(0.2, 0.8, 0.2, 1);\n\nconst SoraKineticBuild: React.FC<{\n  text: string;\n  fontSize?: number;\n  color?: string;\n  fontWeight?: number;\n}> = ({ text, fontSize = 72, color = INK, fontWeight = 400 }) => {\n  const frame = useCurrentFrame();\n\n  const [fontsReady, setFontsReady] = useState(false);\n  useEffect(() => {\n    if (typeof document === \"undefined\" || !document.fonts) return;\n    const handle = delayRender(\"sora-kinetic-measure\");\n    let cancelled = false;\n    document.fonts.ready.then(() => {\n      if (!cancelled) setFontsReady(true);\n      requestAnimationFrame(() =>\n        requestAnimationFrame(() => continueRender(handle)),\n      );\n    });\n    return () => {\n      cancelled = true;\n      continueRender(handle);\n    };\n  }, []);\n\n  const words = useMemo(() => text.split(\" \"), [text]);\n  const widths = useMemo(() => {\n    const fallback = words.map((w) => w.length * fontSize * 0.62);\n    if (typeof document === \"undefined\") return fallback;\n    const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n    if (!ctx) return fallback;\n    ctx.font = `${fontWeight} ${fontSize}px ${SORA_FAMILY}`;\n    return words.map((w) => ctx.measureText(w).width);\n    // fontsReady retriggers the measurement once the real face is available\n  }, [words, fontSize, fontWeight, fontsReady]);\n\n  const positionsAt = useMemo(() => {\n    const out: number[][] = [];\n    for (let k = 1; k <= words.length; k++) {\n      let total = KB_GAP * (k - 1);\n      for (let j = 0; j < k; j++) total += widths[j];\n      let cursor = -total / 2;\n      const xs: number[] = [];\n      for (let j = 0; j < k; j++) {\n        xs.push(cursor + widths[j] / 2);\n        cursor += widths[j] + KB_GAP;\n      }\n      out.push(xs);\n    }\n    return out;\n  }, [words, widths]);\n\n  const n = words.length;\n\n  return (\n    <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n      <div\n        style={{\n          position: \"relative\",\n          fontSize,\n          fontWeight,\n          color,\n          fontFamily: SANS,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {words.map((word, j) => {\n          const entryStart = j === 0 ? 0 : KB_FIRST_DUR + (j - 1) * KB_PUSH_DUR;\n          const entryEnd = entryStart + (j === 0 ? KB_FIRST_DUR : KB_PUSH_DUR);\n          const targetX = positionsAt[j][j];\n          const xFrom = j === 0 ? 0 : targetX + KB_ENTRY_OFFSET;\n\n          let x = targetX;\n          let opacity = 1;\n          let scale = 1;\n          let blur = 0;\n          let y = 0;\n\n          if (frame < entryStart) {\n            opacity = 0;\n            x = xFrom;\n            scale = KB_ENTRY_SCALE;\n            blur = KB_ENTRY_BLUR;\n            y = j === 0 ? KB_FIRST_WORD_Y : 0;\n          } else if (frame <= entryEnd) {\n            const range: [number, number] = [entryStart, entryEnd];\n            const opts = { ...clampOpts, easing: KB_EASING };\n            x = interpolate(frame, range, [xFrom, targetX], opts);\n            opacity = interpolate(frame, range, [0, 1], opts);\n            scale = interpolate(frame, range, [KB_ENTRY_SCALE, 1], opts);\n            blur = interpolate(frame, range, [KB_ENTRY_BLUR, 0], opts);\n            y =\n              j === 0\n                ? interpolate(frame, range, [KB_FIRST_WORD_Y, 0], opts)\n                : 0;\n          } else {\n            for (let w = j + 1; w < n; w++) {\n              const pushStart = KB_FIRST_DUR + (w - 1) * KB_PUSH_DUR;\n              const pushEnd = pushStart + KB_PUSH_DUR;\n              const fromX = positionsAt[w - 1][j];\n              const toX = positionsAt[w][j];\n              if (frame >= pushEnd) {\n                x = toX;\n              } else if (frame >= pushStart) {\n                x = interpolate(frame, [pushStart, pushEnd], [fromX, toX], {\n                  ...clampOpts,\n                  easing: KB_EASING,\n                });\n                blur = interpolate(\n                  frame,\n                  [pushStart, (pushStart + pushEnd) / 2, pushEnd],\n                  [0, KB_REFLOW_BLUR, 0],\n                  clampOpts,\n                );\n                break;\n              } else {\n                x = fromX;\n                break;\n              }\n            }\n          }\n\n          return (\n            <span\n              key={j}\n              style={{\n                position: \"absolute\",\n                left: \"50%\",\n                top: \"50%\",\n                whiteSpace: \"nowrap\",\n                backfaceVisibility: \"hidden\",\n                transform: `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0) scale(${scale})`,\n                filter: `blur(${blur}px)`,\n                opacity,\n              }}\n            >\n              {word}\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// The claim gets its own interstitial beat — the line assembles word by\n// word in the center, then the demo proves it.\nconst AiTitleScene: React.FC = () => (\n  <AbsoluteFill>\n    <Sequence from={6} layout=\"none\">\n      <SoraKineticBuild\n        text=\"AI generates and polishes your READMEs\"\n        fontSize={46}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// The chat beat: the prompt types itself, then the chat dips DOWN with a\n// short push while a blur+fade envelope dissolves it by ~2/3 of the push —\n// the fade does the exit, so the travel stays small instead of sliding the\n// whole card off the stage.\nconst AiChatBeat: React.FC = () => {\n  const frame = useCurrentFrame();\n  const y = interpolate(\n    frame,\n    [AI_PUSH_AT, AI_PUSH_AT + AI_PUSH_DUR],\n    [0, 240],\n    { ...clampOpts, easing: Easing.in(Easing.quad) },\n  );\n  const blur = interpolate(\n    frame,\n    [AI_PUSH_AT + 1, AI_PUSH_AT + 10],\n    [0, 16],\n    clampOpts,\n  );\n  const opacity = interpolate(\n    frame,\n    [AI_PUSH_AT + 3, AI_PUSH_AT + 12],\n    [1, 0],\n    clampOpts,\n  );\n  return (\n    <AbsoluteFill\n      style={{\n        transform: `translateY(${y}px)`,\n        filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n        opacity,\n      }}\n    >\n      <ClaudeChat prompt=\"generate a readme for jal-co/shieldcn\" speed={1.3} />\n    </AbsoluteFill>\n  );\n};\n\n// The thinking beat: \"Thinking…\" rises out of the chat-card zone (the card\n// spans ~y300–478 in the 720 ref frame; +70px starts the line inside it),\n// materializing through a reverse blur+fade as the dissolving chat departs —\n// it never enters from the screen edge and is never drawn over the chat.\nconst AiThinkingBeat: React.FC = () => {\n  const frame = useCurrentFrame();\n  const yIn = interpolate(frame, [0, AI_PUSH_DUR + 6], [70, 0], {\n    ...clampOpts,\n    easing: Easing.bezier(0.45, 0.05, 0.15, 1),\n  });\n  // Exit: keeps drifting upward while dissolving, handing off to the result.\n  const yOut = interpolate(frame, [AI_THINK, AI_THINK + AI_XFADE], [0, -60], {\n    ...clampOpts,\n    easing: Easing.in(Easing.quad),\n  });\n  const blur =\n    interpolate(frame, [0, 14], [10, 0], clampOpts) +\n    interpolate(frame, [AI_THINK, AI_THINK + AI_XFADE], [0, 10], clampOpts);\n  const opacity =\n    interpolate(frame, [2, 12], [0, 1], clampOpts) *\n    interpolate(frame, [AI_THINK, AI_THINK + AI_XFADE - 2], [1, 0], clampOpts);\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        transform: `translateY(${yIn + yOut}px)`,\n        filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n        opacity,\n      }}\n    >\n      <ShimmerSweep\n        text=\"Thinking…\"\n        fontSize={46}\n        fontWeight={400}\n        baseColor=\"#3f3f46\"\n        shineColor={INK}\n      />\n    </AbsoluteFill>\n  );\n};\n\n// The result beat: the mini README materializes through a reverse blur+fade\n// rise while \"Thinking…\" dissolves upward — the same hand-off language as\n// the chat → thinking transition.\nconst AiResultBeat: React.FC = () => {\n  const frame = useCurrentFrame();\n  const y = interpolate(frame, [0, 14], [36, 0], {\n    ...clampOpts,\n    easing: Easing.bezier(0.45, 0.05, 0.15, 1),\n  });\n  const blur = interpolate(frame, [0, 12], [8, 0], clampOpts);\n  const opacity = interpolate(frame, [0, 10], [0, 1], clampOpts);\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        transform: `translateY(${y}px)`,\n        filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n        opacity,\n      }}\n    >\n      {/* The result — the REAL jal-co/shieldcn README (live shieldcn.dev\n          renders: graph header, stats group, star-history chart), framed as\n          a README.md rendering on GitHub like the brand scene's card */}\n      <div\n        style={{\n          width: 620,\n          borderRadius: 12,\n          border: `1px solid ${BORDER}`,\n          background: CARD,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 8,\n            padding: \"10px 16px\",\n            borderBottom: `1px solid ${BORDER}`,\n            fontFamily: SANS,\n            fontSize: 13.5,\n            color: INK,\n          }}\n        >\n          <svg width={16} height={16} viewBox=\"0 0 16 16\">\n            <path\n              fill={MUTED}\n              d=\"M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z\"\n            />\n          </svg>\n          README.md\n        </div>\n        <div\n          style={{\n            padding: 18,\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: 12,\n          }}\n        >\n          <CardPop at={8}>\n            <Img\n              src={demoAsset(\"shieldcn/plus/header-shieldcn.svg\")}\n              style={{ width: 584, height: \"auto\", display: \"block\" }}\n            />\n          </CardPop>\n          <CardPop at={16}>\n            <Img\n              src={demoAsset(\"shieldcn/plus/b-group-shieldcn.svg\")}\n              style={{ height: 24, width: \"auto\", display: \"block\" }}\n            />\n          </CardPop>\n          <CardPop at={22}>\n            <Img\n              src={demoAsset(\"shieldcn/plus/chart-stars-shieldcn.svg\")}\n              style={{ width: 480, height: \"auto\", display: \"block\" }}\n            />\n          </CardPop>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// Three beats inside one AI story: the prompt is typed into the Claude chat\n// input (the remocn claude-chat component, local copy themed dark), the\n// parallax hand-off throws \"Thinking\" up out of the chat, and the generated\n// README lands.\nconst AiScene: React.FC = () => (\n  <AbsoluteFill>\n    {/* Thinking renders BELOW the chat so the opaque card occludes it\n        until the push-down reveals it from underneath. Its sequence runs\n        AI_XFADE frames into the result beat for the cross-dissolve exit. */}\n    <Sequence\n      from={AI_PUSH_AT}\n      durationInFrames={AI_THINK + AI_XFADE}\n      layout=\"none\"\n    >\n      <AiThinkingBeat />\n    </Sequence>\n    <Sequence durationInFrames={AI_PUSH_AT + AI_PUSH_DUR} layout=\"none\">\n      <AiChatBeat />\n    </Sequence>\n    <Sequence\n      from={AI_PUSH_AT + AI_THINK}\n      durationInFrames={AI_RESULT}\n      layout=\"none\"\n    >\n      <AiResultBeat />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 10 — One managed brand. The crown: the accent value flips in place,\n// and every REAL artifact re-renders in the new color simultaneously. The\n// three color states are real shieldcn.dev renders (color= param), not CSS.\n// ===========================================================================\nconst BRAND_STAGE_AT = 34; //  stage enters after the title exits itself\nconst FLIP_1 = 120; //         green → violet (scene-global frames)\nconst FLIP_2 = 180; //         violet → amber\nconst FLIP_DUR = 9;\n\nconst BRAND_COLORS = [\n  { name: \"green\", hex: \"#22c55e\", dot: GREEN },\n  { name: \"violet\", hex: \"#8b5cf6\", dot: VIOLET },\n  { name: \"amber\", hex: \"#f59e0b\", dot: AMBER },\n];\n\n// Opacity envelopes for the three stacked real renders.\nconst stackOpacity = (frame: number, idx: number): number => {\n  if (idx === 0)\n    return interpolate(frame, [FLIP_1, FLIP_1 + FLIP_DUR], [1, 0], clampOpts);\n  if (idx === 1) {\n    return (\n      interpolate(frame, [FLIP_1, FLIP_1 + FLIP_DUR], [0, 1], clampOpts) *\n      interpolate(frame, [FLIP_2, FLIP_2 + FLIP_DUR], [1, 0], clampOpts)\n    );\n  }\n  return interpolate(frame, [FLIP_2, FLIP_2 + FLIP_DUR], [0, 1], clampOpts);\n};\n\nconst MorphStack: React.FC<{\n  base: string;\n  width: number;\n  height: number;\n}> = ({ base, width, height }) => {\n  const frame = useCurrentFrame();\n  return (\n    <div style={{ position: \"relative\", width, height }}>\n      {BRAND_COLORS.map(({ name }, i) => (\n        <Img\n          key={name}\n          src={demoAsset(`shieldcn/plus/${base}-${name}.svg`)}\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            width,\n            height,\n            opacity: stackOpacity(frame, i),\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n\n// The accent value flips in place — old up and out, new up and in.\nconst FlipHex: React.FC = () => {\n  const frame = useCurrentFrame();\n  const flipP = (at: number) =>\n    interpolate(frame, [at, at + FLIP_DUR], [0, 1], {\n      ...clampOpts,\n      easing: Easing.inOut(Easing.cubic),\n    });\n  const p1 = flipP(FLIP_1);\n  const p2 = flipP(FLIP_2);\n  const dotColor = interpolateColors(\n    p1 + p2,\n    [0, 1, 2],\n    [GREEN, VIOLET, AMBER],\n  );\n  const items = BRAND_COLORS.map(({ hex }, i) => {\n    // 0: 1 → out on p1; 1: in on p1 → out on p2; 2: in on p2\n    let opacity = 1;\n    let y = 0;\n    if (i === 0) {\n      opacity = 1 - p1;\n      y = p1 * -12;\n    } else if (i === 1) {\n      opacity = p1 * (1 - p2);\n      y = (1 - p1) * 12 + p2 * -12;\n    } else {\n      opacity = p2;\n      y = (1 - p2) * 12;\n    }\n    return { hex, opacity, y };\n  });\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 12 }}>\n      <span\n        style={{\n          width: 16,\n          height: 16,\n          borderRadius: 999,\n          background: dotColor,\n          flex: \"none\",\n        }}\n      />\n      <span\n        style={{\n          position: \"relative\",\n          display: \"inline-block\",\n          fontFamily: MONO,\n          fontSize: 24,\n          height: 30,\n          width: \"8ch\",\n        }}\n      >\n        {items.map(({ hex, opacity, y }) =>\n          opacity <= 0.001 ? null : (\n            <span\n              key={hex}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                whiteSpace: \"nowrap\",\n                color: INK,\n                opacity,\n                transform: `translateY(${y}px)`,\n              }}\n            >\n              {hex}\n            </span>\n          ),\n        )}\n      </span>\n    </div>\n  );\n};\n\nconst BrandScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  // The title beat plays first and exits itself.\n  const titleOut = interpolate(frame, [26, 38], [0, 1], {\n    ...clampOpts,\n    easing: Easing.in(Easing.cubic),\n  });\n  // A tiny settle pop on the artifact column at each flip.\n  const pop = (at: number) =>\n    interpolate(frame, [at, at + 6, at + 18], [0, 1, 0], clampOpts);\n  const colScale = 1 + (pop(FLIP_1) + pop(FLIP_2)) * 0.015;\n  return (\n    <AbsoluteFill>\n      {/* Title beat */}\n      <AbsoluteFill\n        style={{\n          opacity: 1 - titleOut,\n          transform: `translateY(${titleOut * -10}px)`,\n          filter: titleOut > 0 ? `blur(${titleOut * 6}px)` : undefined,\n        }}\n      >\n        <ShortSlideRight\n          text=\"One managed brand\"\n          fontSize={50}\n          color={INK}\n          fontWeight={400}\n        />\n      </AbsoluteFill>\n      {/* The stage */}\n      <Sequence from={BRAND_STAGE_AT} layout=\"none\">\n        <AbsoluteFill\n          style={{\n            flexDirection: \"row\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            gap: 46,\n          }}\n        >\n          {/* Left — the brand token card */}\n          <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n            <Reveal delay={2} distance={14} blur={10}>\n              <div\n                style={{\n                  width: 280,\n                  borderRadius: 14,\n                  border: `1px solid ${BORDER}`,\n                  background: CARD,\n                  padding: \"16px 20px\",\n                  display: \"flex\",\n                  flexDirection: \"column\",\n                  gap: 12,\n                }}\n              >\n                <span style={{ fontFamily: MONO, fontSize: 19, color: INK }}>\n                  ?brand=acme\n                </span>\n                <div style={{ height: 1, background: BORDER }} />\n                <span style={{ fontFamily: SANS, fontSize: 13, color: FAINT }}>\n                  accent\n                </span>\n                <Sequence from={-BRAND_STAGE_AT} layout=\"none\">\n                  {/* FlipHex reads scene-global frames for the flip timing. */}\n                  <FlipHex />\n                </Sequence>\n              </div>\n            </Reveal>\n          </div>\n          {/* Right — the same real artifacts, re-rendered per accent,\n              framed as a README rendering on GitHub */}\n          <Sequence from={-BRAND_STAGE_AT} layout=\"none\">\n            <div style={{ transform: `scale(${colScale})` }}>\n              <Reveal delay={BRAND_STAGE_AT + 4} distance={18} blur={12}>\n                <div\n                  style={{\n                    width: 541,\n                    borderRadius: 12,\n                    border: `1px solid ${BORDER}`,\n                    background: CARD,\n                    overflow: \"hidden\",\n                  }}\n                >\n                  {/* GitHub's README file header, in the video's own card\n                      palette so it matches the ?brand token card */}\n                  <div\n                    style={{\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      gap: 8,\n                      padding: \"10px 16px\",\n                      borderBottom: `1px solid ${BORDER}`,\n                      fontFamily: SANS,\n                      fontSize: 13.5,\n                      color: INK,\n                    }}\n                  >\n                    <svg width={16} height={16} viewBox=\"0 0 16 16\">\n                      <path\n                        fill={MUTED}\n                        d=\"M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z\"\n                      />\n                    </svg>\n                    README.md\n                  </div>\n                  <div\n                    style={{\n                      padding: 18,\n                      display: \"flex\",\n                      flexDirection: \"column\",\n                      gap: 12,\n                    }}\n                  >\n                    <Reveal delay={BRAND_STAGE_AT + 10} distance={16} blur={10}>\n                      <MorphStack base=\"header-acme\" width={505} height={175} />\n                    </Reveal>\n                    <Reveal delay={BRAND_STAGE_AT + 16} distance={12} blur={9}>\n                      <div style={{ display: \"flex\", gap: 9 }}>\n                        <MorphStack base=\"b-build\" width={94} height={26} />\n                        <MorphStack base=\"b-cov\" width={102} height={26} />\n                        <MorphStack base=\"b-stars\" width={91} height={26} />\n                      </div>\n                    </Reveal>\n                    <Reveal delay={BRAND_STAGE_AT + 22} distance={16} blur={10}>\n                      <MorphStack base=\"chart\" width={430} height={215} />\n                    </Reveal>\n                  </div>\n                </div>\n              </Reveal>\n            </div>\n          </Sequence>\n        </AbsoluteFill>\n      </Sequence>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 11 — The offer. The launch discount accumulates line by line.\n// ===========================================================================\nconst OfferScene: React.FC = () => (\n  <AbsoluteFill>\n    <Sequence from={10}>\n      <LineByLineSlide\n        text={\"20% off\\nyour first 6 months\"}\n        fontSize={50}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 12 — CTA. The shield draws on once more and the wordmark settles —\n// nothing else competes with the lockup.\n// ===========================================================================\nconst CtaScene: React.FC = () => (\n  <AbsoluteFill\n    style={{\n      alignItems: \"center\",\n      justifyContent: \"center\",\n    }}\n  >\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 14 }}>\n      {/* The enlarged mark's viewBox padding is pulled back in, so the\n          visible glyph sits one word-space from the name. */}\n      <span style={{ display: \"inline-flex\", marginRight: -18 }}>\n        <ShieldMark size={112} />\n      </span>\n      <Reveal delay={8} distance={12} blur={10} display=\"inline-block\">\n        <span\n          style={{\n            fontFamily: SANS,\n            fontWeight: 500,\n            fontSize: 62,\n            color: INK,\n          }}\n        >\n          shieldcn\n        </span>\n      </Reveal>\n    </div>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 13 — post-credits stinger. Empty beat, then \"launch20\" catches\n// signal: a deterministic flicker + RGB-split burst on entry, a quiet hold\n// with a muted label, and a glitch-out burst thrown upward through blur.\n// ===========================================================================\n// Pop-in flicker levels for the first frames after STING_IN.\nconst STING_FLICKER = [1, 0.2, 1, 0.35, 1];\n\nconst StingerScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const rel = frame - STING_IN;\n  const appear = rel < 0 ? 0 : (STING_FLICKER[rel] ?? 1);\n\n  const captionOpacity = interpolate(\n    frame,\n    [STING_CAPTION, STING_CAPTION + 10],\n    [0, 1],\n    clampOpts,\n  );\n  const captionRise = interpolate(\n    frame,\n    [STING_CAPTION, STING_CAPTION + 10],\n    [8, 0],\n    clampOpts,\n  );\n\n  const exitY = interpolate(frame, [STING_EXIT, STING_EXIT + 10], [0, -90], {\n    ...clampOpts,\n    easing: Easing.in(Easing.quad),\n  });\n  const exitBlur = interpolate(\n    frame,\n    [STING_EXIT, STING_EXIT + 10],\n    [0, 16],\n    clampOpts,\n  );\n  const exitOpacity = interpolate(\n    frame,\n    [STING_EXIT + 2, STING_EXIT + 11],\n    [1, 0],\n    clampOpts,\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        transform: `translateY(${exitY}px)`,\n        filter: exitBlur > 0.1 ? `blur(${exitBlur}px)` : undefined,\n        opacity: exitOpacity,\n      }}\n    >\n      <div style={{ position: \"absolute\", inset: 0, opacity: appear }}>\n        {/* Two instances of the same text swap invisibly at STING_EXIT so the\n            one-window glitch component can burst twice: on entry and on exit. */}\n        <Sequence durationInFrames={STING_EXIT} layout=\"none\">\n          <RGBGlitchText\n            text=\"launch20\"\n            fontSize={92}\n            fontWeight={500}\n            color={INK}\n            glitchAt={STING_IN}\n            glitchDuration={10}\n            intensity={7}\n            seed=\"stinger-in\"\n          />\n        </Sequence>\n        <Sequence from={STING_EXIT} layout=\"none\">\n          <RGBGlitchText\n            text=\"launch20\"\n            fontSize={92}\n            fontWeight={500}\n            color={INK}\n            glitchAt={0}\n            glitchDuration={14}\n            intensity={11}\n            seed=\"stinger-out\"\n          />\n        </Sequence>\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: \"50%\",\n            textAlign: \"center\",\n            transform: `translateY(${-96 + captionRise}px)`,\n            opacity: captionOpacity,\n            fontFamily: SANS,\n            fontWeight: 400,\n            fontSize: 22,\n            lineHeight: 1,\n            color: MUTED,\n          }}\n        >\n          Use promo code\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Transition presentations — the shieldcn family grammar: squeeze, pill iris,\n// zoom-blur, crossfade. Deliberately no swirl, no dither.\n// ===========================================================================\ntype EmptyProps = Record<string, never>;\n\nconst Crossfade: React.FC<TransitionPresentationComponentProps<EmptyProps>> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n}) => {\n  const entering = presentationDirection === \"entering\";\n  const opacity = entering ? presentationProgress : 1 - presentationProgress;\n  return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;\n};\nconst crossfade = (): TransitionPresentation<EmptyProps> => ({\n  component: Crossfade,\n  props: {},\n});\n\n// The squeeze keeps its mechanical scaleY collapse, but the deformation is\n// hidden under a blur + fade envelope: while the squash is still subtle\n// (scaleY > ~0.8) there is barely any blur, and by the time the frame is\n// visibly compressed it is already defocused and fading — the squash reads\n// as shutter motion blur, never as a cheaply stretched screenshot.\nconst SqueezePres: React.FC<\n  TransitionPresentationComponentProps<EmptyProps>\n> = ({ children, presentationProgress, presentationDirection }) => {\n  const entering = presentationDirection === \"entering\";\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: Easing.bezier(0.7, 0, 0.3, 1),\n  });\n  if (!entering) {\n    const blur = interpolate(p, [0.08, 0.5], [0, 18], clampOpts);\n    const opacity = interpolate(p, [0.18, 0.62], [1, 0], clampOpts);\n    return (\n      <AbsoluteFill\n        style={{\n          transform: `scaleY(${Math.max(0.002, 1 - p)})`,\n          transformOrigin: \"50% 0%\",\n          opacity,\n          filter: blur > 0 ? `blur(${blur}px)` : undefined,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n    );\n  }\n  const blur = interpolate(p, [0.5, 0.92], [18, 0], clampOpts);\n  const opacity = interpolate(p, [0.38, 0.82], [0, 1], clampOpts);\n  return (\n    <AbsoluteFill\n      style={{\n        transform: `scaleY(${Math.max(0.002, p)})`,\n        transformOrigin: \"50% 100%\",\n        opacity,\n        filter: p < 1 && blur > 0 ? `blur(${blur}px)` : undefined,\n      }}\n    >\n      {children}\n    </AbsoluteFill>\n  );\n};\nconst squeeze = (): TransitionPresentation<EmptyProps> => ({\n  component: SqueezePres,\n  props: {},\n});\n\nconst IrisPres: React.FC<TransitionPresentationComponentProps<EmptyProps>> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n}) => {\n  const entering = presentationDirection === \"entering\";\n  if (entering) {\n    const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n      easing: Easing.out(Easing.cubic),\n    });\n    const vy = (1 - p) * 46;\n    const vx = (1 - p) * 38;\n    const r = (1 - p) * 380;\n    return (\n      <AbsoluteFill\n        style={{\n          clipPath: `inset(${vy}% ${vx}% ${vy}% ${vx}% round ${r}px)`,\n          transform: `scale(${1.04 - p * 0.04})`,\n        }}\n      >\n        {children}\n      </AbsoluteFill>\n    );\n  }\n  const p = presentationProgress;\n  return (\n    <AbsoluteFill style={{ opacity: 1 - p * 0.8, filter: `blur(${p * 6}px)` }}>\n      {children}\n    </AbsoluteFill>\n  );\n};\nconst iris = (): TransitionPresentation<EmptyProps> => ({\n  component: IrisPres,\n  props: {},\n});\n\nconst ZoomBlur: React.FC<\n  TransitionPresentationComponentProps<{ rise: number }>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const { rise } = passedProps;\n  const entering = presentationDirection === \"entering\";\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: entering ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),\n  });\n  const style: React.CSSProperties = entering\n    ? {\n        opacity: p,\n        transform: `translateY(${(1 - p) * rise}px) scale(${0.86 + p * 0.14})`,\n        filter: p < 1 ? `blur(${(1 - p) * 18}px)` : undefined,\n      }\n    : {\n        opacity: 1 - p,\n        transform: `translateY(${-p * rise}px) scale(${1 + p * 0.18})`,\n        filter: p > 0 ? `blur(${p * 18}px)` : undefined,\n      };\n  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;\n};\nconst zoomBlur = (rise = 0): TransitionPresentation<{ rise: number }> => ({\n  component: ZoomBlur,\n  props: { rise },\n});\n\n// ===========================================================================\n// Composition root.\n// ===========================================================================\nexport const ShieldcnPlusDemo: React.FC = () => {\n  return (\n    <RemocnUIProvider>\n      <AbsoluteFill\n        style={\n          {\n            background: BG,\n            \"--font-geist-sans\": SORA_FAMILY,\n            \"--font-geist-mono\": SORA_FAMILY,\n          } as React.CSSProperties\n        }\n      >\n        {/* Living shader backdrop — the paper.design warp (checks) preset,\n            recolored into a quiet zinc monochrome so it never competes with\n            the content. */}\n        <ShaderWarp\n          speed={2.5}\n          colors={[\"#0e0e10\", \"#16161a\", \"#1f1f25\"]}\n          proportion={0.05}\n          softness={0}\n          distortion={0.25}\n          swirl={0.8}\n          swirlIterations={10}\n          shape=\"checks\"\n          shapeScale={0.28}\n          scale={1.2}\n          rotation={44}\n        />\n        <AbsoluteFill\n          style={{\n            background:\n              \"radial-gradient(120% 120% at 50% 45%, rgba(9,9,11,0) 30%, rgba(9,9,11,0.85) 100%)\",\n          }}\n        />\n\n        <TransitionSeries>\n          {/* 1 — The front door assembles itself; it exits through the\n              blurred squeeze, so the collapse never shows squashed content */}\n          <TransitionSeries.Sequence durationInFrames={S_FRONTDOOR}>\n            <FrontDoorScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_SQ })}\n            presentation={squeeze()}\n          />\n\n          {/* 2 — This is shieldcn → the creed */}\n          <TransitionSeries.Sequence durationInFrames={S_WHAT}>\n            <WhatScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_ZOOM })}\n            presentation={zoomBlur(44)}\n          />\n\n          {/* 3 — The news: accounts, with sync */}\n          <TransitionSeries.Sequence durationInFrames={S_NEWS}>\n            <NewsScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_ZOOM })}\n            presentation={zoomBlur(44)}\n          />\n\n          {/* 4 — Meet shieldcn Plus */}\n          <TransitionSeries.Sequence durationInFrames={S_MEET}>\n            <MeetPlusScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_SQ })}\n            presentation={squeeze()}\n          />\n\n          {/* 6 — 75 saved READMEs, synced */}\n          <TransitionSeries.Sequence durationInFrames={S_SYNC}>\n            <SyncScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_X })}\n            presentation={crossfade()}\n          />\n\n          {/* 7 — The saved badges library */}\n          <TransitionSeries.Sequence durationInFrames={S_LIBRARY}>\n            <LibraryScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_SQ })}\n            presentation={squeeze()}\n          />\n\n          {/* 8 — Mass migration */}\n          <TransitionSeries.Sequence durationInFrames={S_MIGRATE}>\n            <MigrationScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_X })}\n            presentation={crossfade()}\n          />\n\n          {/* — Interstitial title: AI generates and polishes your READMEs */}\n          <TransitionSeries.Sequence durationInFrames={S_AI_TITLE}>\n            <AiTitleScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_SQ })}\n            presentation={squeeze()}\n          />\n\n          {/* 9 — AI writes the README */}\n          <TransitionSeries.Sequence durationInFrames={S_AI}>\n            <AiScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_ZOOM })}\n            presentation={zoomBlur(44)}\n          />\n\n          {/* 10 — One managed brand (the crown) */}\n          <TransitionSeries.Sequence durationInFrames={S_BRAND}>\n            <BrandScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_X })}\n            presentation={crossfade()}\n          />\n\n          {/* 11 — The offer */}\n          <TransitionSeries.Sequence durationInFrames={S_OFFER}>\n            <OfferScene />\n          </TransitionSeries.Sequence>\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_IRIS })}\n            presentation={iris()}\n          />\n\n          {/* 12 — CTA */}\n          <TransitionSeries.Sequence durationInFrames={S_CTA}>\n            <CtaScene />\n          </TransitionSeries.Sequence>\n\n          {/* 13 — post-credits stinger: no transition on purpose; the hard\n              cut over the continuous warp backdrop IS the after-credits beat */}\n          <TransitionSeries.Sequence durationInFrames={S_STINGER}>\n            <StingerScene />\n          </TransitionSeries.Sequence>\n        </TransitionSeries>\n      </AbsoluteFill>\n    </RemocnUIProvider>\n  );\n};\n",
      "type": "registry:component",
      "target": "demos/shieldcn-plus/index.tsx"
    },
    {
      "path": "src/lib/demo-assets.ts",
      "content": "import { staticFile } from \"remotion\";\n\n// Demo assets are loaded from absolute URLs so a demo installed into another\n// project via the registry renders identically without copying this repo's\n// public/ directory. The same URLs are used on the site and in local renders,\n// which keeps a single code path (network access is required for rendering).\n//\n// Override with REMOTION_DEMO_ASSETS_BASE — only REMOTION_-prefixed env vars\n// reach Remotion compositions (next.config.js additionally inlines it for the\n// site's <Player>). Two override forms:\n//   - \"local\" — serve straight from this repo's public/ via staticFile();\n//     works in Remotion Studio, CLI renders and the Next dev site. Set it in\n//     the gitignored .env so previews never depend on pushed assets.\n//   - any URL — e.g. a local static server (http://127.0.0.1:8123).\nexport const DEMO_ASSETS_BASE =\n  process.env.REMOTION_DEMO_ASSETS_BASE ||\n  \"https://raw.githubusercontent.com/Remocn/remocn-collections/main/public\";\n\nexport const demoAsset = (path: string): string => {\n  const clean = path.replace(/^\\/+/, \"\");\n  if (DEMO_ASSETS_BASE === \"local\") return staticFile(clean);\n  return `${DEMO_ASSETS_BASE}/${clean}`;\n};\n",
      "type": "registry:component",
      "target": "lib/demo-assets.ts"
    }
  ],
  "docs": "Register the composition in your Remotion Root:\n\n  import { ShieldcnPlusDemo } from \"@/demos/shieldcn-plus\";\n  <Composition id=\"shieldcn-plus\" component={ShieldcnPlusDemo} durationInFrames={300} fps={30} width={1280} height={720} />\n\nRequires Tailwind v4 wired into Remotion (@remotion/tailwind-v4).\nRender locally: npx remotion render shieldcn-plus out/shieldcn-plus.mp4 --gl=angle",
  "type": "registry:block"
}