All demos

Changelog — New Chat Components

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/chat-changelog

Render it locally

Renders the MP4 on your machine with the Remotion CLI.

$ pnpm dlx remotion render chat-changelog out/chat-changelog.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95

The prompt

Reconstructed draft

A reconstruction of the prompt this video was generated from.

Make a changelog video for the new chat components in remocn — chat-flow plus iMessage and Telegram-style flows. Show each one actually playing live inside a phone frame over an image backdrop, so people can see the bubbles and typing indicator animate in each skin. Make the point that all three are built from the same message-bubble and typing-indicator primitives, so it's really one message API with three different skins on top, not three separate implementations. Use remocn's chat components for the phone demos.

The code

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

import React, { type ReactNode } from "react";
import { AbsoluteFill, Easing, Sequence, interpolate, useCurrentFrame } from "remotion";
import { demoAsset } from "@/lib/demo-assets";
import {
  TransitionSeries,
  linearTiming,
  type TransitionPresentation,
} from "@remotion/transitions";
import { fade, type FadeProps } from "@remotion/transitions/fade";
import { loadFont } from "@remotion/google-fonts/Manrope";
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";

import { RemocnUIProvider } from "@/lib/remocn-ui";
import { Backdrop } from "@/components/remocn/backdrop";
import { Typewriter } from "@/components/remocn/typewriter";

// New chat primitives — the subject of this changelog.
import {
  ChatFlow,
  chatFlowDuration,
  type ChatMessage,
} from "@/components/remocn/chat-flow";
import {
  ImessageChatFlow,
  imessageChatFlowDuration,
  type ImessageMessage,
} from "@/components/remocn/imessage-chat-flow";
import {
  TelegramChatFlow,
  telegramChatFlowDuration,
  type TelegramMessage,
} from "@/components/remocn/telegram-chat-flow";

// Manrope, bound to the CSS variable every remocn component reads.
const { fontFamily } = loadFont("normal", {
  subsets: ["latin"],
  weights: ["400", "500", "600", "700", "800"],
});
const FONT_STACK = `${fontFamily}, sans-serif`;

const { fontFamily: monoFamily } = loadMono("normal", {
  subsets: ["latin"],
  weights: ["400", "500"],
});
const MONO_STACK = `${monoFamily}, ui-monospace, SFMono-Regular, Menlo, monospace`;

const INK = "#fafafa";
const MUTED = "rgba(250,250,250,0.62)";
const FAINT = "rgba(250,250,250,0.42)";

// ---------------------------------------------------------------------------
// Platform registry — one source of truth for the montage and the counter.
// Each entry is a self-contained chat-flow scene; the component runs its own
// internal timeline, so the scene is static and only the chat "plays".
// ---------------------------------------------------------------------------
// Shared persona across all three skins. The avatar (unavatar.io/x/shadcn) is
// vendored into public/ and loaded via staticFile so it renders deterministically
// and offline — remote URLs aren't reliably fetched during a frame capture.
const CONTACT = { name: "shadcn", avatar: demoAsset("shadcn-avatar.png") };

const CF_MESSAGES: ChatMessage[] = [
  { from: "me", text: "remocn ships chat components now?" },
  { from: "them", text: "Copy-paste. You own the code.", reaction: "🔥" },
];

const IM_MESSAGES: ImessageMessage[] = [
  { from: "me", text: "iMessage style too?" },
  { from: "them", text: "Blue bubbles + tapbacks.", reaction: "❤️" },
];

const TG_MESSAGES: TelegramMessage[] = [
  { from: "me", text: "And Telegram?", time: "9:41" },
  { from: "them", text: "Same API, telegram skin.", reaction: "👍", time: "9:41" },
];

type Platform = {
  /** registry slug — also the counter label and the install target. */
  name: string;
  label: string;
  blurb: string;
  /** time-multiplier passed to the chat flow (>1 plays faster). */
  speed: number;
  /** phone-screen background behind the bubbles. */
  screen: string;
  accent: string;
  render: (speed: number) => ReactNode;
  /** the flow's natural length, in composition frames. */
  duration: number;
};

const PLATFORMS: Platform[] = [
  {
    name: "chat-flow",
    label: "Chat Flow",
    blurb: "shadcn-style bubbles, a live composer, and reactions — frame-driven.",
    speed: 1.35,
    screen: "#ffffff",
    accent: "#7c5cff",
    render: (s) => (
      <ChatFlow contact={CONTACT} messages={CF_MESSAGES} speed={s} />
    ),
    duration: chatFlowDuration(CF_MESSAGES, 1.35),
  },
  {
    name: "imessage-chat-flow",
    label: "iMessage",
    blurb: "The blue-bubble look — gray inbound, tapback reactions, the works.",
    speed: 1.25,
    screen: "#ffffff",
    accent: "#0a7cff",
    render: (s) => (
      <ImessageChatFlow contact={CONTACT} messages={IM_MESSAGES} speed={s} />
    ),
    duration: imessageChatFlowDuration(IM_MESSAGES, 1.25),
  },
  {
    name: "telegram-chat-flow",
    label: "Telegram",
    blurb: "Same message API, a Telegram skin — timestamps, ticks and accent blue.",
    speed: 1.2,
    screen: "linear-gradient(180deg, #d6dfea 0%, #c6d2e0 100%)",
    accent: "#3390ec",
    render: (s) => (
      <TelegramChatFlow contact={CONTACT} messages={TG_MESSAGES} speed={s} />
    ),
    duration: telegramChatFlowDuration(TG_MESSAGES, 1.2),
  },
];

// ---------------------------------------------------------------------------
// Transitions — in-place opacity cross-fades only. No camera movement; each
// scene resolves on the spot and dissolves on the spot.
// ---------------------------------------------------------------------------
type Trans = { dur: number; presentation: () => TransitionPresentation<FadeProps> };
const DISSOLVE: Trans = { dur: 12, presentation: () => fade() };

// One reveal primitive: rise a touch and un-blur in place.
const Reveal: React.FC<{ children: ReactNode; delay?: number; y?: number }> = ({
  children,
  delay = 0,
  y = 12,
}) => {
  const frame = useCurrentFrame();
  const p = interpolate(frame, [delay, delay + 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.bezier(0.22, 1, 0.36, 1),
  });
  return (
    <div
      style={{
        opacity: p,
        transform: `translateY(${(1 - p) * y}px)`,
        filter: p < 1 ? `blur(${(1 - p) * 8}px)` : undefined,
      }}
    >
      {children}
    </div>
  );
};

// ---------------------------------------------------------------------------
// Speech-bubble mark — each path is normalised to pathLength 1, then "drawn"
// by sweeping its dash offset from hidden (1) to shown (0). A bookend brand
// glyph for the intro and outro.
// ---------------------------------------------------------------------------
const ChatMark: React.FC<{ size?: number; color?: string }> = ({
  size = 34,
  color = INK,
}) => {
  const frame = useCurrentFrame();
  const appear = interpolate(frame, [0, 8], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const draw = interpolate(frame, [2, 32], [1, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.inOut(Easing.cubic),
  });
  const dots = interpolate(frame, [22, 34], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke={color}
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      style={{ opacity: appear }}
    >
      <path
        pathLength={1}
        strokeDasharray={1}
        strokeDashoffset={draw}
        d="M7.5 18.5L4 21V6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v9a2.5 2.5 0 0 1-2.5 2.5z"
      />
      <g opacity={dots} fill={color} stroke="none">
        <circle cx={8.5} cy={11} r={1.05} />
        <circle cx={12} cy={11} r={1.05} />
        <circle cx={15.5} cy={11} r={1.05} />
      </g>
    </svg>
  );
};

// Small lock-up: the mark draws itself in beside the wordmark.
const Wordmark: React.FC<{ markSize?: number; fontSize?: number }> = ({
  markSize = 26,
  fontSize = 21,
}) => (
  <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
    <ChatMark size={markSize} />
    <span
      style={{
        fontFamily: FONT_STACK,
        fontSize,
        fontWeight: 600,
        color: "rgba(250,250,250,0.78)",
      }}
    >
      remocn
    </span>
  </div>
);

// ---------------------------------------------------------------------------
// Phone — a device frame whose screen holds a running chat flow.
// Sized to a real handset aspect: iPhone 16 Pro is 402×874pt (≈2.17:1) and a
// Pixel ~412×915 (≈2.22:1). 302×656 keeps the narrow, tall proportions of both
// while fitting inside the 720px canvas.
// ---------------------------------------------------------------------------
const PHONE_W = 302;
const PHONE_H = 656;
const STATUS_H = 40; // safe-area / status-bar inset above the app header.

// iOS-style status-bar glyphs — dark, on the white inset that matches every
// app header (chat-flow/iMessage/Telegram all render a white-ish top bar).
const SignalIcon: React.FC = () => (
  <svg width={17} height={11} viewBox="0 0 17 11" fill="#000">
    <rect x={0} y={7} width={3} height={4} rx={1} />
    <rect x={4.5} y={5} width={3} height={6} rx={1} />
    <rect x={9} y={2.5} width={3} height={8.5} rx={1} />
    <rect x={13.5} y={0} width={3} height={11} rx={1} />
  </svg>
);
const WifiIcon: React.FC = () => (
  <svg width={16} height={12} viewBox="0 0 16 12" fill="none">
    <path d="M8 10.2a1.4 1.4 0 1 0 0-2.8 1.4 1.4 0 0 0 0 2.8Z" fill="#000" />
    <path
      d="M3.2 5.2A7 7 0 0 1 12.8 5.2M5.2 7.2a4.1 4.1 0 0 1 5.6 0"
      stroke="#000"
      strokeWidth={1.5}
      strokeLinecap="round"
    />
  </svg>
);
const BatteryIcon: React.FC = () => (
  <svg width={26} height={12} viewBox="0 0 26 12" fill="none">
    <rect
      x={0.5}
      y={0.5}
      width={22}
      height={11}
      rx={3}
      stroke="#000"
      strokeOpacity={0.4}
    />
    <rect x={2} y={2} width={17} height={8} rx={1.6} fill="#000" />
    <rect x={24} y={4} width={1.5} height={4} rx={0.75} fill="#000" fillOpacity={0.4} />
  </svg>
);

const StatusBar: React.FC = () => (
  <div
    style={{
      position: "relative",
      flexShrink: 0,
      height: STATUS_H,
      background: "#ffffff",
      display: "flex",
      alignItems: "center",
      justifyContent: "space-between",
      padding: "0 18px 0 22px",
      fontFamily: FONT_STACK,
      zIndex: 3,
    }}
  >
    <span
      style={{
        fontSize: 14,
        fontWeight: 700,
        color: "#000",
        letterSpacing: "0.01em",
        fontVariantNumeric: "tabular-nums",
      }}
    >
      9:41
    </span>
    {/* Dynamic Island */}
    <div
      style={{
        position: "absolute",
        top: 8,
        left: "50%",
        transform: "translateX(-50%)",
        width: 82,
        height: 24,
        borderRadius: 999,
        background: "#000",
      }}
    />
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <SignalIcon />
      <WifiIcon />
      <BatteryIcon />
    </div>
  </div>
);

const Phone: React.FC<{ screen: string; children: ReactNode }> = ({
  screen,
  children,
}) => (
  <div
    style={{
      width: PHONE_W,
      height: PHONE_H,
      borderRadius: 48,
      padding: 11,
      background: "#0a0a0b",
      border: "1px solid rgba(255,255,255,0.1)",
      boxShadow:
        "0 40px 90px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.6) inset",
    }}
  >
    <div
      style={{
        position: "relative",
        display: "flex",
        flexDirection: "column",
        width: "100%",
        height: "100%",
        borderRadius: 38,
        overflow: "hidden",
        background: screen,
      }}
    >
      <StatusBar />
      <div style={{ position: "relative", flex: 1, minHeight: 0 }}>{children}</div>
    </div>
  </div>
);

// ---------------------------------------------------------------------------
// Montage scene — left feature copy, right the live phone.
// ---------------------------------------------------------------------------
const PlatformScene: React.FC<{ platform: Platform; index: number }> = ({
  platform,
  index,
}) => {
  const frame = useCurrentFrame();
  const enter = interpolate(frame, [0, 24], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.bezier(0.22, 1, 0.36, 1),
  });

  return (
    <AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 76 }}>
        {/* Left: the changelog entry. */}
        <div style={{ width: 420, fontFamily: FONT_STACK }}>
          <Reveal delay={2}>
            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: 9,
                marginBottom: 22,
              }}
            >
              <span
                style={{
                  width: 8,
                  height: 8,
                  borderRadius: "50%",
                  background: platform.accent,
                  boxShadow: `0 0 14px ${platform.accent}`,
                }}
              />
              <span style={{ fontSize: 15, fontWeight: 600, color: MUTED }}>
                New · {String(index).padStart(2, "0")} / {String(PLATFORMS.length).padStart(2, "0")}
              </span>
            </div>

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