{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "new-transitions",
  "title": "Changelog — Eleven New Transitions",
  "description": "remocn demo composition \"Changelog — Eleven New Transitions\" — installs the full Remotion composition. Generated with AI from the prompt in demos/new-transitions/prompt.md.",
  "dependencies": [
    "@paper-design/shaders-react",
    "@remotion/google-fonts",
    "@remotion/transitions",
    "remotion"
  ],
  "registryDependencies": [
    "https://remocn.dev/r/blur-out-up.json",
    "https://remocn.dev/r/dither-dissolve.json",
    "https://remocn.dev/r/focus-pull.json",
    "https://remocn.dev/r/grain-dissolve.json",
    "https://remocn.dev/r/line-by-line-slide.json",
    "https://remocn.dev/r/perlin-dissolve.json",
    "https://remocn.dev/r/push-through.json",
    "https://remocn.dev/r/ripple-zoom.json",
    "https://remocn.dev/r/scale-down-fade.json",
    "https://remocn.dev/r/shader-dithering.json",
    "https://remocn.dev/r/shader-grain-gradient.json",
    "https://remocn.dev/r/shader-perlin-noise.json",
    "https://remocn.dev/r/shader-smoke-ring.json",
    "https://remocn.dev/r/shader-swirl.json",
    "https://remocn.dev/r/shader-warp.json",
    "https://remocn.dev/r/short-slide-right.json",
    "https://remocn.dev/r/smoke-dissolve.json",
    "https://remocn.dev/r/swirl-dissolve.json",
    "https://remocn.dev/r/warp-dissolve.json",
    "https://remocn.dev/r/wave-wipe.json",
    "https://remocn.dev/r/whip-pan.json"
  ],
  "files": [
    {
      "path": "src/demos/new-transitions/index.tsx",
      "content": "import React from \"react\";\nimport { AbsoluteFill, Easing, Img, Sequence, interpolate, spring, 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 { loadFont as loadSans } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\n\nimport { ScaleDownFade } from \"@/components/remocn/scale-down-fade\";\nimport { ShortSlideRight } from \"@/components/remocn/short-slide-right\";\nimport { LineByLineSlide } from \"@/components/remocn/line-by-line-slide\";\nimport { BlurOutUp } from \"@/components/remocn/blur-out-up\";\n\nimport { whipPan } from \"@/components/remocn/whip-pan\";\nimport { pushThrough } from \"@/components/remocn/push-through\";\nimport { focusPull } from \"@/components/remocn/focus-pull\";\nimport { ditherDissolve } from \"@/components/remocn/dither-dissolve\";\nimport { waveWipe } from \"@/components/remocn/wave-wipe\";\nimport { grainDissolve } from \"@/components/remocn/grain-dissolve\";\nimport { rippleZoom } from \"@/components/remocn/ripple-zoom\";\nimport { warpDissolve } from \"@/components/remocn/warp-dissolve\";\nimport { perlinDissolve } from \"@/components/remocn/perlin-dissolve\";\nimport { smokeDissolve } from \"@/components/remocn/smoke-dissolve\";\nimport { swirlDissolve } from \"@/components/remocn/swirl-dissolve\";\n\nconst { fontFamily: SANS_FAMILY } = loadSans(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"500\"],\n});\nconst { fontFamily: MONO_FAMILY } = loadMono(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\"],\n});\n\nconst SANS = `${SANS_FAMILY}, -apple-system, BlinkMacSystemFont, sans-serif`;\nconst MONO = `${MONO_FAMILY}, ui-monospace, SFMono-Regular, monospace`;\n\nconst OBSIDIAN = \"#141318\";\nconst INK = \"#f2f2f2\";\nconst MUTED = \"rgba(242,242,242,0.62)\";\nconst FAINT = \"rgba(242,242,242,0.4)\";\nconst LIME = \"#C3E88D\";\n\nconst clampOpts = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\n// ---------------------------------------------------------------------------\n// Scene timings (frames @ 30fps). Transitions overlap the adjacent scenes.\n// ---------------------------------------------------------------------------\nconst S_HOOK = 100;\nconst S_INTRO = 110;\nconst S_VALUE = 170;\nconst S_INSTALL = 152;\nconst S_OUTRO = 170;\n\nconst T_XF = 14; //  crossfade — the last deliberately plain cut\nconst T_VALUE = 46; // focus-pull, reused into the value block\nconst T_INSTALL = 26; // whip-pan up, reused into the install beat\nconst T_OUTRO = 40; // push-through, the climactic arrival\n\n// The remocn mark — the real brand asset from remocn.dev/logo.svg.\nconst RemocnMark: React.FC<{ size: number }> = ({ size }) => (\n  <Img\n    src={demoAsset(\"remocn-logo.svg\")}\n    style={{ width: size, height: size, display: \"block\" }}\n  />\n);\n\n// ===========================================================================\n// The showcase — each beat enters through the transition it names.\n// Labels are static on purpose: all motion belongs to the cut itself.\n// ===========================================================================\ntype ShowcaseBeat = {\n  name: string;\n  dur: number;\n  tIn: number;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  presentation: () => TransitionPresentation<any>;\n  transparent?: boolean;\n  // Obsidian layer fading in over [from, to] (scene-local frames). Needed\n  // when the incoming shader field ends on a solid color that expects an\n  // opaque scene to cover it — the veil resolves it into the root obsidian\n  // without an unmount pop.\n  veil?: { from: number; to: number };\n  tint: string;\n};\n\nconst BEATS: ShowcaseBeat[] = [\n  {\n    name: \"Whip Pan\",\n    dur: 92,\n    tIn: 26,\n    presentation: () => whipPan({ direction: \"left\" }),\n    tint: \"#1a1922\",\n  },\n  {\n    name: \"Push Through\",\n    dur: 112,\n    tIn: 40,\n    presentation: () => pushThrough(),\n    tint: \"#17161f\",\n  },\n  {\n    name: \"Focus Pull\",\n    dur: 112,\n    tIn: 46,\n    presentation: () => focusPull(),\n    tint: \"#191720\",\n  },\n  {\n    name: \"Dither Dissolve\",\n    dur: 122,\n    tIn: 40,\n    presentation: () =>\n      ditherDissolve({ colorBack: OBSIDIAN, colorFront: LIME }),\n    tint: \"#161a15\",\n  },\n  {\n    name: \"Wave Wipe\",\n    dur: 144,\n    tIn: 56,\n    presentation: () => waveWipe({ colorBack: OBSIDIAN }),\n    // Scene B stays transparent so the wave field reads behind it as it\n    // rides up, instead of being covered by an opaque frame.\n    transparent: true,\n    tint: OBSIDIAN,\n  },\n  {\n    name: \"Grain Dissolve\",\n    dur: 164,\n    tIn: 66,\n    presentation: () => grainDissolve({ colorBack: OBSIDIAN }),\n    tint: \"#1a1820\",\n  },\n  {\n    name: \"Ripple Zoom\",\n    dur: 164,\n    tIn: 76,\n    presentation: () => rippleZoom({ colorBack: OBSIDIAN }),\n    // The dolly lands on the ripple field — the frame stays transparent\n    // so the field (and the root obsidian after it) reads as the ground.\n    transparent: true,\n    tint: OBSIDIAN,\n  },\n  {\n    name: \"Warp Dissolve\",\n    dur: 176,\n    tIn: 66,\n    presentation: () => warpDissolve(),\n    tint: \"#171622\",\n  },\n  {\n    name: \"Perlin Dissolve\",\n    dur: 200,\n    tIn: 90,\n    presentation: () =>\n      // Darker front keeps the mid-sweep flash in the video's tonal range.\n      perlinDissolve({ colorBack: OBSIDIAN, colorFront: \"#5b5773\" }),\n    // The perlin field ends solid colorFront — the veil dissolves it into\n    // the root obsidian before the presentation unmounts at frame 90.\n    transparent: true,\n    veil: { from: 58, to: 86 },\n    tint: OBSIDIAN,\n  },\n  {\n    name: \"Smoke Dissolve\",\n    dur: 210,\n    tIn: 90,\n    presentation: () => smokeDissolve({ colorBack: OBSIDIAN }),\n    // The ring expands past the frame by the end — the field resolves to\n    // colorBack on its own, so the transparent frame lands seamlessly.\n    transparent: true,\n    tint: OBSIDIAN,\n  },\n  {\n    name: \"Swirl Dissolve\",\n    dur: 180,\n    tIn: 100,\n    presentation: () =>\n      swirlDissolve({\n        colors: [\"#1f1d29\", \"#413d56\", LIME],\n        colorBack: OBSIDIAN,\n      }),\n    // The vortex winds back to twist=1 (collapses into colorBack) by the\n    // end, so the transparent frame lands seamlessly.\n    transparent: true,\n    tint: OBSIDIAN,\n  },\n];\n\nconst ShowcaseFrame: React.FC<{\n  name: string;\n  transparent?: boolean;\n  veil?: { from: number; to: number };\n  tint: string;\n}> = ({ name, transparent, veil, tint }) => {\n  const frame = useCurrentFrame();\n  const { durationInFrames } = useVideoConfig();\n\n  // The only internal motion: a slow drift so the hold never feels frozen.\n  const drift = interpolate(frame, [0, durationInFrames], [1, 1.045]);\n\n  const veilOpacity = veil\n    ? interpolate(frame, [veil.from, veil.to], [0, 1], clampOpts)\n    : 0;\n\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: transparent\n          ? undefined\n          : `radial-gradient(110% 110% at 50% 38%, ${tint} 0%, ${OBSIDIAN} 72%)`,\n      }}\n    >\n      {veil ? (\n        <AbsoluteFill style={{ background: OBSIDIAN, opacity: veilOpacity }} />\n      ) : null}\n      <span\n        style={{\n          fontFamily: SANS,\n          fontWeight: 400,\n          fontSize: 76,\n          color: INK,\n          whiteSpace: \"nowrap\",\n          transform: `scale(${drift})`,\n        }}\n      >\n        {name}\n      </span>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 1 — Hook. Two pain lines land solo; the world before the update.\n// ===========================================================================\nconst HookScene: React.FC = () => (\n  <AbsoluteFill style={{ background: OBSIDIAN }}>\n    <Sequence durationInFrames={48}>\n      <ScaleDownFade\n        text=\"Everyone animates their scenes\"\n        fontSize={54}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n    <Sequence from={48} durationInFrames={48}>\n      <ScaleDownFade\n        text=\"Nobody animates the cut\"\n        fontSize={54}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 2 — The announcement. The title glides in and holds alone.\n// ===========================================================================\nconst IntroScene: React.FC = () => (\n  <AbsoluteFill style={{ background: OBSIDIAN }}>\n    <Sequence durationInFrames={S_INTRO - BEATS[0].tIn}>\n      <ShortSlideRight\n        text=\"Introducing Remocn transitions\"\n        fontSize={62}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 14 — The value block. Three claims slide in and hold.\n// ===========================================================================\nconst ValueScene: React.FC = () => (\n  <AbsoluteFill style={{ background: OBSIDIAN }}>\n    <Sequence from={24} durationInFrames={S_VALUE - 24 - T_INSTALL}>\n      <LineByLineSlide\n        text={\n          \"Eleven new transitions\\nFrame-driven, deterministic renders\\nEach one lands in your repo\"\n        }\n        fontSize={48}\n        fontWeight={400}\n        color={INK}\n      />\n    </Sequence>\n  </AbsoluteFill>\n);\n\n// ===========================================================================\n// Scene 15 — Install. The headline assembles; the command resolves below.\n// ===========================================================================\nconst InstallScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const cmdOpacity = interpolate(frame, [54, 70], [0, 1], clampOpts);\n  const cmdY = interpolate(frame, [54, 74], [16, 0], {\n    ...clampOpts,\n    easing: Easing.out(Easing.cubic),\n  });\n  const cmdBlur = interpolate(frame, [54, 70], [6, 0], clampOpts);\n\n  return (\n    <AbsoluteFill style={{ background: OBSIDIAN }}>\n      <Sequence durationInFrames={S_INSTALL - T_OUTRO}>\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            transform: \"translateY(-40px)\",\n          }}\n        >\n          <BlurOutUp\n            text=\"Any cut, one command\"\n            fontSize={56}\n            fontWeight={400}\n            color={INK}\n            staggerDelay={3}\n          />\n        </div>\n      </Sequence>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: 418,\n          display: \"flex\",\n          justifyContent: \"center\",\n          opacity: cmdOpacity,\n          transform: `translateY(${cmdY}px)`,\n          filter: `blur(${cmdBlur}px)`,\n        }}\n      >\n        <span style={{ fontFamily: MONO, fontSize: 27, color: MUTED }}>\n          <span style={{ color: FAINT }}>$ </span>\n          npx shadcn add <span style={{ color: LIME }}>@remocn/whip-pan</span>\n        </span>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// Scene 16 — Outro. The push-through lands the camera on the lockup.\n// ===========================================================================\nconst OutroScene: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const markIn = spring({\n    frame: frame - 26,\n    fps,\n    config: { damping: 13, stiffness: 150, mass: 0.8 },\n  });\n  const markScale = interpolate(markIn, [0, 1], [0.5, 1]);\n\n  const wordIn = spring({\n    frame: frame - 36,\n    fps,\n    config: { damping: 14, stiffness: 130, mass: 0.9 },\n  });\n  const wordX = interpolate(wordIn, [0, 1], [-18, 0]);\n  const wordOpacity = interpolate(frame, [36, 50], [0, 1], clampOpts);\n\n  const creditOpacity = interpolate(frame, [96, 114], [0, 1], clampOpts);\n\n  return (\n    <AbsoluteFill\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: OBSIDIAN,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 26,\n          transform: \"translateY(-14px)\",\n        }}\n      >\n        <div style={{ opacity: markIn, transform: `scale(${markScale})` }}>\n          <RemocnMark size={76} />\n        </div>\n        <span\n          style={{\n            fontFamily: SANS,\n            fontWeight: 400,\n            fontSize: 84,\n            letterSpacing: \"-0.03em\",\n            color: INK,\n            opacity: wordOpacity,\n            transform: `translateX(${wordX}px)`,\n          }}\n        >\n          Remocn\n        </span>\n      </div>\n\n      <span\n        style={{\n          position: \"absolute\",\n          bottom: 42,\n          fontFamily: SANS,\n          fontWeight: 400,\n          fontSize: 19,\n          color: FAINT,\n          opacity: creditOpacity,\n        }}\n      >\n        Shader transitions built on Paper's shaders\n      </span>\n    </AbsoluteFill>\n  );\n};\n\n// ===========================================================================\n// The one plain cut left in the video — hook → announcement.\n// ===========================================================================\ntype EmptyProps = Record<string, never>;\n\nconst CrossfadePresentation: React.FC<\n  TransitionPresentationComponentProps<EmptyProps>\n> = ({ children, presentationProgress, presentationDirection }) => {\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: CrossfadePresentation,\n  props: {},\n});\n\n// ===========================================================================\n// Composition root.\n// ===========================================================================\nconst SHOWCASE_SCENES = BEATS.reduce((acc, b) => acc + b.dur, 0);\nconst SHOWCASE_TRANSITIONS = BEATS.reduce((acc, b) => acc + b.tIn, 0);\n\nexport const NEW_TRANSITIONS_DURATION =\n  S_HOOK +\n  S_INTRO +\n  SHOWCASE_SCENES +\n  S_VALUE +\n  S_INSTALL +\n  S_OUTRO -\n  (T_XF + SHOWCASE_TRANSITIONS + T_VALUE + T_INSTALL + T_OUTRO);\n\nexport const NewTransitionsDemo: React.FC = () => {\n  return (\n    <AbsoluteFill\n      style={\n        {\n          // remocn typography components read this CSS variable.\n          \"--font-geist-sans\": SANS_FAMILY,\n          background: OBSIDIAN,\n          fontFamily: SANS,\n        } as React.CSSProperties\n      }\n    >\n      <TransitionSeries>\n        {/* 1 — Hook */}\n        <TransitionSeries.Sequence durationInFrames={S_HOOK}>\n          <HookScene />\n        </TransitionSeries.Sequence>\n        <TransitionSeries.Transition\n          timing={linearTiming({ durationInFrames: T_XF })}\n          presentation={crossfade()}\n        />\n\n        {/* 2 — The announcement */}\n        <TransitionSeries.Sequence durationInFrames={S_INTRO}>\n          <IntroScene />\n        </TransitionSeries.Sequence>\n\n        {/* 3–13 — The showcase: each beat enters through the cut it names.\n            Flat array (no fragments) — TransitionSeries only accepts direct\n            Sequence/Transition children. */}\n        {BEATS.flatMap((beat) => [\n          <TransitionSeries.Transition\n            key={`${beat.name}-transition`}\n            timing={linearTiming({ durationInFrames: beat.tIn })}\n            presentation={beat.presentation()}\n          />,\n          <TransitionSeries.Sequence\n            key={`${beat.name}-scene`}\n            durationInFrames={beat.dur}\n          >\n            <ShowcaseFrame\n              name={beat.name}\n              transparent={beat.transparent}\n              veil={beat.veil}\n              tint={beat.tint}\n            />\n          </TransitionSeries.Sequence>,\n        ])}\n\n        {/* 14 — Value block, entered via a reused focus-pull */}\n        <TransitionSeries.Transition\n          timing={linearTiming({ durationInFrames: T_VALUE })}\n          presentation={focusPull()}\n        />\n        <TransitionSeries.Sequence durationInFrames={S_VALUE}>\n          <ValueScene />\n        </TransitionSeries.Sequence>\n\n        {/* 15 — Install, entered via a reused whip-pan (up) */}\n        <TransitionSeries.Transition\n          timing={linearTiming({ durationInFrames: T_INSTALL })}\n          presentation={whipPan({ direction: \"up\" })}\n        />\n        <TransitionSeries.Sequence durationInFrames={S_INSTALL}>\n          <InstallScene />\n        </TransitionSeries.Sequence>\n\n        {/* 16 — Outro, the camera lands on the brand */}\n        <TransitionSeries.Transition\n          timing={linearTiming({ durationInFrames: T_OUTRO })}\n          presentation={pushThrough()}\n        />\n        <TransitionSeries.Sequence durationInFrames={S_OUTRO}>\n          <OutroScene />\n        </TransitionSeries.Sequence>\n      </TransitionSeries>\n    </AbsoluteFill>\n  );\n};\n",
      "type": "registry:component",
      "target": "demos/new-transitions/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/new-transitions/prompt.md",
      "content": "<!-- TODO(draft): placeholder written by AI — replace with the real prompt used to generate this video -->\n\nMake an announcement video for the eleven new transition components we just shipped in remocn, and make it self-demonstrating — after the intro, every single scene change in the video should actually be one of the new transitions, labeled as it happens. Use the remocn brand look: Manrope regular, warm obsidian background, one lime accent. Open with a couple of pain lines about nobody animating the actual cut between scenes, then \"Introducing Remocn transitions\", then cascade through them one by one — whip-pan, push-through, focus-pull, a lime dither-dissolve, wave-wipe, grain-dissolve, ripple-zoom, warp-dissolve, perlin-dissolve, smoke-dissolve, and end the cascade on a lime swirl-dissolve as the big finale. Reuse a couple of the transitions to carry the value block and the install command scene, and close on the remocn wordmark at remocn.dev crediting Paper for the shaders.\n",
      "type": "registry:file",
      "target": "demos/new-transitions/prompt.md"
    }
  ],
  "docs": "Register the composition in your Remotion Root:\n\n  import { NewTransitionsDemo } from \"@/demos/new-transitions\";\n  <Composition id=\"new-transitions\" component={NewTransitionsDemo} durationInFrames={1556} fps={30} width={1280} height={720} />\n\nRequires Tailwind v4 wired into Remotion (@remotion/tailwind-v4).\nRender locally: npx remotion render new-transitions out/new-transitions.mp4 --gl=angle",
  "type": "registry:block"
}