@devinilabs/reelstack 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE +128 -0
  2. package/README.md +125 -0
  3. package/cli/beats.js +124 -0
  4. package/cli/bootstrap.js +124 -0
  5. package/cli/capture.js +34 -0
  6. package/cli/direction.js +114 -0
  7. package/cli/icons.js +49 -0
  8. package/cli/index.js +101 -0
  9. package/cli/init.js +253 -0
  10. package/cli/license.js +168 -0
  11. package/cli/lint.js +865 -0
  12. package/cli/preview.js +59 -0
  13. package/cli/render.js +404 -0
  14. package/cli/scaffold.js +239 -0
  15. package/cli/smoke.js +76 -0
  16. package/cli/update.js +26 -0
  17. package/cli/utils.js +184 -0
  18. package/docs/buyers-guide.md +220 -0
  19. package/docs/design-discipline.md +130 -0
  20. package/docs/family-galleries/dark.md +95 -0
  21. package/docs/family-galleries/forbidden.md +78 -0
  22. package/docs/family-galleries/glass.md +98 -0
  23. package/docs/family-galleries/paper.md +82 -0
  24. package/docs/family-galleries/warm.md +86 -0
  25. package/docs/superpowers/plans/2026-05-09-reelstack-init-readiness-gate.md +1166 -0
  26. package/docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md +233 -0
  27. package/families/dark/components/DriftingSpotlights.tsx +59 -0
  28. package/families/dark/components/FilmGrain.tsx +44 -0
  29. package/families/dark/components/ForestCard.tsx +43 -0
  30. package/families/dark/components/GridBackground.tsx +29 -0
  31. package/families/dark/components/RadialVignette.tsx +21 -0
  32. package/families/dark/components/Scanlines.tsx +35 -0
  33. package/families/dark/components/SegmentOpacity.ts +37 -0
  34. package/families/dark/components/index.ts +13 -0
  35. package/families/dark/index.ts +31 -0
  36. package/families/dark/palette.ts +98 -0
  37. package/families/dark/presets/claudedispatch.ts +46 -0
  38. package/families/dark/presets/codedrop.ts +37 -0
  39. package/families/dark/presets/gpt55.ts +54 -0
  40. package/families/dark/presets/notebooklm.ts +50 -0
  41. package/families/dark/presets/resourcescta.ts +35 -0
  42. package/families/dark/presets/skills.ts +40 -0
  43. package/families/dark/presets/stitch.ts +46 -0
  44. package/families/dark/presets/stitch2.ts +43 -0
  45. package/families/dark/typography.ts +16 -0
  46. package/families/forbidden/components/ForbiddenCausticBlobs.tsx +52 -0
  47. package/families/forbidden/components/NewsprintTexture.tsx +28 -0
  48. package/families/forbidden/components/TintedShadow.tsx +36 -0
  49. package/families/forbidden/components/index.ts +38 -0
  50. package/families/forbidden/index.ts +17 -0
  51. package/families/forbidden/palette.ts +88 -0
  52. package/families/forbidden/presets/heretic.ts +44 -0
  53. package/families/forbidden/typography.ts +18 -0
  54. package/families/glass/components/BreakdownCard.tsx +158 -0
  55. package/families/glass/components/CausticBlobs.tsx +49 -0
  56. package/families/glass/components/Counter.tsx +72 -0
  57. package/families/glass/components/EyebrowPill.tsx +59 -0
  58. package/families/glass/components/FilmStrip.tsx +202 -0
  59. package/families/glass/components/FloatingGlyphs.tsx +78 -0
  60. package/families/glass/components/GlassCard.tsx +58 -0
  61. package/families/glass/components/GlassCardBezel.tsx +45 -0
  62. package/families/glass/components/HairlineGrid.tsx +30 -0
  63. package/families/glass/components/IridescentRing.tsx +114 -0
  64. package/families/glass/components/IridescentText.tsx +98 -0
  65. package/families/glass/components/LightBeam.tsx +46 -0
  66. package/families/glass/components/ParticleBurst.tsx +62 -0
  67. package/families/glass/components/SonarRings.tsx +81 -0
  68. package/families/glass/components/StaggeredWords.tsx +74 -0
  69. package/families/glass/components/index.ts +20 -0
  70. package/families/glass/index.ts +31 -0
  71. package/families/glass/palette.ts +93 -0
  72. package/families/glass/presets/claudewatch.ts +64 -0
  73. package/families/glass/presets/claudewatchcta.ts +43 -0
  74. package/families/glass/presets/graphify.ts +45 -0
  75. package/families/glass/presets/gstack.ts +48 -0
  76. package/families/glass/presets/jcode.ts +50 -0
  77. package/families/glass/presets/lilagents.ts +52 -0
  78. package/families/glass/presets/paperclip.ts +43 -0
  79. package/families/glass/typography.ts +15 -0
  80. package/families/index.ts +49 -0
  81. package/families/paper/components/CardSpring.tsx +42 -0
  82. package/families/paper/components/CreamGrid.tsx +26 -0
  83. package/families/paper/components/EditorialSerifText.tsx +51 -0
  84. package/families/paper/components/GreenAccentCard.tsx +10 -0
  85. package/families/paper/components/PaperShadow.tsx +30 -0
  86. package/families/paper/components/ScaleBlurText.tsx +40 -0
  87. package/families/paper/components/index.ts +11 -0
  88. package/families/paper/index.ts +23 -0
  89. package/families/paper/palette.ts +102 -0
  90. package/families/paper/presets/designreel.ts +32 -0
  91. package/families/paper/presets/devini3d.ts +45 -0
  92. package/families/paper/presets/justdrop.ts +39 -0
  93. package/families/paper/presets/opus.ts +48 -0
  94. package/families/paper/typography.ts +17 -0
  95. package/families/warm/components/AccentGlow.tsx +60 -0
  96. package/families/warm/components/BentoCell.tsx +56 -0
  97. package/families/warm/components/BentoGrid.tsx +30 -0
  98. package/families/warm/components/FilmGrain.tsx +36 -0
  99. package/families/warm/components/ScaleBlurCounter.tsx +71 -0
  100. package/families/warm/components/WarmSurface.tsx +35 -0
  101. package/families/warm/components/index.ts +11 -0
  102. package/families/warm/index.ts +19 -0
  103. package/families/warm/palette.ts +81 -0
  104. package/families/warm/presets/huashu.ts +49 -0
  105. package/families/warm/presets/mempalace.ts +51 -0
  106. package/families/warm/typography.ts +17 -0
  107. package/package.json +85 -0
  108. package/reference/dark/claudedispatch.tsx +2441 -0
  109. package/reference/dark/notebooklm.tsx +2316 -0
  110. package/reference/dark/stitch.tsx +3040 -0
  111. package/reference/forbidden/heretic.tsx +2636 -0
  112. package/reference/glass/claudewatch.tsx +3827 -0
  113. package/reference/glass/graphify.tsx +2418 -0
  114. package/reference/glass/paperclip.tsx +2218 -0
  115. package/reference/paper/designreel.tsx +883 -0
  116. package/reference/paper/justdrop.tsx +1898 -0
  117. package/reference/paper/opus.tsx +1770 -0
  118. package/reference/warm/huashu.tsx +3413 -0
  119. package/reference/warm/mempalace.tsx +2909 -0
  120. package/skill/SKILL.md +229 -0
  121. package/skill/commands/reelstack-beats.md +20 -0
  122. package/skill/commands/reelstack-capture.md +24 -0
  123. package/skill/commands/reelstack-critique.md +15 -0
  124. package/skill/commands/reelstack-dark.md +40 -0
  125. package/skill/commands/reelstack-direction.md +17 -0
  126. package/skill/commands/reelstack-forbidden.md +25 -0
  127. package/skill/commands/reelstack-glass.md +39 -0
  128. package/skill/commands/reelstack-icons.md +22 -0
  129. package/skill/commands/reelstack-init.md +17 -0
  130. package/skill/commands/reelstack-lint.md +22 -0
  131. package/skill/commands/reelstack-paper.md +36 -0
  132. package/skill/commands/reelstack-render.md +20 -0
  133. package/skill/commands/reelstack-warm.md +36 -0
  134. package/templates/dark/template.tsx +115 -0
  135. package/templates/forbidden/template.tsx +111 -0
  136. package/templates/glass/template.tsx +201 -0
  137. package/templates/paper/template.tsx +133 -0
  138. package/templates/warm/template.tsx +210 -0
  139. package/utils/ai-purple-blocklist.ts +13 -0
  140. package/utils/banned-fonts.ts +11 -0
  141. package/utils/cubic-bezier.ts +36 -0
  142. package/utils/easing.ts +84 -0
  143. package/utils/grid.ts +13 -0
  144. package/utils/render-presets.json +56 -0
  145. package/utils/safe-zones.tsx +57 -0
@@ -0,0 +1,18 @@
1
+ /** Forbidden typography scale. Geist Sans + Geist Mono only.
2
+ * Slightly larger body / caption rhythm than Glass — matches HereticReel's
3
+ * declassified-document feel. Pulled from existing template usage
4
+ * (h1 84, h2 48, body 36); locks the implicit scale into named tokens.
5
+ * Text colors come from palette (ink / inkSoft / inkMuted) — never embed
6
+ * colors in typography tokens.
7
+ */
8
+ export const typography = {
9
+ hero: { fontSize: 96, fontWeight: 700, letterSpacing: -2, lineHeight: 1.05, fontFamily: "Geist, system-ui" },
10
+ h1: { fontSize: 84, fontWeight: 700, letterSpacing: -1.5, lineHeight: 1.1, fontFamily: "Geist, system-ui" },
11
+ h2: { fontSize: 48, fontWeight: 700, letterSpacing: -1.2, lineHeight: 1.15, fontFamily: "Geist, system-ui" },
12
+ h3: { fontSize: 36, fontWeight: 600, letterSpacing: -1, lineHeight: 1.2, fontFamily: "Geist, system-ui" },
13
+ body: { fontSize: 36, fontWeight: 500, letterSpacing: -0.2, lineHeight: 1.45, fontFamily: "Geist, system-ui" },
14
+ caption: { fontSize: 22, fontWeight: 500, letterSpacing: 0.5, lineHeight: 1.4, fontFamily: "Geist, system-ui", textTransform: "uppercase" as const },
15
+ mono: { fontSize: 24, fontWeight: 500, letterSpacing: 0.3, lineHeight: 1.4, fontFamily: "Geist Mono, monospace" },
16
+ counter: { fontSize: 144, fontWeight: 700, letterSpacing: -2, lineHeight: 1, fontFamily: "Geist Mono, monospace" },
17
+ } as const;
18
+ export type Typography = typeof typography;
@@ -0,0 +1,158 @@
1
+ import React, { CSSProperties } from "react";
2
+ import { spring, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { palette } from "../palette";
4
+
5
+ /**
6
+ * BreakdownCard — glass card with a filename header, accent dot, and arrow rows.
7
+ *
8
+ * Source: ClaudeWatchReel "Type. Paste. Watch." scene — the BREAKDOWN.MD lower
9
+ * callout (around line 1307) showing what the skill extracted from a video.
10
+ * The same shape doubles as a TRIGGERS.MD / FEATURES.MD / NOTES.MD anywhere a
11
+ * reel needs a "structured output" beat.
12
+ *
13
+ * Use when: a scene needs to reveal 1–4 mono rows of "what was extracted"
14
+ * style content, anchored under a hero illustration. The blinking-cursor
15
+ * variant evokes terminal output finishing in real time.
16
+ *
17
+ * Behavior: glass-card surface (mirrors `GlassCard` variant=`strong` shadows
18
+ * + blur). Title row shows accent dot + uppercase Geist-Mono filename. Each
19
+ * row springs in with a 4-frame stagger (gentle damping). Optional 6px-wide
20
+ * blinking cursor toggles every 15 frames after the last row finishes.
21
+ */
22
+ export const BreakdownCard: React.FC<{
23
+ title?: string;
24
+ accentColor?: string;
25
+ rows: { arrow?: string; left: string; right?: string; rightAccent?: string }[];
26
+ startFrame?: number;
27
+ cursor?: boolean;
28
+ style?: CSSProperties;
29
+ }> = ({
30
+ title = "BREAKDOWN.MD",
31
+ accentColor = "#FF7A4D",
32
+ rows,
33
+ startFrame = 0,
34
+ cursor = true,
35
+ style,
36
+ }) => {
37
+ const frame = useCurrentFrame();
38
+ const { fps } = useVideoConfig();
39
+ const local = Math.max(0, frame - startFrame);
40
+
41
+ // Title row entrance
42
+ const titleT = spring({
43
+ frame: local,
44
+ fps,
45
+ config: { damping: 15, stiffness: 80 },
46
+ });
47
+
48
+ // Cursor begins blinking after the last row has had time to settle.
49
+ const lastRowStart = rows.length * 4 + 14;
50
+ const cursorVisible =
51
+ cursor && local >= lastRowStart
52
+ ? Math.floor((local - lastRowStart) / 15) % 2 === 0
53
+ : false;
54
+
55
+ return (
56
+ <div
57
+ style={{
58
+ maxWidth: 720,
59
+ background: palette.glassFillStrong,
60
+ backdropFilter: "blur(20px) saturate(180%)",
61
+ WebkitBackdropFilter: "blur(20px) saturate(180%)",
62
+ border: `1.5px solid ${palette.glassBorder}`,
63
+ borderRadius: 24,
64
+ padding: 32,
65
+ boxShadow: [
66
+ "0 1px 0 rgba(255,255,255,0.7) inset",
67
+ "0 -1px 0 rgba(255,255,255,0.3) inset",
68
+ "0 8px 24px rgba(20,20,32,0.10)",
69
+ "0 32px 64px rgba(20,20,32,0.14)",
70
+ ].join(", "),
71
+ fontFamily: "Geist Mono, monospace",
72
+ opacity: titleT,
73
+ transform: `translateY(${(1 - titleT) * 14}px)`,
74
+ ...style,
75
+ }}
76
+ >
77
+ {/* Title row */}
78
+ <div
79
+ style={{
80
+ display: "flex",
81
+ alignItems: "center",
82
+ gap: 12,
83
+ marginBottom: 18,
84
+ fontSize: 18,
85
+ color: palette.inkMuted,
86
+ letterSpacing: "0.14em",
87
+ fontWeight: 700,
88
+ textTransform: "uppercase",
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ width: 10,
94
+ height: 10,
95
+ borderRadius: "50%",
96
+ background: accentColor,
97
+ boxShadow: `0 0 14px ${accentColor}`,
98
+ }}
99
+ />
100
+ <span>{title}</span>
101
+ {cursor ? (
102
+ <span
103
+ style={{
104
+ marginLeft: "auto",
105
+ display: "inline-block",
106
+ width: 6,
107
+ height: 22,
108
+ background: cursorVisible ? palette.ink : "transparent",
109
+ }}
110
+ />
111
+ ) : null}
112
+ </div>
113
+
114
+ {/* Rows */}
115
+ {rows.map((row, i) => {
116
+ const rowLocal = local - (i + 1) * 4;
117
+ const t = spring({
118
+ frame: Math.max(0, rowLocal),
119
+ fps,
120
+ config: { damping: 15, stiffness: 80 },
121
+ });
122
+ const arrow = row.arrow ?? "→";
123
+ return (
124
+ <div
125
+ key={i}
126
+ style={{
127
+ display: "flex",
128
+ alignItems: "baseline",
129
+ gap: 12,
130
+ fontSize: 26,
131
+ fontWeight: 600,
132
+ color: palette.ink,
133
+ letterSpacing: "0.02em",
134
+ lineHeight: 1.55,
135
+ opacity: t,
136
+ transform: `translateY(${(1 - t) * 12}px)`,
137
+ willChange: "transform, opacity",
138
+ }}
139
+ >
140
+ <span style={{ color: accentColor, flexShrink: 0 }}>{arrow}</span>
141
+ <span style={{ flex: 1 }}>{row.left}</span>
142
+ {row.right ? (
143
+ <span
144
+ style={{
145
+ color: row.rightAccent ?? accentColor,
146
+ fontWeight: 700,
147
+ flexShrink: 0,
148
+ }}
149
+ >
150
+ {row.right}
151
+ </span>
152
+ ) : null}
153
+ </div>
154
+ );
155
+ })}
156
+ </div>
157
+ );
158
+ };
@@ -0,0 +1,49 @@
1
+ import React from "react";
2
+ import { AbsoluteFill, useCurrentFrame } from "remotion";
3
+ import { palette, gradients } from "../palette";
4
+
5
+ /**
6
+ * CausticBlobs — animated radial gradients drifting on sine paths.
7
+ *
8
+ * Simulates underwater light refraction. The signature background motion of
9
+ * the Glass Iridescent family. Stack 4 blobs (one per iridescent stop) and
10
+ * the result reads as living, premium glass.
11
+ *
12
+ * Frame-budget: cheap. 4 absolutely-positioned divs with CSS gradients only.
13
+ */
14
+ export const CausticBlobs: React.FC<{
15
+ intensity?: number; // 0..1 — overall opacity multiplier
16
+ reduceMotion?: boolean;
17
+ }> = ({ intensity = 1, reduceMotion = false }) => {
18
+ const frame = useCurrentFrame();
19
+ const blobs = [
20
+ { gradient: gradients.causticCyan, speedX: 0.0040, speedY: 0.0030, size: 1200 },
21
+ { gradient: gradients.causticViolet, speedX: 0.0033, speedY: 0.0042, size: 1100 },
22
+ { gradient: gradients.causticRose, speedX: 0.0027, speedY: 0.0035, size: 1300 },
23
+ { gradient: gradients.causticGold, speedX: 0.0046, speedY: 0.0024, size: 1000 },
24
+ ];
25
+ return (
26
+ <AbsoluteFill style={{ background: palette.bg, opacity: intensity }}>
27
+ {blobs.map((b, i) => {
28
+ const cx = reduceMotion ? 0.5 : 0.5 + 0.35 * Math.sin(frame * b.speedX + i);
29
+ const cy = reduceMotion ? 0.5 : 0.5 + 0.30 * Math.cos(frame * b.speedY + i * 1.3);
30
+ return (
31
+ <div
32
+ key={i}
33
+ style={{
34
+ position: "absolute",
35
+ left: `${cx * 100 - 50}%`,
36
+ top: `${cy * 100 - 50}%`,
37
+ width: b.size,
38
+ height: b.size,
39
+ background: b.gradient,
40
+ filter: "blur(60px)",
41
+ mixBlendMode: "screen",
42
+ pointerEvents: "none",
43
+ }}
44
+ />
45
+ );
46
+ })}
47
+ </AbsoluteFill>
48
+ );
49
+ };
@@ -0,0 +1,72 @@
1
+ import React, { CSSProperties } from "react";
2
+ import { useCurrentFrame } from "remotion";
3
+ import { palette } from "../palette";
4
+
5
+ /**
6
+ * Counter — numeric ticker. Animates from 0 (or fromValue) to target with a
7
+ * power4Out easing curve. Snaps to integers per frame. Scale + blur entrance.
8
+ *
9
+ * 5-digit MONO counters cap at 256px width per house rules — be careful with
10
+ * fontSize × digit-count.
11
+ */
12
+ export const Counter: React.FC<{
13
+ startFrame: number;
14
+ durationFrames?: number;
15
+ fromValue?: number;
16
+ toValue: number;
17
+ prefix?: string;
18
+ suffix?: string;
19
+ fontSize?: number;
20
+ color?: string;
21
+ fontFamily?: string;
22
+ maxWidth?: number;
23
+ reduceMotion?: boolean;
24
+ style?: CSSProperties;
25
+ }> = ({
26
+ startFrame,
27
+ durationFrames = 36,
28
+ fromValue = 0,
29
+ toValue,
30
+ prefix = "",
31
+ suffix = "",
32
+ fontSize = 144,
33
+ color = palette.ink,
34
+ fontFamily = "Geist Mono, monospace",
35
+ maxWidth = 256,
36
+ reduceMotion = false,
37
+ style,
38
+ }) => {
39
+ const frame = useCurrentFrame();
40
+ const local = Math.max(0, frame - startFrame);
41
+ const tRaw = Math.min(1, local / durationFrames);
42
+ // power4Out
43
+ const t = reduceMotion ? 1 : 1 - Math.pow(1 - tRaw, 4);
44
+ const value = reduceMotion ? toValue : Math.round(fromValue + (toValue - fromValue) * t);
45
+ const blur = reduceMotion ? 0 : (1 - t) * 12; // px
46
+ const scale = reduceMotion ? 1 : 0.92 + t * 0.08;
47
+
48
+ return (
49
+ <span
50
+ style={{
51
+ display: "inline-block",
52
+ fontFamily,
53
+ fontSize,
54
+ fontWeight: 700,
55
+ letterSpacing: -2,
56
+ color,
57
+ filter: `blur(${blur}px)`,
58
+ transform: `scale(${scale})`,
59
+ transformOrigin: "center",
60
+ maxWidth,
61
+ overflow: "hidden",
62
+ textOverflow: "ellipsis",
63
+ whiteSpace: "nowrap",
64
+ ...style,
65
+ }}
66
+ >
67
+ {prefix}
68
+ {value.toLocaleString()}
69
+ {suffix}
70
+ </span>
71
+ );
72
+ };
@@ -0,0 +1,59 @@
1
+ import React, { CSSProperties, ReactNode } from "react";
2
+ import { spring, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { palette } from "../palette";
4
+
5
+ /**
6
+ * EyebrowPill — small contextual label that springs in at a beat.
7
+ *
8
+ * Iridescent background, 1.5px border, gentle spring entrance (damping 15,
9
+ * stiffness 80). Used as a "chapter mark" callout above hero text.
10
+ */
11
+ export const EyebrowPill: React.FC<{
12
+ startFrame: number;
13
+ tint?: "violet" | "teal";
14
+ reduceMotion?: boolean;
15
+ style?: CSSProperties;
16
+ children?: ReactNode;
17
+ }> = ({ startFrame, tint = "violet", reduceMotion = false, style, children }) => {
18
+ const frame = useCurrentFrame();
19
+ const { fps } = useVideoConfig();
20
+ const local = Math.max(0, frame - startFrame);
21
+ const t = reduceMotion
22
+ ? 1
23
+ : spring({ frame: local, fps, config: { damping: 15, stiffness: 80 } });
24
+
25
+ const bg = tint === "violet" ? palette.violetPill : palette.tealPill;
26
+
27
+ return (
28
+ <div
29
+ style={{
30
+ display: "inline-flex",
31
+ // Hug content even when parent is a flex/grid that would otherwise
32
+ // stretch us. inline-flex alone is not enough — flex parents apply
33
+ // align-items: stretch by default on the cross axis.
34
+ alignSelf: "flex-start",
35
+ width: "max-content",
36
+ maxWidth: "100%",
37
+ alignItems: "center",
38
+ gap: 8,
39
+ padding: "10px 18px",
40
+ borderRadius: 999,
41
+ background: `${bg}38`, // ~22% alpha
42
+ border: `1.5px solid ${bg}AA`,
43
+ color: palette.ink,
44
+ fontFamily: "Geist, system-ui",
45
+ fontSize: 22,
46
+ fontWeight: 600,
47
+ letterSpacing: 0.5,
48
+ textTransform: "uppercase",
49
+ opacity: t,
50
+ transform: reduceMotion
51
+ ? "none"
52
+ : `translateY(${(1 - t) * 12}px) scale(${0.95 + t * 0.05})`,
53
+ ...style,
54
+ }}
55
+ >
56
+ {children}
57
+ </div>
58
+ );
59
+ };
@@ -0,0 +1,202 @@
1
+ import React, { CSSProperties } from "react";
2
+ import { interpolate, useCurrentFrame } from "remotion";
3
+ import { palette } from "../palette";
4
+
5
+ /**
6
+ * FilmStrip — perforated 35mm film roll with colored play-button thumbnails.
7
+ *
8
+ * Source: ClaudeWatchReel "Type. Paste. Watch." scene (frames 491–717). The
9
+ * centerpiece illustration of a video being scrubbed frame-by-frame.
10
+ *
11
+ * Use when: a reel needs to communicate "video / frame / playback" without
12
+ * dropping in actual footage. Pair with a vertical scan beam (LightBeam) and
13
+ * a BreakdownCard underneath for the full ClaudeWatch deluxe look.
14
+ *
15
+ * Behavior: each tile reveals with translateY(20px) + scale(0.94→1) + opacity
16
+ * 0→1 over 14 frames, staggered by `staggerFrames`. Each tile shows a centered
17
+ * play-triangle. If `playheadSpeed > 0`, a vertical scrubber line sweeps
18
+ * left→right across the strip, looping.
19
+ */
20
+ export const FilmStrip: React.FC<{
21
+ tiles: { color: string; gradient?: string; label?: string }[];
22
+ startFrame?: number;
23
+ staggerFrames?: number;
24
+ playheadSpeed?: number; // 0 disables the playhead; >0 = canvas-px per frame
25
+ style?: CSSProperties;
26
+ }> = ({
27
+ tiles,
28
+ startFrame = 0,
29
+ staggerFrames = 4,
30
+ playheadSpeed = 0,
31
+ style,
32
+ }) => {
33
+ const frame = useCurrentFrame();
34
+ const local = Math.max(0, frame - startFrame);
35
+
36
+ const tileWidth = 140;
37
+ const tileGap = 12;
38
+ const stripPadX = 16;
39
+ const sprocketBand = 16;
40
+ const stripHeight = 200;
41
+
42
+ const stripWidth =
43
+ tiles.length * tileWidth + Math.max(0, tiles.length - 1) * tileGap + stripPadX * 2;
44
+
45
+ // Playhead sweeps left→right and loops
46
+ const innerWidth = stripWidth - stripPadX * 2;
47
+ const playheadX =
48
+ playheadSpeed > 0
49
+ ? stripPadX + ((local * playheadSpeed) % innerWidth)
50
+ : 0;
51
+
52
+ return (
53
+ <div
54
+ style={{
55
+ position: "relative",
56
+ display: "inline-block",
57
+ width: stripWidth,
58
+ height: stripHeight,
59
+ background: palette.ink,
60
+ borderRadius: 6,
61
+ overflow: "hidden",
62
+ boxShadow:
63
+ "0 6px 14px -6px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,255,255,0.08)",
64
+ ...style,
65
+ }}
66
+ >
67
+ {/* Sprocket holes — top */}
68
+ <div
69
+ style={{
70
+ position: "absolute",
71
+ top: 4,
72
+ left: 0,
73
+ right: 0,
74
+ height: 12,
75
+ backgroundImage: `repeating-linear-gradient(90deg, ${palette.bg} 0 14px, transparent 14px 30px)`,
76
+ opacity: 0.85,
77
+ pointerEvents: "none",
78
+ }}
79
+ />
80
+ {/* Sprocket holes — bottom */}
81
+ <div
82
+ style={{
83
+ position: "absolute",
84
+ bottom: 4,
85
+ left: 0,
86
+ right: 0,
87
+ height: 12,
88
+ backgroundImage: `repeating-linear-gradient(90deg, ${palette.bg} 0 14px, transparent 14px 30px)`,
89
+ opacity: 0.85,
90
+ pointerEvents: "none",
91
+ }}
92
+ />
93
+
94
+ {/* Tiles */}
95
+ <div
96
+ style={{
97
+ position: "absolute",
98
+ top: sprocketBand + 6,
99
+ bottom: sprocketBand + 6,
100
+ left: stripPadX,
101
+ right: stripPadX,
102
+ display: "flex",
103
+ gap: tileGap,
104
+ }}
105
+ >
106
+ {tiles.map((tile, i) => {
107
+ const tileLocal = local - i * staggerFrames;
108
+ const t = interpolate(tileLocal, [0, 14], [0, 1], {
109
+ extrapolateLeft: "clamp",
110
+ extrapolateRight: "clamp",
111
+ });
112
+ const scale = 0.94 + t * 0.06;
113
+ const y = (1 - t) * 20;
114
+ const bg =
115
+ tile.gradient ??
116
+ `linear-gradient(135deg, ${palette.inkSoft}, ${tile.color}99)`;
117
+ return (
118
+ <div
119
+ key={i}
120
+ style={{
121
+ width: tileWidth,
122
+ height: "100%",
123
+ borderRadius: 4,
124
+ background: bg,
125
+ boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.18)",
126
+ position: "relative",
127
+ opacity: t,
128
+ transform: `translateY(${y}px) scale(${scale})`,
129
+ willChange: "transform, opacity",
130
+ }}
131
+ >
132
+ {/* Play triangle */}
133
+ <div
134
+ style={{
135
+ position: "absolute",
136
+ top: "50%",
137
+ left: "50%",
138
+ transform: "translate(-50%,-50%)",
139
+ width: 0,
140
+ height: 0,
141
+ borderTop: "10px solid transparent",
142
+ borderBottom: "10px solid transparent",
143
+ borderLeft: "16px solid rgba(255,255,255,0.55)",
144
+ }}
145
+ />
146
+ {tile.label ? (
147
+ <div
148
+ style={{
149
+ position: "absolute",
150
+ bottom: 6,
151
+ left: 8,
152
+ right: 8,
153
+ fontFamily: "Geist Mono, monospace",
154
+ fontSize: 10,
155
+ fontWeight: 600,
156
+ color: "rgba(255,255,255,0.78)",
157
+ letterSpacing: "0.08em",
158
+ textTransform: "uppercase",
159
+ pointerEvents: "none",
160
+ }}
161
+ >
162
+ {tile.label}
163
+ </div>
164
+ ) : null}
165
+ </div>
166
+ );
167
+ })}
168
+ </div>
169
+
170
+ {/* Vertical scrubber playhead */}
171
+ {playheadSpeed > 0 ? (
172
+ <>
173
+ <div
174
+ style={{
175
+ position: "absolute",
176
+ top: -4,
177
+ bottom: -4,
178
+ left: playheadX,
179
+ width: 4,
180
+ background: `linear-gradient(180deg, transparent, ${palette.iriCyan}, transparent)`,
181
+ boxShadow: `0 0 24px ${palette.iriCyan}, 0 0 48px ${palette.iriCyan}88`,
182
+ transform: "translateX(-50%)",
183
+ pointerEvents: "none",
184
+ }}
185
+ />
186
+ <div
187
+ style={{
188
+ position: "absolute",
189
+ top: 0,
190
+ bottom: 0,
191
+ left: playheadX,
192
+ width: 120,
193
+ background: `radial-gradient(ellipse at center, ${palette.iriCyan}33, transparent 60%)`,
194
+ transform: "translateX(-50%)",
195
+ pointerEvents: "none",
196
+ }}
197
+ />
198
+ </>
199
+ ) : null}
200
+ </div>
201
+ );
202
+ };
@@ -0,0 +1,78 @@
1
+ import React, { useMemo } from "react";
2
+ import { useCurrentFrame } from "remotion";
3
+ import { palette } from "../palette";
4
+
5
+ /**
6
+ * FloatingGlyphs — unicode math/code symbols drifting on sine paths.
7
+ *
8
+ * House rule: NEVER emojis. Always unicode math/code symbols.
9
+ * Default set: ± ∞ ∑ ⟨ ⟩ ∫ ≠ ∂ ∇ ⊕ ⊗.
10
+ *
11
+ * Glyphs fade as they approach canvas edges so they don't distract from
12
+ * the hero subject.
13
+ */
14
+ export const FloatingGlyphs: React.FC<{
15
+ count?: number;
16
+ glyphs?: string[];
17
+ fontSize?: number;
18
+ speed?: number;
19
+ color?: string;
20
+ reduceMotion?: boolean;
21
+ }> = ({
22
+ count = 14,
23
+ glyphs = ["±", "∞", "∑", "⟨", "⟩", "∫", "≠", "∂", "∇", "⊕", "⊗"],
24
+ fontSize = 36,
25
+ speed = 0.0009,
26
+ color = palette.inkMuted,
27
+ reduceMotion = false,
28
+ }) => {
29
+ const frame = useCurrentFrame();
30
+ const items = useMemo(() => {
31
+ return Array.from({ length: count }).map((_, i) => {
32
+ const seed = i * 137;
33
+ return {
34
+ glyph: glyphs[i % glyphs.length],
35
+ baseX: ((seed * 53) % 1000) / 1000, // 0..1
36
+ baseY: ((seed * 71) % 1000) / 1000,
37
+ amplitudeX: 0.06 + ((seed * 17) % 100) / 1500,
38
+ amplitudeY: 0.04 + ((seed * 23) % 100) / 1800,
39
+ phase: (i * 0.27) % (Math.PI * 2),
40
+ };
41
+ });
42
+ }, [count, glyphs]);
43
+
44
+ return (
45
+ <div
46
+ style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}
47
+ >
48
+ {items.map((g, i) => {
49
+ const t = frame * speed + g.phase;
50
+ const x = reduceMotion ? g.baseX * 100 : (g.baseX + Math.sin(t * 7 + i) * g.amplitudeX) * 100;
51
+ const y = reduceMotion ? g.baseY * 100 : (g.baseY + Math.cos(t * 5 + i) * g.amplitudeY) * 100;
52
+ // edge fade — fade out within 8% of any edge
53
+ const fade = Math.max(
54
+ 0,
55
+ Math.min(1, (Math.min(x, 100 - x, y, 100 - y) - 4) / 6),
56
+ );
57
+ return (
58
+ <span
59
+ key={i}
60
+ style={{
61
+ position: "absolute",
62
+ left: `${x}%`,
63
+ top: `${y}%`,
64
+ transform: "translate(-50%, -50%)",
65
+ fontFamily: "Geist Mono, monospace",
66
+ fontSize,
67
+ fontWeight: 500,
68
+ color,
69
+ opacity: 0.42 * fade,
70
+ }}
71
+ >
72
+ {g.glyph}
73
+ </span>
74
+ );
75
+ })}
76
+ </div>
77
+ );
78
+ };
@@ -0,0 +1,58 @@
1
+ import React, { CSSProperties, ReactNode } from "react";
2
+ import { palette } from "../palette";
3
+
4
+ /**
5
+ * GlassCard — frosted-glass surface. The Glass family's most-used primitive.
6
+ *
7
+ * Variants:
8
+ * - "default" — backdropFilter blur(16px), rgba 0.42 fill
9
+ * - "strong" — rgba 0.62 fill, blur(20px)
10
+ * - "heavy" — rgba 0.62 fill, blur(32px) saturate(180%) — gstack feel
11
+ *
12
+ * Multi-layer shadow stack reads as printed glass, not flat overlay.
13
+ */
14
+ export const GlassCard: React.FC<{
15
+ variant?: "default" | "strong" | "heavy";
16
+ radius?: number;
17
+ padding?: number | string;
18
+ bezel?: boolean;
19
+ style?: CSSProperties;
20
+ children?: ReactNode;
21
+ }> = ({ variant = "default", radius = 24, padding = 32, bezel = false, style, children }) => {
22
+ const fills = {
23
+ default: palette.glassFill,
24
+ strong: palette.glassFillStrong,
25
+ heavy: palette.glassFillStrong,
26
+ };
27
+ const blurs = {
28
+ default: "blur(16px)",
29
+ strong: "blur(20px)",
30
+ heavy: "blur(32px) saturate(180%)",
31
+ };
32
+ const baseShadows = [
33
+ "0 1px 0 rgba(255,255,255,0.7) inset",
34
+ "0 -1px 0 rgba(255,255,255,0.3) inset",
35
+ "0 8px 24px rgba(20,20,32,0.10)",
36
+ "0 32px 64px rgba(20,20,32,0.14)",
37
+ ];
38
+ // Soft Double-Bezel: inner 1px white border at radius - 1
39
+ const shadows = bezel
40
+ ? ["inset 0 0 0 1px rgba(255,255,255,0.6)", ...baseShadows]
41
+ : baseShadows;
42
+ return (
43
+ <div
44
+ style={{
45
+ background: fills[variant],
46
+ backdropFilter: blurs[variant],
47
+ WebkitBackdropFilter: blurs[variant],
48
+ border: `1.5px solid ${palette.glassBorder}`,
49
+ borderRadius: radius,
50
+ padding,
51
+ boxShadow: shadows.join(", "),
52
+ ...style,
53
+ }}
54
+ >
55
+ {children}
56
+ </div>
57
+ );
58
+ };