{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typography",
  "title": "Typography — New Text Animations",
  "description": "remocn demo composition \"Typography — New Text Animations\" — installs the full Remotion composition. Generated with AI from the prompt in demos/typography/prompt.md.",
  "dependencies": [
    "@remotion/google-fonts",
    "@remotion/transitions",
    "culori",
    "remotion"
  ],
  "registryDependencies": [
    "https://remocn.dev/r/backdrop.json",
    "https://remocn.dev/r/blur-out-up.json",
    "https://remocn.dev/r/bottom-up-letters.json",
    "https://remocn.dev/r/caret.json",
    "https://remocn.dev/r/fade-through.json",
    "https://remocn.dev/r/focus-blur-resolve.json",
    "https://remocn.dev/r/kinetic-center-build.json",
    "https://remocn.dev/r/line-by-line-slide.json",
    "https://remocn.dev/r/mask-reveal-up.json",
    "https://remocn.dev/r/micro-scale-fade.json",
    "https://remocn.dev/r/per-character-rise.json",
    "https://remocn.dev/r/per-word-crossfade.json",
    "https://remocn.dev/r/remocn-ui.json",
    "https://remocn.dev/r/scale-down-fade.json",
    "https://remocn.dev/r/shared-axis-y.json",
    "https://remocn.dev/r/shared-axis-z.json",
    "https://remocn.dev/r/short-slide-down.json",
    "https://remocn.dev/r/short-slide-right.json",
    "https://remocn.dev/r/soft-blur-in.json",
    "https://remocn.dev/r/spring-scale-in.json",
    "https://remocn.dev/r/top-down-letters.json"
  ],
  "files": [
    {
      "path": "src/components/remocn/typewriter.tsx",
      "content": "\"use client\";\n\nimport { Caret } from \"@/components/remocn/caret\";\nimport { useTypewriter } from \"@/lib/remocn-ui\";\n\nexport interface TypewriterProps {\n  text: string;\n  cursor?: boolean;\n  charsPerSecond?: number;\n  speed?: number;\n  fontSize?: number;\n  color?: string;\n  cursorColor?: string;\n  fontWeight?: number;\n  className?: string;\n}\n\nexport function Typewriter({\n  text,\n  cursor = true,\n  charsPerSecond = 22,\n  speed = 1,\n  fontSize = 48,\n  color = \"#171717\",\n  cursorColor = \"#171717\",\n  fontWeight = 600,\n  className,\n}: TypewriterProps) {\n  const tw = useTypewriter(text, { cps: charsPerSecond, speed });\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      <span\n        className={className}\n        style={{\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        {tw.text}\n        {cursor && (\n          <Caret\n            color={cursorColor}\n            blink={!tw.typing}\n            speed={speed}\n            radius={0}\n            style={{\n              width: \"0.08em\",\n              height: \"1em\",\n              marginLeft: \"0.04em\",\n              verticalAlign: \"text-bottom\",\n            }}\n          />\n        )}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/typewriter.tsx"
    },
    {
      "path": "src/demos/typography/index.tsx",
      "content": "import React, { type ReactNode } from \"react\";\nimport { AbsoluteFill, Easing, Loop, Sequence, interpolate, useCurrentFrame } from \"remotion\";\nimport { demoAsset } from \"@/lib/demo-assets\";\nimport {\n  TransitionSeries,\n  linearTiming,\n  type TransitionPresentation,\n} from \"@remotion/transitions\";\nimport { fade, type FadeProps } from \"@remotion/transitions/fade\";\nimport { loadFont } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/JetBrainsMono\";\n\nimport { RemocnUIProvider } from \"@/lib/remocn-ui\";\nimport { Backdrop } from \"@/components/remocn/backdrop\";\n\n// Typography animations — the cast of this showcase.\nimport { SoftBlurIn } from \"@/components/remocn/soft-blur-in\";\nimport { Typewriter } from \"@/components/remocn/typewriter\";\nimport { FocusBlurResolve } from \"@/components/remocn/focus-blur-resolve\";\nimport { PerCharacterRise } from \"@/components/remocn/per-character-rise\";\nimport { TopDownLetters } from \"@/components/remocn/top-down-letters\";\nimport { BottomUpLetters } from \"@/components/remocn/bottom-up-letters\";\nimport { SpringScaleIn } from \"@/components/remocn/spring-scale-in\";\nimport { MicroScaleFade } from \"@/components/remocn/micro-scale-fade\";\nimport { ScaleDownFade } from \"@/components/remocn/scale-down-fade\";\nimport { MaskRevealUp } from \"@/components/remocn/mask-reveal-up\";\nimport { LineByLineSlide } from \"@/components/remocn/line-by-line-slide\";\nimport { KineticCenterBuild } from \"@/components/remocn/kinetic-center-build\";\nimport { ShortSlideDown } from \"@/components/remocn/short-slide-down\";\nimport { ShortSlideRight } from \"@/components/remocn/short-slide-right\";\nimport { BlurOutUp } from \"@/components/remocn/blur-out-up\";\nimport { FadeThrough } from \"@/components/remocn/fade-through\";\nimport { SharedAxisY } from \"@/components/remocn/shared-axis-y\";\nimport { SharedAxisZ } from \"@/components/remocn/shared-axis-z\";\nimport { PerWordCrossfade } from \"@/components/remocn/per-word-crossfade\";\n\n// Manrope, bound to the CSS variable every remocn typography component reads.\nconst { fontFamily } = loadFont(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"500\", \"600\", \"700\", \"800\"],\n});\n\nconst FONT_STACK = `${fontFamily}, sans-serif`;\nconst INK = \"#fafafa\";\n\n// Monospace for the install command pill.\nconst { fontFamily: monoFamily } = loadMono(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"500\"],\n});\nconst MONO_STACK = `${monoFamily}, ui-monospace, SFMono-Regular, Menlo, monospace`;\n\n// Global slow-down for the full-screen demos. Components advance their internal\n// clock by `useCurrentFrame() * speed`, so < 1 plays the motion more slowly and\n// gives each animation room to breathe.\nconst SPEED = 0.85;\n\n// Trims the held tail of each montage scene so the faster effects don't sit\n// around — keeps the pacing snappy. (Enter still completes before Stage's exit.)\nconst DUR_SCALE = 0.85;\n\nconst NAME_SIZE = 70;\nconst NAME_WEIGHT = 700;\nconst CARD_SIZE = 26;\n\n// ---------------------------------------------------------------------------\n// Animation registry — one source of truth used by BOTH the overview grid\n// (small sample) and the full-screen montage (the effect spelled out by name).\n// ---------------------------------------------------------------------------\ntype Anim = {\n  name: string;\n  /** montage scene length, frames @30fps */\n  dur: number;\n  /** full-screen render; `speed` slows the effect's internal clock. */\n  full: (speed: number) => ReactNode;\n  /** small grid-card sample (looped) */\n  card: () => ReactNode;\n};\n\nconst ANIMATIONS: Anim[] = [\n  {\n    name: \"soft-blur-in\",\n    dur: 90,\n    full: (s) => <SoftBlurIn text=\"Soft blur in\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <SoftBlurIn text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"typewriter\",\n    dur: 80,\n    full: (s) => (\n      <Centered>\n        <Typewriter text=\"Typewriter\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} cursorColor={INK} charsPerSecond={16} speed={s} />\n      </Centered>\n    ),\n    card: () => (\n      <Centered cardHeight>\n        <Typewriter text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} cursorColor={INK} charsPerSecond={14} speed={SPEED} />\n      </Centered>\n    ),\n  },\n  {\n    name: \"focus-blur-resolve\",\n    dur: 90,\n    full: (s) => <FocusBlurResolve text=\"Focus blur resolve\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <FocusBlurResolve text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"per-character-rise\",\n    dur: 96,\n    full: (s) => <PerCharacterRise text=\"Per character rise\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <PerCharacterRise text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"top-down-letters\",\n    dur: 92,\n    // Tighter cascade + slightly higher speed so 16 letters finish well within\n    // the scene (default stagger 3 × speed 0.62 ran past it).\n    full: () => <TopDownLetters text=\"Top down letters\" staggerDelay={2} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={0.9} />,\n    card: () => <TopDownLetters text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"bottom-up-letters\",\n    dur: 94,\n    full: () => <BottomUpLetters text=\"Bottom up letters\" staggerDelay={2} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={0.9} />,\n    card: () => <BottomUpLetters text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"spring-scale-in\",\n    dur: 74,\n    full: (s) => <SpringScaleIn text=\"Spring scale in\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <SpringScaleIn text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"micro-scale-fade\",\n    dur: 74,\n    full: (s) => <MicroScaleFade text=\"Micro scale fade\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <MicroScaleFade text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"scale-down-fade\",\n    dur: 74,\n    full: (s) => <ScaleDownFade text=\"Scale down fade\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <ScaleDownFade text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,\n  },\n  {\n    name: \"mask-reveal-up\",\n    dur: 86,\n    full: (s) => <MaskRevealUp text={\"Mask reveal\\nup\"} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <MaskRevealUp text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,\n  },\n  {\n    name: \"line-by-line-slide\",\n    dur: 92,\n    full: (s) => <LineByLineSlide text={\"Line by line\\nslide\"} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <LineByLineSlide text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"kinetic-center-build\",\n    dur: 90,\n    full: (s) => <KineticCenterBuild text=\"Kinetic center build\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <KineticCenterBuild text=\"Remocn UI\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"short-slide-down\",\n    dur: 94,\n    full: (s) => <ShortSlideDown text=\"Short slide down\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <ShortSlideDown text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"short-slide-right\",\n    dur: 86,\n    full: (s) => <ShortSlideRight text=\"Short slide right\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <ShortSlideRight text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"blur-out-up\",\n    dur: 84,\n    full: (s) => <BlurOutUp text=\"Blur out up\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <BlurOutUp text=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,\n  },\n  {\n    name: \"fade-through\",\n    dur: 82,\n    full: (s) => <FadeThrough fromText=\"Static text\" toText=\"Fade through\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <FadeThrough fromText=\"Before\" toText=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"shared-axis-y\",\n    dur: 84,\n    full: (s) => <SharedAxisY fromText=\"Outgoing\" toText=\"Shared axis y\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <SharedAxisY fromText=\"Before\" toText=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"shared-axis-z\",\n    dur: 90,\n    full: (s) => <SharedAxisZ fromText=\"Outgoing\" toText=\"Shared axis z\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <SharedAxisZ fromText=\"Before\" toText=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n  {\n    name: \"per-word-crossfade\",\n    dur: 100,\n    full: (s) => <PerWordCrossfade fromText=\"Word by word\" toText=\"Per word crossfade\" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,\n    card: () => <PerWordCrossfade fromText=\"Word by word\" toText=\"Remocn\" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,\n  },\n];\n\n// A relative full-frame box so absolute-centred components (and Sequenced ones\n// like Typewriter) land in the middle of their slot.\nconst Centered: React.FC<{ children: ReactNode; cardHeight?: boolean }> = ({\n  children,\n  cardHeight,\n}) => (\n  <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n    <div\n      style={{\n        position: \"relative\",\n        width: \"100%\",\n        height: cardHeight ? CARD_SIZE * 1.8 : NAME_SIZE * 1.6,\n      }}\n    >\n      {children}\n    </div>\n  </AbsoluteFill>\n);\n\n// ---------------------------------------------------------------------------\n// Transitions — NO camera movement. Every hand-off is a short, in-place opacity\n// cross-fade; the scene never slides, scales, or pushes. The visual change is\n// carried entirely by the text effects: each one animates in on the spot, and\n// Stage dissolves it out on the spot. The \"transition\" lives in the animation.\n// ---------------------------------------------------------------------------\ntype Trans = { dur: number; presentation: () => TransitionPresentation<FadeProps> };\nconst td = (dur: number, presentation: () => TransitionPresentation<FadeProps>): Trans => ({\n  dur,\n  presentation,\n});\n\nconst DISSOLVE: Trans = td(12, () => fade());\n\n// In-place exit: as a scene ends, its text dissolves away where it stands\n// (opacity + a soft blur, NO translate/scale) so it has cleared before the next\n// text resolves — no two words overlap, and nothing moves across the frame.\nconst STAGE_EXIT = 16;\nconst Stage: React.FC<{ dur: number; transOut: number; children: ReactNode }> = ({\n  dur,\n  transOut,\n  children,\n}) => {\n  const frame = useCurrentFrame();\n  const exitEnd = dur - transOut + 2;\n  const p = interpolate(frame, [exitEnd - STAGE_EXIT, exitEnd], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.in(Easing.cubic),\n  });\n  return (\n    <AbsoluteFill\n      style={{\n        opacity: 1 - p,\n        filter: p > 0.001 ? `blur(${p * 10}px)` : undefined,\n      }}\n    >\n      {children}\n    </AbsoluteFill>\n  );\n};\n\n\n// ---------------------------------------------------------------------------\n// Hand-drawn mark: each path is normalised to pathLength 1, then \"drawn\" by\n// sweeping its dash offset from hidden (1) to fully shown (0).\n// ---------------------------------------------------------------------------\nconst IntroIcon: React.FC<{ size?: number }> = ({ size = 86 }) => {\n  const frame = useCurrentFrame();\n  const appear = interpolate(frame, [0, 8], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const draw1 = interpolate(frame, [2, 30], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.inOut(Easing.cubic),\n  });\n  const draw2 = interpolate(frame, [16, 46], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.inOut(Easing.cubic),\n  });\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={INK}\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      style={{ opacity: appear }}\n    >\n      <path\n        pathLength={1}\n        strokeDasharray={1}\n        strokeDashoffset={draw1}\n        d=\"M14 19L11.1069 10.7479C9.76348 6.91597 9.09177 5 8 5C6.90823 5 6.23652 6.91597 4.89309 10.7479L2 19M4.5 12H11.5\"\n      />\n      <path\n        pathLength={1}\n        strokeDasharray={1}\n        strokeDashoffset={draw2}\n        d=\"M21.9692 13.9392V18.4392M21.9692 13.9392C22.0164 13.1161 22.0182 12.4891 21.9194 11.9773C21.6864 10.7709 20.4258 10.0439 19.206 9.89599C18.0385 9.75447 17.1015 10.055 16.1535 11.4363M21.9692 13.9392L19.1256 13.9392C18.6887 13.9392 18.2481 13.9603 17.8272 14.0773C15.2545 14.7925 15.4431 18.4003 18.0233 18.845C18.3099 18.8944 18.6025 18.9156 18.8927 18.9026C19.5703 18.8724 20.1955 18.545 20.7321 18.1301C21.3605 17.644 21.9692 16.9655 21.9692 15.9392V13.9392Z\"\n      />\n    </svg>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Overview grid: every effect plays on its own card; the cards wave in\n// diagonally, hold, then the target card pulls focus and the camera dives in.\n// ---------------------------------------------------------------------------\nconst GRID = 168;\nconst GRID_ZOOM = 34; // closing camera push\nconst GRID_ZOOM_START = GRID - GRID_ZOOM;\nconst GRID_FOCUS = 20; // pre-dive focus pull on the target card\nconst CARD_LOOP = 80;\n\nconst GRID_COLS = 5;\nconst GRID_CELLS = ANIMATIONS.length + 1; // effects + one closing brand tile\nconst GRID_ROWS = Math.ceil(GRID_CELLS / GRID_COLS);\nconst ZOOM_TARGET = 0; // soft-blur-in — also the first montage scene\n\n// Card centre as a fraction of the frame, used as the zoom transform-origin.\nconst ZOOM_PAD_X = 70;\nconst ZOOM_PAD_TOP = 128;\nconst ZOOM_PAD_BOTTOM = 60;\nconst ZOOM_GAP = 16;\nconst targetCol = ZOOM_TARGET % GRID_COLS;\nconst targetRow = Math.floor(ZOOM_TARGET / GRID_COLS);\nconst cellW = (1280 - 2 * ZOOM_PAD_X - (GRID_COLS - 1) * ZOOM_GAP) / GRID_COLS;\nconst cellH =\n  (720 - ZOOM_PAD_TOP - ZOOM_PAD_BOTTOM - (GRID_ROWS - 1) * ZOOM_GAP) / GRID_ROWS;\nconst ZOOM_ORIGIN_X =\n  ((ZOOM_PAD_X + targetCol * (cellW + ZOOM_GAP) + cellW / 2) / 1280) * 100;\nconst ZOOM_ORIGIN_Y =\n  ((ZOOM_PAD_TOP + targetRow * (cellH + ZOOM_GAP) + cellH / 2) / 720) * 100;\n\n// Diagonal entrance wave shared by every tile.\nconst cardEntrance = (frame: number, index: number) => {\n  const col = index % GRID_COLS;\n  const row = Math.floor(index / GRID_COLS);\n  const appear = (col + row) * 3;\n  const enter = interpolate(frame - appear, [0, 18], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  return { appear, enter };\n};\n\nconst GridCard: React.FC<{\n  anim: Anim;\n  index: number;\n  zoom: number;\n  focus: number;\n}> = ({ anim, index, zoom, focus }) => {\n  const frame = useCurrentFrame();\n  const { appear, enter } = cardEntrance(frame, index);\n  const isTarget = index === ZOOM_TARGET;\n\n  // Pre-dive focus: the target brightens and lifts; the rest dim back so the\n  // camera clearly lands on the effect we continue into. Then the dive fades\n  // every non-target card out entirely.\n  const dim = isTarget ? 0 : focus * 0.55;\n  const zoomFade = isTarget ? 1 : 1 - zoom;\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        overflow: \"hidden\",\n        borderRadius: 16,\n        border: `1px solid rgba(255,255,255,${0.08 + (isTarget ? focus * 0.55 : 0)})`,\n        background: \"rgba(255,255,255,0.04)\",\n        boxShadow:\n          isTarget && focus > 0.01\n            ? `0 0 ${focus * 46}px rgba(255,255,255,${focus * 0.14})`\n            : \"0 12px 30px rgba(0,0,0,0.28)\",\n        opacity: enter * zoomFade * (1 - dim),\n        translate: `0px ${(1 - enter) * 18}px`,\n        scale:\n          (0.94 + enter * 0.06) *\n          (isTarget ? 1 + focus * 0.06 : 1 - focus * 0.03),\n      }}\n    >\n      {/* glassy top sheen */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background:\n            \"linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0) 42%)\",\n        }}\n      />\n      <Sequence from={Math.round(appear)} layout=\"none\">\n        <Loop durationInFrames={CARD_LOOP}>{anim.card()}</Loop>\n      </Sequence>\n      <span\n        style={{\n          position: \"absolute\",\n          top: 11,\n          left: 14,\n          fontFamily: FONT_STACK,\n          fontSize: 12,\n          fontWeight: 700,\n          color: \"rgba(250,250,250,0.5)\",\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        {String(index + 1).padStart(2, \"0\")}\n      </span>\n      <span\n        style={{\n          position: \"absolute\",\n          bottom: 11,\n          left: 14,\n          fontFamily: FONT_STACK,\n          fontSize: 12,\n          fontWeight: 500,\n          color: \"rgba(250,250,250,0.5)\",\n        }}\n      >\n        {anim.name}\n      </span>\n    </div>\n  );\n};\n\n// Closing brand tile fills the final cell so the grid reads as a clean 5×4.\nconst BrandTile: React.FC<{ index: number; zoom: number; focus: number }> = ({\n  index,\n  zoom,\n  focus,\n}) => {\n  const frame = useCurrentFrame();\n  const { appear, enter } = cardEntrance(frame, index);\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        overflow: \"hidden\",\n        borderRadius: 16,\n        border: \"1px solid rgba(255,255,255,0.1)\",\n        background: \"rgba(255,255,255,0.06)\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        gap: 8,\n        opacity: enter * (1 - zoom) * (1 - focus * 0.55),\n        translate: `0px ${(1 - enter) * 18}px`,\n        scale: (0.94 + enter * 0.06) * (1 - focus * 0.03),\n      }}\n    >\n      <Sequence from={Math.round(appear)} layout=\"none\">\n        <IntroIcon size={30} />\n      </Sequence>\n      <span\n        style={{\n          fontFamily: FONT_STACK,\n          fontSize: 17,\n          fontWeight: 600,\n          color: \"rgba(250,250,250,0.85)\",\n        }}\n      >\n        remocn\n      </span>\n    </div>\n  );\n};\n\nconst GridScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const title = interpolate(frame, [4, 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n  // The target card pulls focus in the moments before the dive...\n  const focus = interpolate(\n    frame,\n    [GRID_ZOOM_START - GRID_FOCUS, GRID_ZOOM_START],\n    [0, 1],\n    {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.out(Easing.cubic),\n    },\n  );\n  // ...then the camera pushes into it and glides it to the centre of frame.\n  const zoom = interpolate(frame, [GRID_ZOOM_START, GRID], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.in(Easing.cubic),\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        transformOrigin: `${ZOOM_ORIGIN_X}% ${ZOOM_ORIGIN_Y}%`,\n        translate: `${(50 - ZOOM_ORIGIN_X) * zoom}% ${(50 - ZOOM_ORIGIN_Y) * zoom}%`,\n        scale: 1 + zoom * 5.4,\n        filter: zoom > 0.001 ? `blur(${zoom * 10}px)` : undefined,\n      }}\n    >\n      {/* Header: drawn mark + kicker, the section title, and the count. */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: 30,\n          left: ZOOM_PAD_X,\n          right: ZOOM_PAD_X,\n          display: \"flex\",\n          alignItems: \"flex-end\",\n          justifyContent: \"space-between\",\n          opacity: title,\n        }}\n      >\n        <div style={{ display: \"flex\", flexDirection: \"column\", gap: 9 }}>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 10 }}>\n            <Sequence from={2} layout=\"none\">\n              <IntroIcon size={24} />\n            </Sequence>\n            <span\n              style={{\n                fontFamily: FONT_STACK,\n                fontSize: 20,\n                fontWeight: 600,\n                color: \"rgba(250,250,250,0.78)\",\n              }}\n            >\n              remocn\n            </span>\n          </div>\n          <span\n            style={{\n              fontFamily: FONT_STACK,\n              fontSize: 30,\n              fontWeight: 600,\n              color: INK,\n            }}\n          >\n            Every new effect\n          </span>\n        </div>\n        <span\n          style={{\n            fontFamily: FONT_STACK,\n            fontSize: 17,\n            fontWeight: 500,\n            color: \"rgba(250,250,250,0.45)\",\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          {ANIMATIONS.length} effects\n        </span>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          top: ZOOM_PAD_TOP,\n          left: ZOOM_PAD_X,\n          right: ZOOM_PAD_X,\n          bottom: ZOOM_PAD_BOTTOM,\n          display: \"grid\",\n          gridTemplateColumns: `repeat(${GRID_COLS}, 1fr)`,\n          gridAutoRows: \"1fr\",\n          gap: ZOOM_GAP,\n        }}\n      >\n        {ANIMATIONS.map((anim, i) => (\n          <GridCard key={anim.name} anim={anim} index={i} zoom={zoom} focus={focus} />\n        ))}\n        <BrandTile index={ANIMATIONS.length} zoom={zoom} focus={focus} />\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Intro & outro\n// ---------------------------------------------------------------------------\nconst INTRO = 104;\nconst OUTRO = 112;\n\n// One headline line, revealed in place: rises a touch and un-blurs (no clip, so\n// descenders like the \"p\"/\"y\" in \"typography\" are never cut off).\nconst HeadlineLine: React.FC<{ children: ReactNode; delay: number }> = ({\n  children,\n  delay,\n}) => {\n  const frame = useCurrentFrame();\n  const p = interpolate(frame, [delay, delay + 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  return (\n    <div\n      style={{\n        opacity: p,\n        transform: `translateY(${(1 - p) * 0.5}em)`,\n        filter: p < 1 ? `blur(${(1 - p) * 9}px)` : undefined,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst IntroScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const kicker = interpolate(frame, [4, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n\n  return (\n    <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: 32,\n        }}\n      >\n        {/* Small lock-up kicker — the mark draws itself in next to the wordmark. */}\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 11,\n            opacity: kicker,\n            transform: `translateY(${(1 - kicker) * 8}px)`,\n          }}\n        >\n          <IntroIcon size={34} />\n          <span\n            style={{\n              fontFamily: FONT_STACK,\n              fontSize: 25,\n              fontWeight: 600,\n              color: \"rgba(250,250,250,0.78)\",\n            }}\n          >\n            remocn\n          </span>\n        </div>\n\n        {/* Hero headline — the message, revealed line by line, in place. */}\n        <div\n          style={{\n            textAlign: \"center\",\n            fontFamily: FONT_STACK,\n            fontWeight: 700,\n            fontSize: 94,\n            lineHeight: 1.05,\n            color: INK,\n          }}\n        >\n          <HeadlineLine delay={10}>New typography</HeadlineLine>\n          <HeadlineLine delay={22}>animations</HeadlineLine>\n        </div>\n\n        {/* Typed meta line (remocn Typewriter component). */}\n        <div style={{ position: \"relative\", width: 560, height: 44 }}>\n          <Sequence from={48}>\n            <Typewriter\n              text=\"19 new effects\"\n              fontSize={28}\n              fontWeight={500}\n              color=\"rgba(250,250,250,0.7)\"\n              cursorColor=\"rgba(250,250,250,0.7)\"\n              charsPerSecond={20}\n            />\n          </Sequence>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// Inline lucide icons for the install pill.\nconst CopyIcon: React.FC<{ size: number }> = ({ size }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={2}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <rect width={14} height={14} x={8} y={8} rx={2} ry={2} />\n    <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n  </svg>\n);\n\nconst CheckIcon: React.FC<{ size: number }> = ({ size }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={2.4}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M20 6 9 17l-5-5\" />\n  </svg>\n);\n\n// remocn's hero \"install command\" pill, rebuilt for Remotion: a rounded glass\n// pill with `$ <command>` and a copy icon that ticks to a check (a simulated\n// \"copied!\" beat) — all frame-driven.\nconst InstallPill: React.FC<{ command: string; delay: number; copyAt: number }> = ({\n  command,\n  delay,\n  copyAt,\n}) => {\n  const frame = useCurrentFrame();\n  const enter = interpolate(frame, [delay, delay + 16], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  const copied = frame >= copyAt;\n  // a small pop when the icon swaps to the check\n  const pop = interpolate(frame - copyAt, [0, 8], [0.5, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.34, 1.56, 0.64, 1),\n  });\n\n  return (\n    <div\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: 13,\n        height: 52,\n        padding: \"0 22px\",\n        borderRadius: 999,\n        border: `1px solid rgba(255,255,255,${copied ? 0.22 : 0.14})`,\n        background: \"rgba(255,255,255,0.05)\",\n        backdropFilter: \"blur(10px)\",\n        WebkitBackdropFilter: \"blur(10px)\",\n        boxShadow: \"0 12px 34px rgba(0,0,0,0.3)\",\n        fontFamily: MONO_STACK,\n        fontSize: 18,\n        opacity: enter,\n        translate: `0px ${(1 - enter) * 12}px`,\n      }}\n    >\n      <span style={{ color: \"rgba(250,250,250,0.4)\" }}>$</span>\n      <span style={{ color: INK }}>{command}</span>\n      <span\n        style={{\n          display: \"inline-flex\",\n          marginLeft: 3,\n          color: copied ? \"#4ade80\" : \"rgba(250,250,250,0.55)\",\n          scale: copied ? `${pop}` : \"1\",\n        }}\n      >\n        {copied ? <CheckIcon size={17} /> : <CopyIcon size={17} />}\n      </span>\n    </div>\n  );\n};\n\nconst OutroScene: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  const word = interpolate(frame, [6, 30], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  // The whole closing group lifts away and blurs out at the very end.\n  const exit = interpolate(frame, [OUTRO - 18, OUTRO], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.in(Easing.cubic),\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        opacity: 1 - exit,\n        translate: `0px ${-exit * 60}px`,\n        filter: exit > 0.001 ? `blur(${exit * 16}px)` : undefined,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: 30,\n        }}\n      >\n        {/* Brand lock-up: the mark draws in beside the wordmark (a bookend to\n            the intro). */}\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: 18 }}>\n          <span\n            style={{\n              fontFamily: FONT_STACK,\n              fontSize: 96,\n              fontWeight: 600,\n              color: INK,\n              lineHeight: 1,\n              opacity: word,\n              filter: word < 1 ? `blur(${(1 - word) * 12}px)` : undefined,\n              translate: `0px ${(1 - word) * 10}px`,\n            }}\n          >\n            remocn\n          </span>\n        </div>\n\n      \n\n        <InstallPill\n          command=\"npx shadcn@latest add remocn/soft-blur-in\"\n          delay={52}\n          copyAt={86}\n        />\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Scene assembly\n// ---------------------------------------------------------------------------\ntype SceneSlot = {\n  node: ReactNode;\n  dur: number;\n  /** transition INTO the next scene */\n  trans?: { dur: number; presentation: () => TransitionPresentation<FadeProps> };\n};\n\nconst buildScenes = (): SceneSlot[] => {\n  const slots: SceneSlot[] = [];\n\n  slots.push({ node: <IntroScene />, dur: INTRO, trans: DISSOLVE });\n\n  // Overview grid (its own dive into the first card stays as a one-off).\n  slots.push({ node: <GridScene />, dur: GRID, trans: DISSOLVE });\n\n  ANIMATIONS.forEach((anim) => {\n    const d = Math.round(anim.dur * DUR_SCALE);\n    // Stationary scene: the effect plays in place, then Stage dissolves it out in\n    // place before the in-place cross-fade brings the next effect on. No motion.\n    slots.push({\n      node: (\n        <Stage dur={d} transOut={DISSOLVE.dur}>\n          {anim.full(SPEED)}\n        </Stage>\n      ),\n      dur: d,\n      trans: DISSOLVE,\n    });\n  });\n\n  slots.push({ node: <OutroScene />, dur: OUTRO });\n  return slots;\n};\n\nconst SCENES = buildScenes();\n\nexport const TYPOGRAPHY_DURATION =\n  SCENES.reduce((a, s) => a + s.dur, 0) -\n  SCENES.reduce((a, s) => a + (s.trans?.dur ?? 0), 0);\n\n// ---------------------------------------------------------------------------\n// Persistent scene counter — the tag itself never moves or fades between\n// montage scenes; only its ordinal (and effect name) tick over. Lives OUTSIDE\n// the TransitionSeries so the scene transitions can't drag it around.\n// ---------------------------------------------------------------------------\n// Composition-frame start of each slot (accounts for transition overlaps).\nconst SCENE_STARTS = (() => {\n  const starts: number[] = [];\n  let acc = 0;\n  for (const s of SCENES) {\n    starts.push(acc);\n    acc += s.dur - (s.trans?.dur ?? 0);\n  }\n  return starts;\n})();\n\n// Slots 0 = intro, 1 = grid, 2..(N+1) = montage effects, last = outro.\nconst MONTAGE_TAGS = ANIMATIONS.map((anim, i) => ({\n  start: SCENE_STARTS[i + 2],\n  index: i + 1,\n  name: anim.name,\n}));\nconst MONTAGE_START = MONTAGE_TAGS[0].start;\nconst MONTAGE_END = SCENE_STARTS[SCENES.length - 1]; // outro start\n\nconst SceneCounter: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // Which effect is on screen right now.\n  let active = 0;\n  for (let i = 0; i < MONTAGE_TAGS.length; i++) {\n    if (frame >= MONTAGE_TAGS[i].start) active = i;\n  }\n  const tag = MONTAGE_TAGS[active];\n\n  // Only visible during the montage; the chrome holds completely still.\n  const appear = interpolate(frame, [MONTAGE_START - 8, MONTAGE_START + 6], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const out = interpolate(frame, [MONTAGE_END - 12, MONTAGE_END], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const vis = appear * out;\n\n  // The changing value settles with a soft fade (no movement) on each switch.\n  const valueOpacity = interpolate(frame - tag.start, [0, 7], [0.35, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 70,\n        bottom: 60,\n        display: \"flex\",\n        alignItems: \"baseline\",\n        gap: 14,\n        opacity: vis,\n        fontFamily: FONT_STACK,\n        color: \"rgba(250,250,250,0.62)\",\n      }}\n    >\n      <span\n        style={{\n          fontSize: 22,\n          fontWeight: 700,\n          fontVariantNumeric: \"tabular-nums\",\n          opacity: valueOpacity,\n        }}\n      >\n        {String(tag.index).padStart(2, \"0\")}\n      </span>\n      <span style={{ fontSize: 15, fontWeight: 500, color: \"rgba(250,250,250,0.4)\" }}>\n        / {String(ANIMATIONS.length).padStart(2, \"0\")}\n      </span>\n      <span\n        style={{\n          fontSize: 16,\n          fontWeight: 500,\n          color: \"rgba(250,250,250,0.5)\",\n          opacity: valueOpacity,\n        }}\n      >\n        {tag.name}\n      </span>\n    </div>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Composition root\n// ---------------------------------------------------------------------------\nexport const TypographyDemo: React.FC = () => {\n  const children: ReactNode[] = [];\n  SCENES.forEach((scene, i) => {\n    children.push(\n      <TransitionSeries.Sequence key={`s-${i}`} durationInFrames={scene.dur}>\n        {scene.node}\n      </TransitionSeries.Sequence>,\n    );\n    if (scene.trans) {\n      children.push(\n        <TransitionSeries.Transition\n          key={`t-${i}`}\n          timing={linearTiming({ durationInFrames: scene.trans.dur })}\n          presentation={scene.trans.presentation()}\n        />,\n      );\n    }\n  });\n\n  return (\n    <RemocnUIProvider>\n      <AbsoluteFill\n        style={{ \"--font-geist-sans\": fontFamily } as React.CSSProperties}\n      >\n        {/* Persistent image background for the whole video. */}\n        <Backdrop fill={{ type: \"image\", src: demoAsset(\"bg.png\") }} />\n        {/* Scrim to deepen contrast under the foreground type. */}\n        <AbsoluteFill\n          style={{\n            background:\n              \"radial-gradient(120% 120% at 50% 42%, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.5) 100%)\",\n          }}\n        />\n\n        <TransitionSeries>{children}</TransitionSeries>\n\n        {/* Persistent counter overlay — stays put while scenes change beneath. */}\n        <SceneCounter />\n      </AbsoluteFill>\n    </RemocnUIProvider>\n  );\n};\n",
      "type": "registry:component",
      "target": "demos/typography/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"
    },
    {
      "path": "src/demos/typography/prompt.md",
      "content": "<!-- TODO(draft): placeholder written by AI — replace with the real prompt used to generate this video -->\n\nMake a showcase video for the new text animations we added to remocn. The gimmick should be that each animation introduces itself by animating its own name — so the typewriter effect types out \"typewriter\", the split-text effect splits \"split-text\", and so on down the list. Put it over an image backdrop rather than a flat color, and keep the transitions between each one dynamic and kinetic so it doesn't feel like a static list. Keep it snappy, no long holds, just effect after effect proving itself. Use remocn's actual text animation components for all of it.\n",
      "type": "registry:file",
      "target": "demos/typography/prompt.md"
    }
  ],
  "docs": "Register the composition in your Remotion Root:\n\n  import { TypographyDemo } from \"@/demos/typography\";\n  <Composition id=\"typography\" component={TypographyDemo} durationInFrames={1538} fps={30} width={1280} height={720} />\n\nRequires Tailwind v4 wired into Remotion (@remotion/tailwind-v4).\nRender locally: npx remotion render typography out/typography.mp4",
  "type": "registry:block"
}