Munchable has a workspace package whose only output is video. 2,635 lines of React, no footage, no After Effects project, and a Root.tsx of about a hundred lines that registers everything.
What it registers is 25 compositions and 44 stills. Every composition gets its size from one of three helpers, and all three say the same thing about length:
const size = { fps: FPS, width: W, height: H }; const fullFrame = { ...size, durationInFrames: 1 }; const phone = { fps: FPS, ...PHONE_CANVAS, durationInFrames: 1 }; const cropped = (canvas: { width: number; height: number }) => ({ fps: FPS, ...canvas, durationInFrames: 1 }); One frame, 23 times. Not because those clips are one frame long, but because durationInFrames is required and the real answer is not known until the props are.
The length is a function of the props
Remotion lets a composition compute its own metadata. That turns durationInFrames from a number you maintain into a number you derive:
export const captionMetadata: CalculateMetadataFunction<Props> = ({ props }) => ({ durationInFrames: frames(props.seconds), }); export const phoneScanMetadata: CalculateMetadataFunction<PhoneScanProps> = ({ props }) => ({ durationInFrames: timeline(props).duration, }); The caption case is trivial and still worth having: the prop is seconds, because that is the unit the person placing the clip thinks in, and the conversion lives in one function.
export const seconds = z.number().min(1).max(15); export function frames(s: number): number { return Math.round(s * FPS); } The scan case is the one that earns the design. That overlay shows a phone lifting into frame, scanning several packs one after another, holding an answer for each, then sinking away. Its length is the sum of a dozen beats, and those beats depend on how many packs were passed in and how long each answer is meant to hold. Written as a literal, the number is wrong the first time somebody adds a fourth pack, and wrong in a way that truncates the clip rather than erroring.
So durationInFrames: 1 in the composition is a promise that the number is computed elsewhere. The placeholder reads as a smell until you notice it is the same everywhere, at which point it reads as a convention: a declared duration in this file means the clip has a fixed length, and there is exactly one of those.
The exception is the end card, at CTA_FRAMES, which resolves to 166 frames. It is a fixed animation with a fixed message and it does not take props.
The twenty-fifth composition declares one frame and means it. It is a proof sheet with every illustrated pack, front and back, on a single frame, for checking the label copy before anything is posted. It is marked as not for posting, and it being a composition rather than a still is the only reason it sits next to the rest.
Transparent, and cropped to the content
These are overlays. The footage is a person talking to a camera, and the clips lay over it, so almost every composition renders a transparent background. Exactly one has a background, the end card, because an end card is the one moment the video is allowed to stop being footage.
The second half of that decision is the canvas. The obvious thing is to render every overlay at 1080 by 1920 so it drops onto the timeline aligned. The problem is that a phone on a transparent 1080 by 1920 frame is mostly empty pixels, and scaling or repositioning it in the edit means nudging a frame whose visible content sits somewhere inside it. So each overlay is cropped to its own content:
export const PHONE_PAD = { side: 100, top: 80, bottom: 150 } as const; export const PHONE_CANVAS = { width: PHONE_W + 2 * PHONE_PAD.side, height: PHONE_H + PHONE_PAD.top + PHONE_PAD.bottom, }; The padding is asymmetric because the drop shadow is. 80 above, 150 below. Get that wrong and the shadow is clipped along the bottom edge of an otherwise transparent clip, which looks like a rendering bug and is actually a canvas that is 70 pixels too short.
Two overlays keep the full 1080 by 1920 frame, the caption and the scan guide, and they keep it for the same reason: where they sit on screen is part of what they are. A caption has a top prop in frame coordinates. A scan guide is a box drawn around something in the footage. Crop those and you have thrown away the information the clip exists to carry.
The props are a typed UI
Every composition is registered with a zod schema, which is what makes Remotion Studio usable as the editing surface rather than a preview window:
export const productId = z.enum(keys(PRODUCTS)); export const profileKey = z.enum(keys(PROFILES)); export const verdict = z.enum(['good', 'caution', 'avoid', 'unknown'] satisfies VerdictValue[]); export const condition = z.enum(['none', ...keys(CONDITION_STICKER)] as ['none', ...ConditionId[]]); keys(PRODUCTS) is the move worth stealing. The illustrated cast is a plain object of 16 packs; turning its keys into a z.enum means the Studio shows a dropdown of the real cast, and adding a seventeenth pack adds it to every dropdown in the package without anything being registered twice. The satisfies VerdictValue[] on the verdict enum is the same idea pointed at the app: the list of answers the video can show is checked against the type the product defines, so the two cannot drift apart silently.
The ids are file friendly for the same reason. A still of a pack renders as pack-peanut-butter.png because of one line:
const kebab = (s: string) => s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); 16 packs, front and back, is 32 of the 44 stills. The rest are the verdict markers in two shapes and the brand marks. All of them are rendered from the components the video uses, so a thumbnail and the clip it belongs to cannot disagree about what anything looks like.
The video asks the product rather than quoting it
This is the part that changed how I think about marketing assets.
Each illustrated pack carries a real ingredient list. The clip does not take "show avoid here" as a prop. It takes a pack and a profile, and the answer that appears on the phone is computed by importing the same workspace package the product itself runs:
"dependencies": { "@munchable/rules-engine": "workspace:*", "@munchable/content": "workspace:*", "@munchable/design-tokens": "workspace:*", "remotion": "4.0.524" } So a video cannot show an answer the product would not give. Not as a matter of discipline, but because nowhere in the package is there a field to type one into. If a clip shows something surprising, the clip is right and my expectation was wrong, which is the correct way round for an advert. And the day the product's answer for one of those lists changes, the clip changes with it the next time it renders.
Two smaller rules fall out of the same place. The packs are generic brands invented for the purpose, because a real pack in a paid placement is a trademark problem. And pack colours are deliberately held away from green and red:
/* Pack colours stay off green and red so nothing on a shelf reads as a verdict. */ In a product where green and red mean something specific, a shelf of colourful packs is a shelf of accidental claims. Three sentences of comment, and a palette of sky, butter, blush, lavender, peach, biscuit, cocoa and gold.
What it is like to use
pnpm studio opens Remotion Studio on port 3333 with five folders: the overlays, the old reels' scans kept as phone overlays under their original names, the end card, the icons, and the proof sheet. Pick a composition, pick a pack and a profile from the dropdowns, type how many seconds the answer should hold, and the clip is that long. Render, drop the transparent file onto the footage, place it.
The screens these overlays imitate are in the app, but the public half of the same content is on the site and worth comparing against: the recipe library and a recipe page are the web version of the screen one overlay animates, and the condition index is the eight-way split the compare overlay is about. The end card points at the homepage, which is the only line in the whole package that is pure marketing.
25 compositions, one declared duration between them, and the only hardcoded length in the package belongs to the one clip that does not take an argument.



