# Retention Pacing — Short-Form Video Reference Deep reference for the `short-form-video` skill: a full runnable Remotion short, a hook library, a retention-debugging method, per-platform safe zones, and batch output. All timing is frame-driven so renders are deterministic. ## 1. A complete `` composition One self-contained vertical short: frame-driven shot scheduler with uneven cuts, a punch-in pattern interrupt, an on-screen hook, a 9:16 safe-area overlay (dev only), and a seamless loop. Topic content is a prop, so the same file renders many shorts. ```tsx // Short.tsx — 1080×1920, 30fps. Register at 600 frames (20s) for a clean loop. import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring, Sequence, Img, } from "remotion"; type Beat = { at: number; text: string; src?: string }; // at = seconds export type ShortProps = { hook: string; // on-screen by frame 1 beats: Beat[]; // body, one idea each loopText: string; // sentence that feeds back to frame 0 }; // --- frame-driven shot scheduler: uneven cuts read as momentum --- const useShot = (cuts: number[]) => { const { fps } = useVideoConfig(); const frame = useCurrentFrame(); let i = 0; for (let k = 0; k < cuts.length; k++) if (frame >= cuts[k] * fps) i = k; const startF = cuts[i] * fps; const endF = (cuts[i + 1] ?? cuts[i] + 2) * fps; const local = (frame - startF) / (endF - startF); // 0→1 within shot return { index: i, local, startF }; }; // --- punch-in: the cheapest pattern interrupt, snaps on the cut --- const PunchIn: React.FC<{ startF: number; to?: number; children: React.ReactNode }> = ({ startF, to = 1.1, children, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const s = spring({ frame: frame - startF, fps, config: { damping: 13, stiffness: 220 } }); const scale = interpolate(s, [0, 1], [1, to]); return {children}; }; // --- hook: text present from frame 0, micro-overshoot, no fade-up --- const Hook: React.FC<{ text: string }> = ({ text }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const pop = spring({ frame, fps, config: { damping: 9, stiffness: 260 } }); const scale = interpolate(pop, [0, 1], [0.9, 1]); return (
{text}
); }; export const Short: React.FC = ({ hook, beats, loopText }) => { const { fps, durationInFrames } = useVideoConfig(); const frame = useCurrentFrame(); const cuts = [0, ...beats.map((b) => b.at)]; // hook shot + one per beat const { index, startF } = useShot(cuts); const current = index === 0 ? null : beats[index - 1]; // seamless loop: match the LAST 0.5s back toward the opening frame const tailF = durationInFrames - frame; const loopFade = interpolate(tailF, [0, fps * 0.5], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); return ( {current?.src && } {index === 0 && } {/* body caption — placement only; styling/word-timing belongs to caption-animation */} {current && (
{current.text}
)} {/* loop sentence rises as the clip ends, easing the eye back to frame 0 */}
{loopText}
{/* remove for final render */}
); }; ``` Register it with a duration that *is* the loop length: ```ts // Root.tsx import { Composition } from "remotion"; import { Short } from "./Short"; export const Root = () => ( ); ``` ## 2. The 9:16 safe-area overlay (dev-only) Render this on top while editing; delete it before the final export. ```tsx import { AbsoluteFill } from "remotion"; export const SafeZone: React.FC = () => ( {/* universal safe box: center 900×1400 in 1080×1920 */}
{/* platform UI bands to keep clear */}
); ``` ## 3. Hook pattern library The hook is a sentence that opens a gap the brain must close, plus the most striking frame as frame 0. Copyable shapes: | Pattern | Template | Note | |---|---|---| | Open loop | "The real reason your ___ keeps ___ …" | answer withheld until payoff | | Direct promise | "___ in 30 seconds, no ___ needed" | name the payoff + the timebox | | Negation | "Stop ___. You're doing it backwards." | strong-opinion content | | Pattern interrupt | snap-zoom / whip / hard visual mismatch | entertainment, no text needed | | Stakes | "This ___ cost me $___ — don't repeat it" | story / case-study | | Listicle | "3 ___ that ___ (the 3rd one ___ )" | tees up a forward reference | Rules that matter more than the wording: (1) the hook text is on-screen by frame 1 — most viewers are muted; (2) frame 0 is the best frame, never a fade-up from black; (3) one gap only — stacking two hooks closes neither. ## 4. Retention-curve debugging Treat the per-second retention graph (TikTok Analytics, YT Studio "Audience retention", Reels) as a profiler. | Symptom on the curve | Likely cause | Fix | |---|---|---| | Cliff at 0:00–0:02 | weak hook / slow first frame | new hook line; cut the ramp-in; lead with the best frame | | Steady gentle decline | normal — leave it | — | | Flat-then-drop in the middle | low-energy body, cuts too sparse | add interrupts; tighten cut spacing to 2–4s | | Bump back up near the end | viewers are looping | lean in — tighten the loop, shorten total length | | Spike at one timestamp | a re-watched moment | front-load that beat or make it the hook | Target ≥70% average view-through. Iterate the hook first — it moves the curve more than any other edit because it gates everything after it. ## 5. Per-platform safe zones All on a 1080×1920 canvas. The universal box (center 900×1400) is the intersection; design inside it and one master fits every platform. | Platform | Top clear | Bottom clear | Right clear | Notes | |---|---|---|---|---| | Universal (use this) | ~120px | ~320px | ~120px | center 900×1400 holds on all | | TikTok | ~108px | ~320px | ~120px | caption + button rail at bottom-right | | Instagram Reels | ~120px | ~310px | ~84px | audio bar + caption bottom | | YouTube Shorts | ~120px | ~300px | ~60px | channel + subscribe bottom band | | TikTok Ads | ~108px | ~370px | ~120px | extra room for the CTA button | When unsure, design to the *largest* margin (TikTok Ads bottom 370px) and nothing ever clips. ## 6. Template × topic — batch output The payoff of code-driven shorts: lock the design once, swap only the props, render N videos. Make topic content a prop (already done above), keep one theme object, and loop over JSON files. ```jsx // theme.ts — one source of truth so 50 shorts stay on-brand export const theme = { font: "Inter", hookSize: 76, bodySize: 60, bg: "#0b0b0f", fg: "#fff", accent: "#39d" }; ``` ```bash # render the same Short template for every topic file in /topics for f in topics/*.json; do name=$(basename "$f" .json) npx remotion render Short "out/$name.mp4" --props="$f" done ``` Each `topics/*.json` is one `ShortProps` object (`hook`, `beats`, `loopText`). Keep the hook line, cut points, and loop sentence per-topic; everything else stays fixed so the series reads as one brand. --- ## Built by the team behind iart.ai This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can batch-produce short-form videos from one template × data — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=short-form-video)