All demos

Changelog — remocn/ui arrives

Install this video

Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.

$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/remocn-ui

Render it locally

Renders the MP4 on your machine with the Remotion CLI.

$ pnpm dlx remotion render remocn-ui out/remocn-ui.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95 --gl=angle

The prompt

The prompt this video was generated from.

# Prompt — remocn/ui arrives (changelog cut) Переделать видео remocn-ui в новом стиле (introducing-videorc / introducing-shadcn / introducing-remocn). Переделать сценарий по product-launch story design (STORYBOARD.md + SCRIPT.md). - Проанализировать старое видео (signup-flow — единственный чистый remocn-ui демо), remocn и remocn/changelog (запись 2026-06-12 «remocn/ui arrives»). - Нельзя: letter-spacing, uppercase, badges, pulsing, installation pills. - Нельзя swirl и ripple переходы. Придумать НОВЫЙ переход для registry — получился **skeleton-swap**: кат как loading state — уходящая сцена схлопывается в skeleton-плейсхолдеры, по ним проходит один shimmer, и блоки «гидрируются» во входящую сцену. - Показать remocn-ui в действии (steps API + command menu + breadth montage живыми примитивами). - Видео для changelog-страницы remocn.dev. - Outro — новая сцена из introducing-remocn с новым логотипом (R-марка), унаследована без изменений. - Текущий компонент (signup-flow) не трогать — это новый демо `remocn-ui`.

The code

The exact source the AI wrote — the same files the install command puts in your project.

import React from "react";
import {
  AbsoluteFill,
  Easing,
  Sequence,
  Series,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import {
  TransitionSeries,
  linearTiming,
  type TransitionPresentation,
  type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { loadFont as loadSans } from "@remotion/google-fonts/Manrope";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { loadFont as loadMono } from "@remotion/google-fonts/GeistMono";

import { ScaleDownFade } from "@/components/remocn/scale-down-fade";
import { ShortSlideRight } from "@/components/remocn/short-slide-right";
import { LineByLineSlide } from "@/components/remocn/line-by-line-slide";
import { ShaderSimplexNoise } from "@/components/remocn/shader-simplex-noise";
import { ShaderSmokeRing } from "@/components/remocn/shader-smoke-ring";
import { skeletonSwap } from "@/components/remocn/skeleton-swap";

import { Button } from "@/components/remocn/button";
import { useButtonTransition } from "@/components/remocn/use-button-transition";
import { Checkbox } from "@/components/remocn/checkbox";
import { useCheckboxTransition } from "@/components/remocn/use-checkbox-transition";
import { Switch } from "@/components/remocn/switch";
import { useSwitchTransition } from "@/components/remocn/use-switch-transition";
import { Tabs } from "@/components/remocn/tabs";
import { useTabsTransition } from "@/components/remocn/use-tabs-transition";
import { Select } from "@/components/remocn/select";
import { useSelectTransition } from "@/components/remocn/use-select-transition";
import { Toast } from "@/components/remocn/toast";
import { useToastTransition } from "@/components/remocn/use-toast-transition";
import {
  CommandMenu,
  type CommandMenuEntry,
} from "@/components/remocn/command-menu";
import { useCommandMenuTransition } from "@/components/remocn/use-command-menu-transition";

// Manrope carries every spoken headline; Inter is bound to the UI primitives'
// font variable inside the product scenes only, so the components render in
// the product's own register; Geist Mono carries the steps array, the frame
// counter, and the shell command.
const { fontFamily: SANS_FAMILY } = loadSans("normal", {
  subsets: ["latin"],
  weights: ["400", "500", "600"],
});
const { fontFamily: INTER_FAMILY } = loadInter("normal", {
  subsets: ["latin"],
  weights: ["400", "500", "600"],
});
const { fontFamily: MONO_FAMILY } = loadMono("normal", {
  subsets: ["latin"],
  weights: ["400", "500"],
});

const SANS =
  "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif";
const MONO = `${MONO_FAMILY}, ui-monospace, SFMono-Regular, monospace`;

// The shipped remocn.dev brand: warm obsidian + one lime accent.
const OBSIDIAN = "#141318";
const INK = "#f2f2f2";
const MUTED = "rgba(242,242,242,0.62)";
const FAINT = "rgba(242,242,242,0.4)";
const LIME = "#C3E88D";

const clampOpts = {
  extrapolateLeft: "clamp" as const,
  extrapolateRight: "clamp" as const,
};

// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps). Transitions overlap.
// ---------------------------------------------------------------------------
const S_HOOK = 142; //     three swapping needs + the claim
const S_PAIN = 144; //     two clock pain lines (tail feeds the skeleton exit)
const S_MEET = 100; //     "Meet remocn/ui" (self-exits; the late skeleton
//                         enter means the name only lands near the cover's end)
const S_TAGLINE = 76; //   "The same shapes, driven by the timeline"
const S_STEPS = 225; //    the steps array driving a real button
const S_CMD = 222; //      the command menu run + toast payoff
const S_BREADTH = 208; //  five primitives hard-cut
const S_VALUE = 104; //    three value lines
const S_INSTALL = 120; //  typed command
const S_OUTRO = 150; //    smoke ring → R mark → wordmark

// The skeleton-swap phases are sequential (exit → shimmer hold → enter near
// the end), so the cover runs longer than a dissolve would.
const T_X = 14; //         crossfade
const T_SK = 56; //        skeleton-swap cover (the new transition)
const T_BLUR = 16; //      blur crossfade

// ===========================================================================
// The remocn mark — the R letterform from remocn.dev/logo-mark.svg, inherited
// from the introducing-remocn outro so the changelog series closes in one
// voice: `draw` runs the outline, `fill` lets the solid letter catch up.
// ===========================================================================
const MARK_VIEWBOX = "0 0 124.06 134.26";
const MARK_RATIO = 124.06 / 134.26;
const MARK_PATH =
  "M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z";

const RemocnMark: React.FC<{ size: number; draw?: number; fill?: number }> = ({
  size,
  draw = 1,
  fill = 1,
}) => (
  <svg
    viewBox={MARK_VIEWBOX}
    width={size * MARK_RATIO}
    height={size}
    style={{ display: "block", color: INK, overflow: "visible" }}
  >
    <path
      d={MARK_PATH}
      pathLength={1}
      fill="currentColor"
      fillOpacity={fill}
      stroke="currentColor"
      strokeWidth={2}
      strokeLinejoin="round"
      strokeDasharray={1}
      strokeDashoffset={1 - draw}
    />
  </svg>
);

// Readability scrim over the backdrop shader.
const Scrim: React.FC<{ strength?: number }> = ({ strength = 1 }) => (
  <AbsoluteFill
    style={{
      background: `radial-gradient(120% 120% at 50% 42%, rgba(20,19,24,${
        0.3 * strength
      }) 0%, rgba(20,19,24,${0.78 * strength}) 100%)`,
    }}
  />
);

// A small light stage the primitives perform on — the docs-preview surface.
// The primitives are absolute-fill centered, so the stage is their frame.
const Stage: React.FC<{
  width: number;
  height: number;
  children: React.ReactNode;
}> = ({ width, height, children }) => (
  <div
    style={
      {
        position: "relative",
        width,
        height,
        background: "#fbfbfa",
        border: "1px solid rgba(0,0,0,0.06)",
        borderRadius: 16,
        boxShadow:
          "0 18px 40px -18px rgba(0,0,0,0.5), 0 2px 8px -3px rgba(0,0,0,0.2)",
        "--font-geist-sans": INTER_FAMILY,
      } as React.CSSProperties
    }
  >
    {children}
  </div>
);

// ===========================================================================
// Scene 1 — Hook. Three un-fakeable shots swap in one slot, then the claim.
// ===========================================================================
const HOOK_ITEMS = ["A dialog opening", "A menu filtering", "A form filling itself"];
const HOOK_PER = 26;

const HookScene: React.FC = () => {
  const frame = useCurrentFrame();

  return (
    <AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
      {HOOK_ITEMS.map((item, i) => {
        const start = i * HOOK_PER;
        const end = start + HOOK_PER;
        if (frame < start - 2 || frame > end + 2) return null;
        const enter = interpolate(frame, [start, start + 6], [0, 1], {
          ...clampOpts,
          easing: Easing.out(Easing.cubic),
        });
        const leave = interpolate(frame, [end - 6, end], [0, 1], {
          ...clampOpts,
          easing: Easing.in(Easing.cubic),
        });
        const opacity = enter * (1 - leave);
        if (opacity <= 0.002) return null;
        return (
          <span
            key={item}
            style={{
              position: "absolute",
              fontFamily: SANS,
              fontWeight: 400,
              fontSize: 54,
              color: INK,
              opacity,
              transform: `translateY(${(1 - enter) * 12 - leave * 10}px)`,
              filter: `blur(${(1 - enter) * 5}px)`,
            }}
          >
            {item}
          </span>
        );
      })}
      <Sequence from={82}>
        <ScaleDownFade
          text="Every demo has to show the product"
          fontSize={48}
          fontWeight={400}
          color={INK}
        />
      </Sequence>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 2 — Pain. The changelog entry's own argument, split at its hinge.
// ===========================================================================
const PainScene: React.FC = () => (
  <AbsoluteFill>
    <Sequence durationInFrames={62}>
      <ScaleDownFade
        text="Real components run on the clock"
        fontSize={52}
        fontWeight={400}
        color={INK}
      />
    </Sequence>
    <Sequence from={62} durationInFrames={82}>
      <ScaleDownFade
        text="A video only has a frame number"
        fontSize={52}
        fontWeight={400}
        color={INK}
      />
    </Sequence>
  </AbsoluteFill>
);

// ===========================================================================
// Scene 3 — Meet remocn/ui. Static on entry (the skeleton-swap cover hydrates
// it in), then it plays its own exit so the tagline gets an empty canvas.
// ===========================================================================
const MeetScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();

  const exitP = interpolate(
    frame,
    [durationInFrames - 18, durationInFrames - 2],
    [0, 1],
    { ...clampOpts, easing: Easing.in(Easing.cubic) },
  );

  return (
    <AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
      <span
        style={{
          fontFamily: SANS,
          fontWeight: 400,
          fontSize: 72,
          color: INK,
          opacity: 1 - exitP,
          transform: `translateY(${exitP * -10}px) scale(${1 - exitP * 0.05})`,
          filter: `blur(${exitP * 6}px)`,
        }}
      >
        Meet remocn/ui
      </span>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 3b — Tagline.
// ===========================================================================
const TaglineScene: React.FC = () => (
  <AbsoluteFill>
    <ShortSlideRight
      text="The same shapes, driven by the timeline"
      fontSize={44}
      fontWeight={400}
      color={INK}
    />
  </AbsoluteFill>
);

// ===========================================================================
// Scene 4 — The steps API. A mono steps array reveals line by line on the
// left; a REAL Button on the right walks the exact states as the playhead
// passes each `at`. A mono frame counter ticks beneath the code — the frame
// number the pain beat promised, now doing the driving.
// ===========================================================================
const BTN_T0 = 66; // the button's timeline zero (frame counter zero)

type StepLine = { text: string; at?: number; state?: string };

const STEP_LINES: StepLine[] = [
  { text: "useButtonTransition([" },
  { text: '  { at: 20, state: "hover" },', at: 20, state: "hover" },
  { text: '  { at: 44, state: "press" },', at: 44, state: "press" },
  { text: '  { at: 56, state: "loading" },', at: 56, state: "loading" },
  { text: '  { at: 120, state: "success" },', at: 120, state: "success" },
  { text: "])" },
];

const StepsScene: React.FC = () => {
  const frame = useCurrentFrame();

  const buttonStyle = useButtonTransition([
    { at: BTN_T0 + 20, state: "hover" },
    { at: BTN_T0 + 44, state: "press", duration: 6 },
    { at: BTN_T0 + 56, state: "loading", duration: 6 },
    { at: BTN_T0 + 120, state: "success", duration: 14 },
  ]);

  // Which step the playhead currently sits in — that line reads in lime.
  const playhead = frame - BTN_T0;
  let activeIdx = -1;
  for (let i = 0; i < STEP_LINES.length; i++) {
    const at = STEP_LINES[i].at;
    if (at !== undefined && playhead >= at) activeIdx = i;
  }

  // The arrow gutter: appears with the first step and glides one row down
  // each time the playhead crosses the next step's `at`.
  const stepRows = STEP_LINES.map((l, i) => ({ ...l, row: i })).filter(
    (l): l is { text: string; at: number; state: string; row: number } =>
      l.at !== undefined,
  );
  let arrowRow = stepRows[0].row;
  for (let i = 1; i < stepRows.length; i++) {
    arrowRow += interpolate(
      playhead,
      [stepRows[i].at, stepRows[i].at + 5],
      [0, stepRows[i].row - stepRows[i - 1].row],
      { ...clampOpts, easing: Easing.inOut(Easing.cubic) },
    );
  }
  const arrowOpacity = interpolate(
    playhead,
    [stepRows[0].at, stepRows[0].at + 8],
    [0, 1],
    clampOpts,
  );

  const counter = Math.max(0, Math.min(999, playhead));
  const counterOpacity = interpolate(frame, [40, 54], [0, 1], clampOpts);

  return (
    <AbsoluteFill
      style={{
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "center",
        gap: 84,
      }}
    >
      <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
        <div style={{ position: "relative", display: "flex", flexDirection: "column" }}>
          <span
            style={{
              position: "absolute",
              left: -26,
              top: arrowRow * 38,
              fontFamily: MONO,
              fontSize: 30,
              lineHeight: "38px",
              color: LIME,
              opacity: arrowOpacity,
            }}
          >

          </span>
          {STEP_LINES.map((line, i) => {
            const start = 10 + i * 7;
            const opacity = interpolate(frame, [start, start + 8], [0, 1], clampOpts);
            const x = interpolate(frame, [start, start + 8], [10, 0], {
              ...clampOpts,
              easing: Easing.out(Easing.cubic),
            });
            const active = i === activeIdx;
            return (
              <span
                key={line.text}
                style={{
                  fontFamily: MONO,
                  fontSize: 23,

Showing the first 400 of 1006 lines. View the full file on GitHub.