{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "changelog",
  "title": "Changelog — Rolling Numbers",
  "description": "remocn demo composition \"Changelog — Rolling Numbers\" — installs the full Remotion composition. Generated with AI from the prompt in demos/changelog/prompt.md.",
  "dependencies": [
    "@remotion/google-fonts",
    "@remotion/transitions",
    "culori",
    "remotion"
  ],
  "registryDependencies": [
    "https://remocn.dev/r/backdrop.json",
    "https://remocn.dev/r/caret.json",
    "https://remocn.dev/r/remocn-ui.json",
    "https://remocn.dev/r/rolling-number.json"
  ],
  "files": [
    {
      "path": "src/components/remocn/blur-reveal.tsx",
      "content": "\"use client\";\n\nimport { interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface BlurRevealProps {\n  text: string;\n  className?: string;\n  blur?: number;\n  fontSize?: number;\n  color?: string;\n  fontWeight?: number;\n  speed?: number;\n}\n\nexport function BlurReveal({\n  text,\n  className,\n  blur = 10,\n  fontSize = 48,\n  color = \"#171717\",\n  fontWeight = 600,\n  speed = 1,\n}: BlurRevealProps) {\n  const frame = useCurrentFrame() * speed;\n  const { durationInFrames } = useVideoConfig();\n\n  const opacity = interpolate(frame, [0, durationInFrames * 0.6], [0, 1], {\n    extrapolateRight: \"clamp\",\n  });\n\n  const blurAmount = interpolate(\n    frame,\n    [0, durationInFrames * 0.6],\n    [blur, 0],\n    { extrapolateRight: \"clamp\" },\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      <span\n        className={className}\n        style={{\n          opacity,\n          filter: `blur(${blurAmount}px)`,\n          fontSize,\n          fontWeight,\n          color,\n          letterSpacing: \"-0.05em\",\n          fontFamily:\n            \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n        }}\n      >\n        {text}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/blur-reveal.tsx"
    },
    {
      "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/changelog/index.tsx",
      "content": "import React from \"react\";\nimport { AbsoluteFill, Sequence, Easing, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { demoAsset } from \"@/lib/demo-assets\";\nimport {\n  TransitionSeries,\n  linearTiming,\n  type TransitionPresentation,\n  type TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\nimport { loadFont } from \"@remotion/google-fonts/Manrope\";\n\nimport { RemocnUIProvider } from \"@/lib/remocn-ui\";\nimport { Backdrop } from \"@/components/remocn/backdrop\";\nimport { BlurReveal } from \"@/components/remocn/blur-reveal\";\nimport { Typewriter } from \"@/components/remocn/typewriter\";\nimport { RollingNumber } from \"@/components/remocn/rolling-number\";\n\n// Bind Inter to the CSS variable the remocn components read for their font.\nconst { fontFamily } = loadFont(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"600\", \"700\"],\n});\n\n// ---------------------------------------------------------------------------\n// Scene timings (frames @ 30fps)\n// ---------------------------------------------------------------------------\nconst INTRO = 75;\nconst DEMO = 105;\nconst OUTRO = 70;\nconst T_FADE = 18;\nconst T_SLIDE = 18;\nconst T_GAP = 16; // kinetic transition between consecutive metric scenes\n\n// Shared \"vanish\" parameters: the intro disappears with these, and the outro\n// arrives by replaying the exact same motion in reverse.\nconst EXIT_RISE = 80;\nconst EXIT_BLUR = 16;\nconst EXIT_SCALE_DROP = 0.06;\n\nexport const CHANGELOG_DURATION =\n  INTRO + DEMO * 4 + OUTRO - T_FADE - T_SLIDE - 3 * T_GAP;\n\n// ---------------------------------------------------------------------------\n// Intro: version badge (top-right, blurReveal) + animation label\n//        (bottom-right, typewriter)\n// ---------------------------------------------------------------------------\nconst IntroScene: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // Noticeable exit during the scene's final frames: the whole intro lifts up,\n  // blurs and fades away (rather than just dissolving). This plays out while\n  // the cross-fade hands over to the demo, so the intro clearly \"leaves\".\n  const exit = interpolate(frame, [INTRO - T_FADE, INTRO], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.in(Easing.cubic),\n  });\n  const exitStyle: React.CSSProperties = {\n    opacity: 1 - exit,\n    filter: exit > 0 ? `blur(${exit * EXIT_BLUR}px)` : undefined,\n    transform: `translateY(${-exit * EXIT_RISE}px) scale(${\n      1 - exit * EXIT_SCALE_DROP\n    })`,\n  };\n\n  return (\n    <AbsoluteFill style={exitStyle}>\n      {/* Top-right — \"Remocn v2.0.6\" revealed with a blur. */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: 56,\n          left: 30,\n          width: 380,\n          height: 64,\n        }}\n      >\n        {/* Window spans the whole scene so the text stays after revealing. */}\n        <Sequence durationInFrames={INTRO}>\n          <BlurReveal\n            text=\"Remocn v2.0.6\"\n            fontSize={40}\n            color=\"#fafafa\"\n            fontWeight={700}\n            blur={14}\n          />\n        </Sequence>\n      </div>\n\n      {/* Bottom-right — \"Animation: Rolling Numbers\" typed into a pill badge. */}\n      <div\n        style={{\n          position: \"absolute\",\n          bottom: 80,\n          left: 70,\n          width: 430,\n          height: 56,\n        }}\n      >\n        <Sequence from={22} durationInFrames={INTRO - 22}>\n          <Typewriter\n            text=\"Animation: Rolling Numbers\"\n            fontSize={48}\n            color=\"#fff\"\n            cursorColor=\"#fff\"\n            fontWeight={600}\n            charsPerSecond={26}\n          />\n        </Sequence>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Demo: the rolling-number component, with a fading caption.\n// ---------------------------------------------------------------------------\nconst DemoContent: React.FC<{\n  label: string;\n  count: number;\n  countFrames: number;\n}> = ({ label, count, countFrames }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const captionOpacity = interpolate(frame, [0, 0.5 * fps], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n\n  return (\n    <AbsoluteFill>\n      {/* Caption sits above the full-screen, centred RollingNumber. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: \"30%\",\n          textAlign: \"center\",\n          opacity: captionOpacity,\n          fontFamily: `${fontFamily}, sans-serif`,\n          fontSize: 18,\n          fontWeight: 600,\n          textTransform: \"uppercase\",\n          color: \"rgba(250,250,250,0.6)\",\n        }}\n      >\n        {label}\n      </div>\n\n      {/* Drive the count once the content begins. */}\n      <Sequence durationInFrames={countFrames}>\n        <RollingNumber from={0} to={count} fontSize={150} color=\"#fafafa\" />\n      </Sequence>\n    </AbsoluteFill>\n  );\n};\n\nconst DemoScene: React.FC<{\n  label: string;\n  count: number;\n  /** Hold the scene empty for this many frames before it starts playing. */\n  startDelay?: number;\n}> = ({ label, count, startDelay = 0 }) => {\n  return (\n    <AbsoluteFill>\n      <Sequence from={startDelay} durationInFrames={DEMO - startDelay}>\n        <DemoContent\n          label={label}\n          count={count}\n          countFrames={DEMO - startDelay}\n        />\n      </Sequence>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Outro: the wordmark revealed with a blur.\n// ---------------------------------------------------------------------------\nconst OutroScene: React.FC = () => {\n  return (\n    <AbsoluteFill>\n      <Sequence durationInFrames={OUTRO}>\n        <BlurReveal\n          text=\"remocn\"\n          fontSize={132}\n          color=\"#fafafa\"\n          fontWeight={700}\n          blur={18}\n        />\n      </Sequence>\n    </AbsoluteFill>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// Outro transition: the reverse of the intro's disappearance. The leaving demo\n// dissolves, and the outro arrives by replaying the intro vanish backwards —\n// descending from above, un-blurring and scaling into place.\n// ---------------------------------------------------------------------------\nconst BlurRisePresentation: React.FC<\n  TransitionPresentationComponentProps<Record<string, never>>\n> = ({ children, presentationProgress, presentationDirection }) => {\n  const entering = presentationDirection === \"entering\";\n\n  // Ease so the entrance settles softly (the inverse of the intro's\n  // accelerating exit).\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: entering ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),\n  });\n\n  const style: React.CSSProperties = entering\n    ? {\n        opacity: p,\n        filter: p < 1 ? `blur(${(1 - p) * EXIT_BLUR}px)` : undefined,\n        transform: `translateY(${-(1 - p) * EXIT_RISE}px) scale(${\n          1 - (1 - p) * EXIT_SCALE_DROP\n        })`,\n      }\n    : {\n        // The outgoing demo simply dissolves so it doesn't fight the reveal.\n        opacity: 1 - p,\n        filter: p > 0 ? `blur(${p * EXIT_BLUR}px)` : undefined,\n      };\n\n  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;\n};\n\nconst blurRise = (): TransitionPresentation<Record<string, never>> => ({\n  component: BlurRisePresentation,\n  props: {},\n});\n\n// ---------------------------------------------------------------------------\n// Kinetic slide between metric scenes: the two scenes push each other along an\n// axis while a motion blur peaks mid-transition and the content dips in scale,\n// giving each hand-off a fast, energetic \"whoosh\" instead of a hard cut.\n// ---------------------------------------------------------------------------\ntype SlideDir = \"from-bottom\" | \"from-top\" | \"from-left\" | \"from-right\";\n\nconst KineticSlidePresentation: React.FC<\n  TransitionPresentationComponentProps<{ direction: SlideDir }>\n> = ({ children, presentationProgress, presentationDirection, passedProps }) => {\n  const { direction } = passedProps;\n  const entering = presentationDirection === \"entering\";\n\n  const p = interpolate(presentationProgress, [0, 1], [0, 1], {\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  const axis = direction === \"from-left\" || direction === \"from-right\" ? \"x\" : \"y\";\n  // Where the entering scene starts (off-screen), as a sign of 100%.\n  const enterSign = direction === \"from-right\" || direction === \"from-bottom\" ? 1 : -1;\n\n  // Entering travels start -> 0; exiting is pushed 0 -> opposite side.\n  const offsetPct = entering ? enterSign * (1 - p) * 100 : -enterSign * p * 100;\n  const translate =\n    axis === \"x\" ? `translateX(${offsetPct}%)` : `translateY(${offsetPct}%)`;\n\n  // Speed cue: blur + scale dip peak at the mid-point of the move.\n  const motion = Math.sin(p * Math.PI);\n  const blur = motion * 12;\n  const scale = 1 - motion * 0.08;\n\n  const style: React.CSSProperties = {\n    transform: `${translate} scale(${scale})`,\n    filter: blur > 0.1 ? `blur(${blur}px)` : undefined,\n  };\n\n  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;\n};\n\nconst kineticSlide = (\n  direction: SlideDir,\n): TransitionPresentation<{ direction: SlideDir }> => ({\n  component: KineticSlidePresentation,\n  props: { direction },\n});\n\n// ---------------------------------------------------------------------------\n// Composition root\n// ---------------------------------------------------------------------------\nexport const ChangelogDemo: React.FC = () => {\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        {/* Subtle scrim to deepen contrast for the foreground text. */}\n        <AbsoluteFill\n          style={{\n            background:\n              \"radial-gradient(120% 120% at 50% 40%, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.45) 100%)\",\n          }}\n        />\n\n        <TransitionSeries>\n          <TransitionSeries.Sequence durationInFrames={INTRO}>\n            <IntroScene />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_FADE })}\n            presentation={fade()}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={DEMO}>\n            {/* Wait out the cross-fade so the count starts after the intro leaves. */}\n            <DemoScene label=\"Pageview\" count={51261} startDelay={T_FADE} />\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_GAP })}\n            presentation={kineticSlide(\"from-bottom\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={DEMO}>\n            <DemoScene label=\"Unique Visitors\" count={4426}/>\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_GAP })}\n            presentation={kineticSlide(\"from-right\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={DEMO}>\n            <DemoScene label=\"Sessions\" count={4871}/>\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_GAP })}\n            presentation={kineticSlide(\"from-bottom\")}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={DEMO}>\n            <DemoScene label=\"Bounce Rate\" count={47}/>\n          </TransitionSeries.Sequence>\n\n          <TransitionSeries.Transition\n            timing={linearTiming({ durationInFrames: T_SLIDE })}\n            presentation={blurRise()}\n          />\n\n          <TransitionSeries.Sequence durationInFrames={OUTRO}>\n            <OutroScene />\n          </TransitionSeries.Sequence>\n        </TransitionSeries>\n      </AbsoluteFill>\n    </RemocnUIProvider>\n  );\n};\n",
      "type": "registry:component",
      "target": "demos/changelog/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/changelog/prompt.md",
      "content": "<!-- TODO(draft): placeholder written by AI — replace with the real prompt used to generate this video -->\n\nMake a quick changelog video for remocn v2.0.6 that shows off the rolling-number animation we added. Keep it short and focused — just enough setup to explain what changelog this is, then let the rolling-number component actually demo itself counting up so people see the motion. Use remocn's own rolling-number component for the demo, standard remocn changelog styling.\n",
      "type": "registry:file",
      "target": "demos/changelog/prompt.md"
    }
  ],
  "docs": "Register the composition in your Remotion Root:\n\n  import { ChangelogDemo } from \"@/demos/changelog\";\n  <Composition id=\"changelog\" component={ChangelogDemo} durationInFrames={481} fps={30} width={1280} height={720} />\n\nRequires Tailwind v4 wired into Remotion (@remotion/tailwind-v4).\nRender locally: npx remotion render changelog out/changelog.mp4",
  "type": "registry:block"
}