All demos

shadcn/ui — Introducing shadcn/ui (gift cut)

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/introducing-shadcn

Render it locally

Renders the MP4 on your machine with the Remotion CLI.

$ pnpm dlx remotion render introducing-shadcn out/introducing-shadcn.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.

Make a gift video introducing shadcn/ui, done in shadcn's own monochrome look — zinc-950 canvas, ink text, Geist at regular weight only, no color anywhere except muted shader covers sampled from shadcn's own X avatar. Open with a camera glide across a dense wall of real shadcn/ui components rendered live (pull actual cards from the ui.shadcn.com homepage — create account, calendar, command menu, and so on), then land the name, the "not a component library, it's how you build your own" creed, and a quick montage proving the breadth (blocks, charts, themes, colors). Show the install command turning into a component rolodex, then demo the preset builder — flipping style, base color, theme, and font live while a sign-in card reshapes — collapsing into a single preset code you can pass to init. Close with a big proof number (a million apps a month), a few logos of who's building on it, and a quiet outro lockup with "Open Source. Open Code." and no URL. Use remocn for the shader covers and typography beats.

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,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import {
  TransitionSeries,
  linearTiming,
  type TransitionPresentation,
  type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { loadFont as loadSans } from "@remotion/google-fonts/Geist";
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 { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { LineByLineSlide } from "@/components/remocn/line-by-line-slide";
import { whipPan } from "@/components/remocn/whip-pan";

import { ShaderSimplexNoise } from "@/components/remocn/shader-simplex-noise";
import { ShaderSwirl } from "@/components/remocn/shader-swirl";
import { ShaderDithering } from "@/components/remocn/shader-dithering";
import { ShaderSmokeRing } from "@/components/remocn/shader-smoke-ring";

import { Button } from "@/demos/_ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/demos/_ui/card";
import { Input } from "@/demos/_ui/input";
import { Label } from "@/demos/_ui/label";
import { Switch } from "@/demos/_ui/switch";

import { FieldScene, S_FIELD } from "./field";
import { CategoriesScene, categorySceneDuration } from "./categories";
import { VideoScopeStyle } from "@/demos/_ui/video-scope";

// Geist regular only — the whole video never goes above weight 400.
const { fontFamily: SANS_FAMILY } = loadSans("normal", {
  subsets: ["latin"],
  weights: ["400"],
});
const { fontFamily: MONO_FAMILY } = loadMono("normal", {
  subsets: ["latin"],
  weights: ["400"],
});

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

// shadcn's own monochrome: zinc-950 canvas, ink foreground. The only color in
// the video lives inside shader backgrounds — muted tones sampled from
// shadcn's X avatar (deep plum, dark navy, dusty rose, dusty blue).
const ZINC = "#09090b";
const INK = "#fafafa";
const MUTED = "rgba(250,250,250,0.62)";
const FAINT = "rgba(250,250,250,0.4)";

const PLUM = "#3d2547";
const ROSE = "#6b4054";

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

// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps). Transitions overlap.
// ---------------------------------------------------------------------------
const S_MEET = 130; //     "This is shadcn/ui"
const S_TAGLINE = 76; //   "Beautifully designed components"
const S_CREED_A = 76; //   "Not a component library"
const S_CREED_B = 96; //   kinetic "How you build your own"
const S_PILLARS = 104; //  three pillar lines
const S_INSTALL_TITLE = 70; // "Any component, one command"
const S_INSTALL_CMD = 150; //  typed command + 3D name rolodex
const S_PRESET_TITLE = 66; //  "Your whole design system, one preset"
const S_CREATE = 200; //   shadcn create: settings flip, the preview reacts
const S_PRESET = 176; //   init --preset: the code flips, the UI re-themes live
const S_SWITCH = 64; //    "Switch it any time"
const S_YOURS = 70; //     "And the code is yours"
const S_APPS = 156; //     1,000,000 apps: dive → wheel spins up → pan → whip out
const WHO_BEAT = 24; //    one builder-name hard cut
const S_WHO = WHO_BEAT * 3 + 56; // Startups / YC / Fortune 500s / everyone
const S_ECO = 72; //       "A massive ecosystem — and room for you to build"
const S_BEST = 58; //      "The best part?"
const S_NEEDS = 96; //     kinetic "Everyone needs UI"
const S_OUTRO = 150; //    smoke ring blooms → wordmark + motto

const T_SWIRL = 104; //    shader-swirl cover (twist 1→0 → hold → 0→1)
const T_DITHER = 40; //    dusty-rose dither dissolve (held mid-frame)
const T_X = 14; //         crossfade
const T_WHIP = 18; //      whip-pan carrying the apps pan into the next scene
const T_BLUR = 16; //      blur crossfade

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

// ===========================================================================
// Scene 2 — This is shadcn/ui. Static on entry (the swirl cover's exit
// reveals it with a z-axis scale), then it plays its own exit so the tagline
// starts on 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: 74,
          color: INK,
          opacity: 1 - exitP,
          transform: `translateY(${exitP * -10}px) scale(${1 - exitP * 0.05})`,
          filter: `blur(${exitP * 6}px)`,
        }}
      >
        This is shadcn/ui
      </span>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 3 — Tagline. Glides in only after the name has fully exited.
// ===========================================================================
const TaglineScene: React.FC = () => (
  <AbsoluteFill>
    <ShortSlideRight
      text="Beautifully designed components"
      fontSize={48}
      fontWeight={400}
      color={INK}
    />
  </AbsoluteFill>
);

// ===========================================================================
// Scene 4 — The creed, first half. The negation lands solo.
// ===========================================================================
const CreedAScene: React.FC = () => (
  <AbsoluteFill>
    <Sequence from={20} durationInFrames={56}>
      <ScaleDownFade
        text="Not a component library"
        fontSize={54}
        fontWeight={400}
        color={INK}
      />
    </Sequence>
  </AbsoluteFill>
);

// ===========================================================================
// Scene 5 — The creed, second half. The answer assembles word by word.
// ===========================================================================
const CreedBScene: React.FC = () => (
  <AbsoluteFill>
    <Sequence from={6}>
      <KineticCenterBuild
        text="How you build your own"
        fontSize={62}
        fontWeight={400}
        color={INK}
      />
    </Sequence>
  </AbsoluteFill>
);

// The category montage lives in ./categories — real shadcn/ui blocks,
// charts, theme flips, and the full Tailwind palette, hard-cut like a
// montage. The last beat absorbs the dither overlap.
const S_CATEGORIES = categorySceneDuration(T_DITHER);

// ===========================================================================
// Scene 7 — The pillars. Three claims accumulate as a block.
// ===========================================================================
const PillarsScene: React.FC = () => (
  <AbsoluteFill>
    <Sequence from={24}>
      <LineByLineSlide
        text={
          "Accessible by default\nComposable by design\nOpen code, always"
        }
        fontSize={50}
        fontWeight={400}
        color={INK}
      />
    </Sequence>
  </AbsoluteFill>
);

// ===========================================================================
// Scene 8a — Install title. Its own typographic beat; plays its own exit so
// the command scene starts on an empty canvas.
// ===========================================================================
const InstallTitleScene: 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={{
        opacity: 1 - exitP,
        transform: `translateY(${exitP * -10}px) scale(${1 - exitP * 0.05})`,
        filter: `blur(${exitP * 6}px)`,
      }}
    >
      <ShortSlideRight
        text="Any component, one command"
        fontSize={50}
        fontWeight={400}
        color={INK}
      />
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 8b — The command. It types itself; once typing lands, the component
// name becomes a 3D rolodex flipping through the registry — any component,
// same command.
// ===========================================================================
const CMD = "npx shadcn add ";
const PKG_NAMES = ["button", "dialog", "command", "tabs", "chart-area"];
const SIZER = PKG_NAMES.reduce((a, b) => (b.length > a.length ? b : a));
const CMD_START = 10;
const FLIP_START = 52; // first flip, after the typed command has settled
const FLIP_PER = 20; //  one name every 20 frames
const FLIP_DUR = 10; //  the 3D flip itself

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

  const full = CMD + PKG_NAMES[0];
  const typed = Math.max(
    0,
    Math.min(full.length, Math.floor((frame - CMD_START) * 1.5)),
  );
  const visible = full.slice(0, typed);
  const cmdOpacity = interpolate(
    frame,
    [CMD_START - 4, CMD_START],
    [0, 1],
    clampOpts,
  );
  const typingDone = typed >= full.length;
  const caretOn = typingDone ? Math.floor(frame / 15) % 2 === 0 : true;
  // The caret leaves just before the rolodex starts spinning.
  const caretOpacity = interpolate(
    frame,
    [FLIP_START - 12, FLIP_START - 4],
    [1, 0],
    clampOpts,
  );
  const flipping = frame >= FLIP_START - 4;

  return (
    <AbsoluteFill
      style={{
        alignItems: "center",
        justifyContent: "center",
        opacity: cmdOpacity,
      }}
    >
      <span style={{ fontFamily: MONO, fontSize: 28, color: MUTED }}>
        <span style={{ color: FAINT }}>$ </span>
        <span>{visible.slice(0, Math.min(typed, CMD.length))}</span>
        <span
          style={{
            display: "inline-block",
            width: `${SIZER.length}ch`,
            textAlign: "left",
            position: "relative",
            verticalAlign: "bottom",
            perspective: 420,
            color: INK,
          }}
        >
          {/* Invisible sizer keeps the container's height and baseline. */}
          <span style={{ visibility: "hidden" }}>{SIZER}</span>
          {!flipping ? (
            <span
              style={{
                position: "absolute",
                left: 0,
                top: 0,
                whiteSpace: "nowrap",
              }}
            >
              {typed > CMD.length ? visible.slice(CMD.length) : ""}
              <span
                style={{
                  display: "inline-block",
                  width: 14,
                  height: 28,
                  marginLeft: 3,
                  verticalAlign: "-4px",
                  background: caretOn ? MUTED : "transparent",
                  opacity: caretOpacity,
                }}
              />
            </span>
          ) : (
            PKG_NAMES.map((name, i) => {
              // The outgoing name clears first; the incoming one follows
              // half a flip later, so the two never sit on top of each other.
              const inStart = FLIP_START + (i - 1) * FLIP_PER + 5;
              const outStart = FLIP_START + i * FLIP_PER;

              let rotate = 0;
              let y = 0;
              let opacity = 1;

              if (i > 0) {
                const pIn = interpolate(
                  frame,
                  [inStart, inStart + FLIP_DUR],
                  [0, 1],
                  { ...clampOpts, easing: Easing.out(Easing.cubic) },
                );
                rotate = (1 - pIn) * -85;
                y = (1 - pIn) * 20;
                opacity = pIn;
              }
              if (i < PKG_NAMES.length - 1) {
                const pOut = interpolate(
                  frame,
                  [outStart, outStart + FLIP_DUR],
                  [0, 1],
                  { ...clampOpts, easing: Easing.in(Easing.cubic) },
                );
                rotate += pOut * 85;
                y -= pOut * 20;
                opacity *= 1 - pOut;
              }
              if (opacity <= 0.001) return null;

              return (
                <span
                  key={name}
                  style={{
                    position: "absolute",
                    left: 0,
                    top: 0,
                    whiteSpace: "nowrap",
                    opacity,
                    transform: `translateY(${y}px) rotateX(${rotate}deg)`,
                    transformOrigin: "50% 50%",
                    backfaceVisibility: "hidden",
                  }}
                >
                  {name}
                </span>
              );
            })
          )}
        </span>
      </span>
    </AbsoluteFill>
  );
};

// ===========================================================================
// Scene 8c — Preset title. Same typographic setup as the install title:

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