All demos

Changelog — llms.txt for AI agents

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/llms-txt

Render it locally

Renders the MP4 on your machine with the Remotion CLI.

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

The prompt

Reconstructed draft

A reconstruction of the prompt this video was generated from.

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,
  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 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 { asciiDissolve } from "@/components/remocn/ascii-dissolve";

// Manrope carries every spoken headline; Geist Mono carries the URLs, the
// markup noise, the index/corpus text, and the .md suffix.
const { fontFamily: SANS_FAMILY } = loadSans("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 = 66; //      "Your agent reads the docs too"
const S_SCRAPE = 196; //   the sentence buried in markup + the verdict
const S_TURN = 134; //     "Now the docs are plain text" (self-exits)
const S_ADDR = 72; //      "At the addresses agents already look for"
const S_ROUTES = 344; //   the three routes on one mono surface
const S_VALUE = 96; //     three value lines
const S_OUTRO = 150; //    smoke ring → R mark → wordmark

const T_X = 14; //         crossfade
const T_ASCII_TEXT = 84; // ascii-dissolve in text mode: the verdict decays
//                          into the field, the turn headline condenses out
//                          and holds READABLE before solidifying
const T_ASCII = 56; //     ascii-dissolve cover, generic mode
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%)`,
    }}
  />
);

// ===========================================================================
// Scene 1 — Hook. One line lands solo: the reader the viewer didn't picture.
// ===========================================================================
const HookScene: React.FC = () => (
  <AbsoluteFill>
    <ScaleDownFade
      text="Your agent reads the docs too"
      fontSize={54}
      fontWeight={400}
      color={INK}
    />
  </AbsoluteFill>
);

// ===========================================================================
// Scene 2 — The scrape. A clean docs sentence gets buried by mono markup
// crowding in from all four edges; then everything dims and the entry's own
// verdict lands over the held noise.
// ===========================================================================
type TagFragment = {
  text: string;
  x: number; //   offset from center, px
  y: number;
  size: number;
  dx: number; //  slide-in direction (unit-ish vector, px of travel)
  dy: number;
  o: number; //   settled opacity
};

const TAG_FRAGMENTS: TagFragment[] = [
  { text: '<div class="prose prose-invert max-w-none">', x: -320, y: -236, size: 19, dx: -60, dy: -20, o: 0.6 },
  { text: '<nav aria-label="On this page">', x: 330, y: -262, size: 17, dx: 60, dy: -24, o: 0.5 },
  { text: '<span data-state="open">', x: -430, y: -120, size: 18, dx: -70, dy: 0, o: 0.55 },
  { text: '<script type="application/ld+json">', x: 350, y: -140, size: 16, dx: 70, dy: 0, o: 0.45 },
  { text: '<svg viewBox="0 0 24 24" fill="none">', x: -350, y: 160, size: 17, dx: -60, dy: 20, o: 0.5 },
  { text: "data-radix-collection-item", x: 380, y: 130, size: 18, dx: 66, dy: 16, o: 0.55 },
  { text: 'class="flex items-center gap-2"', x: -260, y: 262, size: 18, dx: -40, dy: 50, o: 0.55 },
  { text: '<button data-slot="trigger" tabindex="-1">', x: 250, y: 236, size: 16, dx: 40, dy: 46, o: 0.45 },
  { text: "</div></div></div></div>", x: 30, y: 300, size: 20, dx: 0, dy: 60, o: 0.6 },
  { text: 'style="--tw-prose-body: #a1a1aa"', x: -60, y: -300, size: 17, dx: 0, dy: -56, o: 0.5 },
  { text: 'aria-hidden="true"', x: -470, y: 40, size: 16, dx: -66, dy: 8, o: 0.4 },
  { text: '<a class="text-sm underline-offset-4">', x: 430, y: -20, size: 16, dx: 66, dy: -6, o: 0.42 },
  // The last wave lands ON the sentence — the burial.
  { text: '<li data-sidebar="menu-item">', x: -150, y: -52, size: 19, dx: -30, dy: -30, o: 0.66 },
  { text: '<meta property="og:type" content="article">', x: 130, y: 44, size: 18, dx: 30, dy: 30, o: 0.66 },
  { text: '<span class="sr-only">Copy</span>', x: 60, y: -110, size: 18, dx: 24, dy: -30, o: 0.62 },
  { text: '<code class="relative rounded px-1">', x: -110, y: 96, size: 18, dx: -26, dy: 32, o: 0.64 },
];

const TAGS_START = 26;
const TAGS_PER = 4;
const DIM_AT = 96;

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

  const sentenceIn = interpolate(frame, [0, 14], [0, 1], {
    ...clampOpts,
    easing: Easing.out(Easing.cubic),
  });

  // Once the soup has buried the sentence, the whole pile dims and holds —
  // the verdict lands over it.
  const dim = interpolate(frame, [DIM_AT, DIM_AT + 16], [1, 0.3], {
    ...clampOpts,
    easing: Easing.inOut(Easing.cubic),
  });
  const dimBlur = interpolate(frame, [DIM_AT, DIM_AT + 16], [0, 2.5], clampOpts);

  return (
    <AbsoluteFill>
      <AbsoluteFill
        style={{
          alignItems: "center",
          justifyContent: "center",
          opacity: dim,
          filter: dimBlur > 0.01 ? `blur(${dimBlur}px)` : undefined,
        }}
      >
        <span
          style={{
            fontFamily: SANS,
            fontWeight: 400,
            fontSize: 30,
            lineHeight: 1.5,
            color: INK,
            maxWidth: 680,
            textAlign: "center",
            opacity: sentenceIn,
            transform: `translateY(${(1 - sentenceIn) * 10}px)`,
          }}
        >
          Wrap two scenes in a TransitionSeries and pass a presentation.
        </span>
        {TAG_FRAGMENTS.map((tag, i) => {
          const start = TAGS_START + i * TAGS_PER;
          const enter = interpolate(frame, [start, start + 12], [0, 1], {
            ...clampOpts,
            easing: Easing.out(Easing.cubic),
          });
          if (enter <= 0.002) return null;
          return (
            <span
              key={tag.text}
              style={{
                position: "absolute",
                left: 640 + tag.x - 200,
                top: 360 + tag.y - 14,
                width: 400,
                textAlign: "center",
                fontFamily: MONO,
                fontSize: tag.size,
                whiteSpace: "nowrap",
                color: MUTED,
                opacity: enter * tag.o,
                transform: `translate(${(1 - enter) * tag.dx}px, ${
                  (1 - enter) * tag.dy
                }px)`,
              }}
            >
              {tag.text}
            </span>
          );
        })}
      </AbsoluteFill>

      <Sequence from={96}>
        <ScaleDownFade
          text="A bad way to learn an API"
          fontSize={58}
          fontWeight={400}
          color={INK}
        />
      </Sequence>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 3 — The turn. Static on entry (the ascii-dissolve cover resolves it
// in), then it plays its own exit so the next beat gets an empty canvas.
// ===========================================================================
const TurnScene: 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)`,
        }}
      >
        Now the docs are plain text
      </span>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 3b — The addresses. The promise in the entry's own words.
// ===========================================================================
const AddressesScene: React.FC = () => (
  <AbsoluteFill>
    <ShortSlideRight
      text="At the addresses agents already look for"
      fontSize={42}
      fontWeight={400}
      color={INK}
    />
  </AbsoluteFill>
);

// ===========================================================================
// Scene 4 — The three routes, one mono surface. The address line is the
// hero: it types /llms.txt (the index pours in beneath), the filename flips
// in place to /llms-full.txt (the whole corpus streams past), then a real
// docs URL takes the slot and .md types itself — the rendered card flips to
// raw Markdown on the last keystroke.
// ===========================================================================
const RT_URL_START = 8; //     "remocn.dev/llms.txt" types
const RT_INDEX_START = 40; //  index lines pour in
const RT_FLIP = 116; //        llms.txt → llms-full.txt flips in place
const RT_STREAM_START = 128; // corpus stream begins
const RT_STREAM_END = 204; //  stream fades
const RT_C_START = 212; //     phase C — the address line hands over
const RT_URL2_START = 218; //  docs URL types
const RT_MD_KEYS = [246, 253, 260] as const; // ".md" keystrokes
const RT_CARD_FLIP = 264; //   rendered → raw flip

const URL_PREFIX = "remocn.dev/";
const FILE_A = "llms.txt";
const FILE_B = "llms-full.txt";

type IndexLine =
  | { kind: "h"; text: string }
  | { kind: "q"; text: string }
  | { kind: "blank" }
  | { kind: "item"; title: string; path: string; desc: string };

const INDEX_LINES: IndexLine[] = [
  { kind: "h", text: "# remocn" },
  { kind: "q", text: "> Remotion components as a shadcn registry" },
  { kind: "blank" },
  { kind: "item", title: "Installation", path: "/docs/installation.md", desc: "registry setup for a Remotion project" },
  { kind: "item", title: "Typography", path: "/docs/typography.md", desc: "31 kinetic text animations" },
  { kind: "item", title: "Transitions", path: "/docs/transitions.md", desc: "scene-to-scene presentations" },
  { kind: "item", title: "Shaders", path: "/docs/shaders.md", desc: "18 frame-driven WebGL backdrops" },
  { kind: "item", title: "UI", path: "/docs/ui.md", desc: "timeline-driven primitives and flows" },
  { kind: "item", title: "Icons", path: "/docs/icons.md", desc: "100 animated icons, draw and action" },
];

// The corpus stream — plausible slices of the full docs as one document.
const CORPUS_LINES: string[] = [
  "# remocn",
  "Production-ready Remotion components, installed as code you own.",
  "",
  "## Installation",
  "Add the registry to components.json, then pull components by name.",
  "```bash",
  "npx shadcn add @remocn/typewriter",
  "```",
  "",
  "## typewriter",
  "Types text character by character behind a blinking block caret.",
  "| prop | type | default |",
  "| --- | --- | --- |",
  "| text | string | — |",
  "| speed | number | 1 |",
  "",
  "## rolling-number",
  "Odometer digits that wheel into place, tabular by default.",
  "",
  "## grain-dissolve",
  "A film-grain burst that carries one scene into the next.",
  "```tsx",
  "<TransitionSeries.Transition",
  "  presentation={grainDissolve()}",
  "  timing={linearTiming({ durationInFrames: 18 })}",
  "/>",
  "```",
  "",
  "## shader-mesh-gradient",
  "Four-color animated mesh, frame-driven for deterministic renders.",
  "",
  "## command-menu",
  "A timeline-driven command palette: open, filter, land on cue.",
  "| prop | type | default |",
  "| items | CommandMenuEntry[] | — |",
  "| revealCount | number | 0 |",
  "",
  "## use-button-transition",
  "Describe steps as { at, state } — the frame renders the rest.",
  "",
  "## backdrop",
  "Full-bleed framing layer with safe-area padding for scenes.",
  "",
  "## confetti",
  "A one-shot deterministic burst, seeded once per render.",
  "",
  "## shader-god-rays",
  "Volumetric light shafts from a movable source point.",
  "",
  "## chat-flow",
  "One message API across iMessage, Telegram, and plain skins.",
];

const corpusColor = (line: string): string => {
  if (line.startsWith("# ") || line.startsWith("## ")) return INK;
  if (line.startsWith("```") || line.startsWith("| ")) return FAINT;

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