timotion-kit
v0.2.0
Published
8-bit Monokai component kit for Remotion concept-explainer videos
Readme
timotion-kit
A Remotion 4 + React 19 component library, published on npm, for building "concept explainer" short-form videos: dark, 8-bit game styled vertical videos (Monokai palette, pixel fonts, hard-edged blocky panels, chiptune sound effects) that visually explain one concept per episode.
Install it once per project and update it like any other dependency — see Installing — instead of copy-pasting source between projects and letting each copy drift out of sync.
For AI-agent-facing conventions (episode-authoring workflow, non-goals, etc.) see the consuming project's own
CLAUDE.md(intimotion-templateor wherever you're building an episode) — that's project-specific and lives outside this package. This README is the human-facing reference for the kit itself.
Folder structure
timotion-kit/
├── src/
│ ├── theme.ts # colors, safeArea, radius, typeScale, pixelShadow, statusColor/Glow, video config
│ ├── fonts.ts # fontFamily / fontFamilyTitle / fontFamilyLabel (Google Fonts, loaded once)
│ ├── sfx.ts # sound name -> public/sfx/*.wav path map
│ ├── index.ts # barrel: re-exports theme, fonts, sfx, components
│ ├── scripts/
│ │ └── generate-sfx.mjs # synthesizes the chiptune WAVs — see "Installing" below
│ └── components/
│ ├── index.ts # barrel: one export pair (component + prop type) per component
│ ├── common/
│ │ └── PixelSound.tsx # shared leaf component — see "Why common/" below
│ ├── TerminalHeader.tsx
│ ├── EntityCard.tsx
│ ├── MessageTravel.tsx
│ ├── LogFeed.tsx
│ ├── ResultBadge.tsx
│ ├── Watermark.tsx
│ ├── PixelBackground.tsx
│ ├── DiagramBox.tsx
│ ├── FadeLine.tsx
│ ├── PulseStream.tsx
│ ├── Typewriter.tsx
│ ├── ChatBubble.tsx
│ └── TypingIndicatorBubble.tsx
└── dist/ # built output (published to npm; not committed)Standing rule: nothing inside src/ imports from outside src/. That
invariant is what keeps the package self-contained and its build simple.
Why common/?
components/common/ holds only code that's genuinely shared between
sibling components — as opposed to code a scene calls directly. Today
that's exactly one file: PixelSound, which 6 of the other 13 components
(TerminalHeader, EntityCard, MessageTravel, LogFeed, ResultBadge,
Typewriter) import internally to trigger their sound effects. Everything
else in components/ is used by scene code, not by another component, so
it stays flat at the top level. If a second genuinely-shared piece ever
emerges, it belongs in common/ too; a component only one other component
happens to use (e.g. ChatBubble importing Typewriter) doesn't qualify —
it's a normal dependency, not shared infrastructure.
The public import path is unaffected by this: import { PixelSound } from
"timotion-kit" keeps working exactly the same, since the barrel re-exports
it regardless of which file it lives in.
Installing
npm install timotion-kit
npx timotion-generate-sfxnpm install timotion-kitadds the package as a normal dependency —remotion,react,react-dom,@remotion/google-fonts, and@remotion/mediaare peer dependencies, so make sure your project already has those installed (any project scaffolded fromtimotion-templatealready does).npx timotion-generate-sfxis a one-time step per project (run it again after anynpm update timotion-kitthat adds a new sound). It synthesizes the chiptune WAVs into your project's ownpublic/sfx/— required because Remotion'sstaticFile()always resolves against the current project'spublic/folder, not intonode_modules, so the actual audio binaries have to live in your project regardless of where the code that references them comes from.- Import everything from
"timotion-kit"(the package root) in your own compositions:import { EntityCard, colors, safeArea } from "timotion-kit". - To pick up kit updates later:
npm update timotion-kit.
Design tokens (theme.ts)
Palette is Monokai (the classic code-editor color scheme).
| Token | Value | Usage |
|---|---|---|
| colors.bg | #272822 | page background |
| colors.bgTop | #3E3D32 | PixelBackground gradient top |
| colors.star | #F8F8F2 | PixelBackground twinkling stars |
| colors.cardFill | #2D2E27 | entity card fill |
| colors.cardBorder | #49483E | default card border |
| colors.divider | rgba(248,248,242,0.15) | thin header divider |
| colors.textPrimary | #F8F8F2 | titles, card values |
| colors.textDim | #75715E | dim meta text |
| colors.textLabel | #CFCFC2 | card labels, log body text |
| colors.green / greenGlow | #A6E22E / rgba | prompt, ok state, success |
| colors.red / redGlow | #F92672 / rgba | warn/error state |
| colors.amber | #E6DB74 | secondary accent |
statusColor / statusGlow map a Status ("idle" | "active" | "ok" |
"warn" | "error") to the right color/glow — always source status colors
from these maps, never hardcode green/red per component.
safeArea (marginX: 64, marginTop: 96, marginBottom: 140) is the
standard inset from the frame edges, so content doesn't collide with
platform UI on Reels/Shorts/TikTok.
radius is 0 everywhere — sharp pixel corners, no rounding, is the core
of the 8-bit look. typeScale holds the font-size scale — extend it rather
than hardcoding new sizes inline.
pixelShadow(color, offset?) produces a hard, unblurred offset shadow
(Npx Npx 0 0 color) — the 8-bit equivalent of a soft glow. Always use
this helper instead of writing a boxShadow string by hand.
video holds the target composition config: { width: 1080, height: 1920,
fps: 30 }.
Typography (fonts.ts)
| Export | Font | Usage |
|---|---|---|
| fontFamilyTitle | Space Grotesk (500/700) | big titles |
| fontFamilyLabel | JetBrains Mono (600/700) | short HUD labels/tags |
| fontFamily | JetBrains Mono (400/500) | body/log copy, long-form text |
fontFamilyLabel and fontFamily are both JetBrains Mono at different
weights — a deliberate two-family system (one display font, one mono).
Text that might wrap to multiple lines should use lineHeight of at least
1.4.
Sound effects (sfx.ts, components/common/PixelSound.tsx)
Short 8-bit/chiptune WAVs, synthesized (not sourced/licensed) via
scripts/generate-sfx.mjs, living in public/sfx/.
| Sound | File | Used by |
|---|---|---|
| blip | sfx/blip.wav | EntityCard pop-in, MessageTravel on arrival |
| type | sfx/type.wav | LogFeed / Typewriter per-character reveal |
| coin | sfx/coin.wav | ResultBadge positive (ok/active) |
| error | sfx/error.wav | ResultBadge negative (warn/error) |
| powerup | sfx/powerup.wav | reserved for bigger positive story-beats |
| start | sfx/start.wav | TerminalHeader title entrance |
Play sounds through <PixelSound name="..." from={...} volume={...}
enabled={...} /> (wraps <Audio> from @remotion/media) — don't import
<Audio> directly in scene/component code. Every sound-playing component
exposes a sound?: boolean prop (default true) so a caller can mute one
instance without silencing the whole video.
Component reference
TerminalHeader—{ conceptTag?, promptUser?, command, title, showCursor?, showDivider?, from?, durationInFrames?, sound?, typeCommand?, charsPerFrame? }. Concept tag + green prompt line + big title with a blinking pixel cursor block + divider. By default the command line fades in as a whole (durationInFramessplits into quarters for prompt/title/divider); passtypeCommand: trueto have it type out character-by-character viaTypewriterinstead — title/divider timing then derives from the command's own length. UseterminalHeaderTypedDuration(command, charsPerFrame?)to size a scene's timeline around a typed header instead of guessing.EntityCard—{ label, value?, sublabel?, status?, glow?, width?, height?, from?, durationInFrames?, sound? }. Dark sharp-cornered card with a square status dot. Self-contained, no self-positioning — the parent places it via flex/grid or absolute coordinates.MessageTravel—{ start, end, bend?, status?, icon?, from?, durationInFrames?, sound? }. Animates an icon traveling between two points along a straight or quadratic-bezier path (viabend). No line is drawn, only the icon moves.LogFeed—{ lines, from?, staggerFrames?, lineDurationInFrames?, align?, sound? }. A stack of[ok]/[warn]log lines that reveal progressively with a stagger.ResultBadge—{ title, subtitle?, status?, from?, durationInFrames?, sound? }. Bordered callout with a big colored headline + dim subtitle, for emphasis moments.Watermark—{ text?, from?, durationInFrames?, fontSize? }. Bottom-right credit text, always positioned viasafeArea.PixelBackground— no required props. Full-frame Monokai gradient plus a sparse field of slowly twinkling pixel stars, deliberately low-contrast. Always the first child of a scene'sAbsoluteFill.DiagramBox—{ x, y, width, height, label, from?, durationInFrames? }. Bordered, unfilled region with a top-left uppercase label — the basic node in a node-and-connector diagram.FadeLine—{ x1, y1, x2, y2, from, to?, durationInFrames?, color?, strokeWidth? }. SVG connector line between two diagram nodes; fades in atfrom, optionally hard-cuts out atto. Render inside an<svg>that overlays the scene.PulseStream—{ x1, y1, x2, y2, activeFrom, activeTo, period?, travelFrames?, color?, glowColor? }. A repeating stream of glow-ring + solid-core dots fired along a line for[activeFrom, activeTo]— represents continuous data flow, not a one-off trip. Pairs withFadeLine, same<svg>overlay.Typewriter—{ text, from, charsPerFrame?, cursor?, cursorColor?, sound?, soundVolume?, style? }. Character-by-character text reveal with a blinkingcurrentColorcursor and a per-chartypetick — the reusable core behind every typing beat. Build scene-specific typed text on top of this rather than re-deriving typed-character/cursor logic.ChatBubble—{ text, from, align, accent?, typeDelay?, fontSize? }. A chat message bubble, left- or right-aligned, withTypewriter-driven text reveal. Usealign="left"for the user's message,"right"for the AI's response.TypingIndicatorBubble—{ from, to, align? }. A "thinking" bubble (three dots pulsing in a wave) that hides itself oncetois reached — chain aChatBubbleright after it withfrom={to}.PixelSound(components/common/) —{ name, from?, volume?, enabled?, durationInFrames? }. Sound-trigger wrapper; the shared leaf every sound-playing component above depends on.Recap—{ sections, x, y, width, from, bulletGap?, sectionStart?, sectionGap?, sound? }. A takeaways/best-practices/pitfalls style recap: named sections ({ title, accent, bullets }[]) stacked full-width (not squeezed into side-by-side columns, which cuts long bullets off), each bullet typing out in turn with a blinking cursor and its row space reserved up front (no layout jump as bullets start typing). Pair withrecapDuration(sections, opts?)to size how long it needs to stay mounted (typing time + areadHold, default 3s) instead of guessing a fixed hold. Every episode so far uses the same 3-section shape (green TAKEAWAYS, green BEST PRACTICES, red PITFALLS) — usebuildRecapSections({ takeaways, bestPractices, pitfalls })to get that standardRecapSection[]from just the bullet lists, or constructRecapSection[]directly for a different set of names/colors.
Timing contract
Every animated component accepts Remotion-native from?: number (start
frame, default 0) and durationInFrames?: number (entrance/travel
duration, each component has its own sensible default) — the same
semantics as native Remotion layer props. Internally each component
computes localFrame = frame - from and drives interpolate() off that,
with extrapolateLeft/Right: "clamp" always set.
Animation implementation rules
- Drive all animation from
useCurrentFrame()+interpolate(). No CSStransition/animation, no Tailwind animation classes — they don't render correctly in Remotion. - Use
Easing.bezier(...)(orEasing.spring(...)) for eased motion. - Use the
translate/scale/rotateCSS properties directly instyle, not atransformstring. Forscale, passoutput: "perceptual-scale"tointerpolate(). - Wrap the animated root element in
<Interactive.Div name="...">so it shows up correctly in Remotion Studio's timeline/trim UI.
