@devinilabs/reelstack 1.3.2 → 1.4.1

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.
@@ -0,0 +1,599 @@
1
+ /**
2
+ * REFERENCE — ClaudeWatchCTAReel (Family: glass)
3
+ *
4
+ * Canonical example of the glass family's motion vocabulary, frame-locked
5
+ * BEAT structure, and scene choreography. Bundled with ReelStack v1.4+
6
+ * for STUDY and as the source for the scaffold-from-reference flow.
7
+ *
8
+ * Asset imports stripped (look for REFERENCE-STRIP markers). Bring your own
9
+ * voiceover, brand SVGs, captures.
10
+ *
11
+ * License: study + adapt patterns OK. Verbatim re-publication as your own
12
+ * template NOT OK. See ReelStack LICENSE.
13
+ *
14
+ * Source: my-video/src/ClaudeWatchCTAReel.tsx
15
+ * Bundled at: 2026-05-12T19:40:04.826Z
16
+ */
17
+ import React from "react";
18
+ import {
19
+ AbsoluteFill,
20
+ interpolate,
21
+ spring,
22
+ useCurrentFrame,
23
+ } from "remotion";
24
+ import {
25
+ C,
26
+ CausticBlobs,
27
+ EyebrowPill,
28
+ FONT,
29
+ FloatingGlyphs,
30
+ GLASS_SHADOW,
31
+ LightBeam,
32
+ MONO,
33
+ ParticleBurst,
34
+ SonarRings,
35
+ ease,
36
+ glassBase,
37
+ } from "./ClaudeWatchReel";
38
+
39
+ // 240 frames @ 30fps = 8s
40
+ export const CWCTA_TOTAL = 240;
41
+
42
+ const SAFE_TOP = 290;
43
+ const SAFE_BOTTOM = 410;
44
+
45
+ const BEATS = {
46
+ b1: 0, // 0.0s — atmosphere & eyebrow
47
+ b2: 30, // 1.0s — hero word stagger
48
+ b3: 105, // 3.5s — sub-line + glass play disc
49
+ b4: 165, // 5.5s — description pill
50
+ b5: 225, // 7.5s — "Start building." kicker
51
+ } as const;
52
+
53
+ // ───────────────────────────────────────────────────────────
54
+ // Frame-based tween (matches the pattern used in ClaudeWatchReel)
55
+ // ───────────────────────────────────────────────────────────
56
+ const tween = (
57
+ frame: number,
58
+ startFrame: number,
59
+ duration: number,
60
+ from: number,
61
+ to: number,
62
+ easeFn: (t: number) => number = ease.expoOut,
63
+ ) =>
64
+ interpolate(frame, [startFrame, startFrame + duration], [from, to], {
65
+ extrapolateLeft: "clamp",
66
+ extrapolateRight: "clamp",
67
+ easing: easeFn,
68
+ });
69
+
70
+ // ───────────────────────────────────────────────────────────
71
+ // HERO LINE — single line with per-word stagger (fade-up + blur)
72
+ // Variant: optional emphasis word recolored & scaled-overshoot.
73
+ // ───────────────────────────────────────────────────────────
74
+ const HeroLine: React.FC<{
75
+ text: string;
76
+ startFrame: number;
77
+ fontSize: number;
78
+ fontWeight?: number;
79
+ color?: string;
80
+ letterSpacing?: string;
81
+ italic?: boolean;
82
+ perWordDelay?: number;
83
+ duration?: number;
84
+ emphasisOvershoot?: boolean;
85
+ }> = ({
86
+ text,
87
+ startFrame,
88
+ fontSize,
89
+ fontWeight = 800,
90
+ color = C.ink,
91
+ letterSpacing = "-0.04em",
92
+ italic = false,
93
+ perWordDelay = 5,
94
+ duration = 22,
95
+ emphasisOvershoot = false,
96
+ }) => {
97
+ const frame = useCurrentFrame();
98
+ const words = text.split(" ");
99
+ return (
100
+ <div
101
+ style={{
102
+ display: "flex",
103
+ flexWrap: "nowrap",
104
+ gap: "0.28em",
105
+ justifyContent: "center",
106
+ fontFamily: FONT,
107
+ fontSize,
108
+ fontWeight,
109
+ color,
110
+ letterSpacing,
111
+ lineHeight: 1.02,
112
+ fontStyle: italic ? "italic" : "normal",
113
+ whiteSpace: "nowrap",
114
+ }}
115
+ >
116
+ {words.map((w, i) => {
117
+ const wordStart = startFrame + i * perWordDelay;
118
+ const local = frame - wordStart;
119
+ const op = interpolate(local, [0, duration], [0, 1], {
120
+ extrapolateLeft: "clamp",
121
+ extrapolateRight: "clamp",
122
+ easing: ease.expoOut,
123
+ });
124
+ const y = interpolate(local, [0, duration], [38, 0], {
125
+ extrapolateLeft: "clamp",
126
+ extrapolateRight: "clamp",
127
+ easing: ease.power3Out,
128
+ });
129
+ const blur = interpolate(local, [0, duration], [10, 0], {
130
+ extrapolateLeft: "clamp",
131
+ extrapolateRight: "clamp",
132
+ });
133
+ // Emphasis overshoot bumps the LAST word past 1.0 then settles
134
+ const isLast = i === words.length - 1;
135
+ const baseScale = interpolate(local, [0, duration], [0.92, 1], {
136
+ extrapolateLeft: "clamp",
137
+ extrapolateRight: "clamp",
138
+ easing: ease.expoOut,
139
+ });
140
+ const overshoot = emphasisOvershoot && isLast
141
+ ? interpolate(local, [duration, duration + 8, duration + 18], [1, 1.06, 1], {
142
+ extrapolateLeft: "clamp",
143
+ extrapolateRight: "clamp",
144
+ easing: ease.power3Out,
145
+ })
146
+ : 1;
147
+ return (
148
+ <span
149
+ key={i}
150
+ style={{
151
+ display: "inline-block",
152
+ opacity: op,
153
+ transform: `translateY(${y}px) scale(${baseScale * overshoot})`,
154
+ filter: `blur(${blur}px)`,
155
+ willChange: "transform, opacity, filter",
156
+ }}
157
+ >
158
+ {w}
159
+ </span>
160
+ );
161
+ })}
162
+ </div>
163
+ );
164
+ };
165
+
166
+ // ───────────────────────────────────────────────────────────
167
+ // GLASS PLAY DISC — 240px disc with ▶ triangle + dual sonar
168
+ // ───────────────────────────────────────────────────────────
169
+ const GlassPlayDisc: React.FC<{ startFrame: number }> = ({ startFrame }) => {
170
+ const frame = useCurrentFrame();
171
+ const local = frame - startFrame;
172
+
173
+ // Spring entrance with overshoot
174
+ const entrance = spring({
175
+ frame: local,
176
+ fps: 30,
177
+ config: { damping: 11, stiffness: 130 },
178
+ from: 0,
179
+ to: 1,
180
+ });
181
+ const op = interpolate(local, [0, 18], [0, 1], {
182
+ extrapolateLeft: "clamp",
183
+ extrapolateRight: "clamp",
184
+ });
185
+
186
+ // Subtle scale-breathe (±2% at ~0.5 Hz)
187
+ const breathe = 1 + Math.sin(frame * 0.06) * 0.02;
188
+ // Outro kick at B5 — extra 4% pop on the last 15 frames
189
+ const kickStart = BEATS.b5;
190
+ const kick = interpolate(frame, [kickStart, kickStart + 8, kickStart + 15], [1, 1.04, 1], {
191
+ extrapolateLeft: "clamp",
192
+ extrapolateRight: "clamp",
193
+ easing: ease.expoOut,
194
+ });
195
+
196
+ const size = 240;
197
+ return (
198
+ <div
199
+ style={{
200
+ position: "absolute",
201
+ top: 820,
202
+ left: "50%",
203
+ width: size,
204
+ height: size,
205
+ marginLeft: -size / 2,
206
+ opacity: op,
207
+ transform: `scale(${entrance * breathe * kick})`,
208
+ willChange: "transform, opacity",
209
+ }}
210
+ >
211
+ {/* Sonar layer 1 — claude coral */}
212
+ <div
213
+ style={{
214
+ position: "absolute",
215
+ inset: 0,
216
+ pointerEvents: "none",
217
+ }}
218
+ >
219
+ <DiscSonar startFrame={startFrame + 6} accent={C.claude} period={70} />
220
+ </div>
221
+ {/* Sonar layer 2 — vector indigo, offset phase */}
222
+ <div
223
+ style={{
224
+ position: "absolute",
225
+ inset: 0,
226
+ pointerEvents: "none",
227
+ }}
228
+ >
229
+ <DiscSonar startFrame={startFrame + 30} accent={C.vector} period={70} />
230
+ </div>
231
+
232
+ {/* The disc itself */}
233
+ <div
234
+ style={{
235
+ position: "absolute",
236
+ inset: 0,
237
+ borderRadius: "50%",
238
+ background: C.glassFillStrong,
239
+ backdropFilter: "blur(32px) saturate(180%)",
240
+ WebkitBackdropFilter: "blur(32px) saturate(180%)",
241
+ border: `1.5px solid ${C.glassBorder}`,
242
+ boxShadow: GLASS_SHADOW,
243
+ display: "flex",
244
+ alignItems: "center",
245
+ justifyContent: "center",
246
+ }}
247
+ >
248
+ {/* ▶ triangle — CSS border, optically nudged right */}
249
+ <div
250
+ style={{
251
+ width: 0,
252
+ height: 0,
253
+ borderLeft: `66px solid ${C.claude}`,
254
+ borderTop: "44px solid transparent",
255
+ borderBottom: "44px solid transparent",
256
+ marginLeft: 16,
257
+ filter: `drop-shadow(0 6px 16px ${C.claude}55)`,
258
+ }}
259
+ />
260
+ </div>
261
+ </div>
262
+ );
263
+ };
264
+
265
+ // Sonar pulse anchored to the disc center (not the canvas center)
266
+ const DiscSonar: React.FC<{ startFrame: number; accent: string; period: number }> = ({
267
+ startFrame,
268
+ accent,
269
+ period,
270
+ }) => {
271
+ const frame = useCurrentFrame();
272
+ const local = frame - startFrame;
273
+ if (local < 0) return null;
274
+ const rings = [0, period * 0.33, period * 0.66];
275
+ return (
276
+ <>
277
+ {rings.map((birth, i) => {
278
+ const ringLocal = local - birth;
279
+ if (ringLocal < 0) return null;
280
+ const cycle = ringLocal % period;
281
+ const scale = interpolate(cycle, [0, period], [0.5, 1.9], {
282
+ extrapolateLeft: "clamp",
283
+ extrapolateRight: "clamp",
284
+ easing: ease.expoOut,
285
+ });
286
+ const op = interpolate(cycle, [0, period * 0.3, period], [0, 0.32, 0], {
287
+ extrapolateLeft: "clamp",
288
+ extrapolateRight: "clamp",
289
+ });
290
+ return (
291
+ <div
292
+ key={i}
293
+ style={{
294
+ position: "absolute",
295
+ inset: 0,
296
+ borderRadius: "50%",
297
+ border: `2px solid ${accent}`,
298
+ transform: `scale(${scale})`,
299
+ opacity: op,
300
+ willChange: "transform, opacity",
301
+ }}
302
+ />
303
+ );
304
+ })}
305
+ </>
306
+ );
307
+ };
308
+
309
+ // ───────────────────────────────────────────────────────────
310
+ // DESCRIPTION PILL — bottom glass card with arrow caption above
311
+ // ───────────────────────────────────────────────────────────
312
+ const DescriptionPill: React.FC<{ startFrame: number }> = ({ startFrame }) => {
313
+ const frame = useCurrentFrame();
314
+ const local = frame - startFrame;
315
+
316
+ const entrance = spring({
317
+ frame: local,
318
+ fps: 30,
319
+ config: { damping: 14, stiffness: 140 },
320
+ from: 0,
321
+ to: 1,
322
+ });
323
+ const op = interpolate(local, [0, 15], [0, 1], {
324
+ extrapolateLeft: "clamp",
325
+ extrapolateRight: "clamp",
326
+ });
327
+ const y = interpolate(local, [0, 22], [40, 0], {
328
+ extrapolateLeft: "clamp",
329
+ extrapolateRight: "clamp",
330
+ easing: ease.power3Out,
331
+ });
332
+
333
+ // Caption pulse (arrows nudge down rhythmically)
334
+ const arrowBob = Math.sin(frame * 0.15) * 6;
335
+ const captionOp = interpolate(local, [0, 18], [0, 1], {
336
+ extrapolateLeft: "clamp",
337
+ extrapolateRight: "clamp",
338
+ });
339
+
340
+ return (
341
+ <div
342
+ style={{
343
+ position: "absolute",
344
+ left: 60,
345
+ right: 60,
346
+ bottom: SAFE_BOTTOM + 20,
347
+ display: "flex",
348
+ flexDirection: "column",
349
+ alignItems: "center",
350
+ gap: 22,
351
+ transform: `translateY(${y}px) scale(${0.96 + entrance * 0.04})`,
352
+ opacity: op,
353
+ }}
354
+ >
355
+ {/* Mono caption with bobbing arrows */}
356
+ <div
357
+ style={{
358
+ fontFamily: MONO,
359
+ fontSize: 22,
360
+ fontWeight: 500,
361
+ letterSpacing: "0.22em",
362
+ textTransform: "uppercase",
363
+ color: C.inkSoft,
364
+ opacity: captionOp,
365
+ display: "flex",
366
+ alignItems: "center",
367
+ gap: 14,
368
+ }}
369
+ >
370
+ <span style={{ transform: `translateY(${arrowBob}px)`, color: C.claude }}>↓</span>
371
+ <span>Tap the description</span>
372
+ <span style={{ transform: `translateY(${arrowBob}px)`, color: C.claude }}>↓</span>
373
+ </div>
374
+
375
+ {/* Glass pill with the message */}
376
+ <div
377
+ style={{
378
+ ...glassBase,
379
+ borderRadius: 32,
380
+ padding: "26px 44px",
381
+ textAlign: "center",
382
+ fontFamily: FONT,
383
+ fontSize: 38,
384
+ fontWeight: 600,
385
+ color: C.ink,
386
+ letterSpacing: "-0.01em",
387
+ lineHeight: 1.18,
388
+ maxWidth: 880,
389
+ }}
390
+ >
391
+ Complete toolkit in <span style={{ color: C.vector, fontWeight: 700 }}>the description</span>
392
+ </div>
393
+ </div>
394
+ );
395
+ };
396
+
397
+ // ───────────────────────────────────────────────────────────
398
+ // VIGNETTE — slow inward darkening
399
+ // ───────────────────────────────────────────────────────────
400
+ const Vignette: React.FC = () => {
401
+ const frame = useCurrentFrame();
402
+ const op = interpolate(frame, [0, 60, BEATS.b5, CWCTA_TOTAL], [0.2, 0.32, 0.32, 0.5], {
403
+ extrapolateLeft: "clamp",
404
+ extrapolateRight: "clamp",
405
+ });
406
+ return (
407
+ <AbsoluteFill
408
+ style={{
409
+ background:
410
+ "radial-gradient(ellipse at 50% 50%, transparent 35%, rgba(20,15,28,1) 100%)",
411
+ opacity: op,
412
+ pointerEvents: "none",
413
+ }}
414
+ />
415
+ );
416
+ };
417
+
418
+ // ───────────────────────────────────────────────────────────
419
+ // MAIN COMPONENT
420
+ // ───────────────────────────────────────────────────────────
421
+ export const ClaudeWatchCTAReel: React.FC = () => {
422
+ const frame = useCurrentFrame();
423
+
424
+ // ─── B2 hero word starts (3 stacked lines)
425
+ const heroStart = BEATS.b2;
426
+ const line1Start = heroStart; // "CATCH THE" (96px, 800)
427
+ const line2Start = heroStart + 18; // "FULL VIDEO" (132px, 800)
428
+ const line3Start = heroStart + 38; // "BELOW" (168px, 800 italic, claude)
429
+
430
+ // ─── B3 sub-line "Dive into this custom skill"
431
+ const subStart = BEATS.b3 + 14;
432
+
433
+ // ─── B5 "Start building." italic kicker
434
+ const kickerLocal = frame - BEATS.b5;
435
+ const kickerScale = spring({
436
+ frame: kickerLocal,
437
+ fps: 30,
438
+ config: { damping: 10, stiffness: 130 },
439
+ from: 0.6,
440
+ to: 1,
441
+ });
442
+ const kickerOp = interpolate(kickerLocal, [0, 14], [0, 1], {
443
+ extrapolateLeft: "clamp",
444
+ extrapolateRight: "clamp",
445
+ });
446
+ const kickerY = interpolate(kickerLocal, [0, 22], [22, 0], {
447
+ extrapolateLeft: "clamp",
448
+ extrapolateRight: "clamp",
449
+ easing: ease.power3Out,
450
+ });
451
+
452
+ // ─── Eyebrow pill spring
453
+ const eyebrowSpring = spring({
454
+ frame: frame - 4,
455
+ fps: 30,
456
+ config: { damping: 14, stiffness: 140 },
457
+ from: 0,
458
+ to: 1,
459
+ });
460
+ const eyebrowOp = interpolate(frame, [0, 18], [0, 1], {
461
+ extrapolateLeft: "clamp",
462
+ extrapolateRight: "clamp",
463
+ });
464
+ const eyebrowY = interpolate(frame, [0, 24], [24, 0], {
465
+ extrapolateLeft: "clamp",
466
+ extrapolateRight: "clamp",
467
+ easing: ease.power3Out,
468
+ });
469
+
470
+ return (
471
+ <AbsoluteFill style={{ background: C.bg, overflow: "hidden" }}>
472
+ {/* Always-on atmosphere */}
473
+ <CausticBlobs />
474
+ <FloatingGlyphs />
475
+
476
+ {/* B2 emphasis — particle burst behind the headline */}
477
+ {frame >= BEATS.b2 + 40 && frame < BEATS.b2 + 130 && (
478
+ <ParticleBurst count={48} palette={[C.claude, C.film, C.vector, C.plasma]} />
479
+ )}
480
+
481
+ {/* B2 light beam sweep #1 (upper third) */}
482
+ {frame >= BEATS.b2 && frame < BEATS.b2 + 70 && (
483
+ <LightBeam delay={BEATS.b2 + 8} angle={-6} />
484
+ )}
485
+
486
+ {/* B4 light beam sweep #2 (opposite diagonal) */}
487
+ {frame >= BEATS.b4 && frame < BEATS.b4 + 70 && (
488
+ <LightBeam delay={BEATS.b4 + 6} angle={10} />
489
+ )}
490
+
491
+ {/* B4 emphasis — particle burst behind the description pill */}
492
+ {frame >= BEATS.b4 + 6 && frame < BEATS.b4 + 90 && (
493
+ <ParticleBurst count={36} palette={[C.vector, C.plasma, C.claude, C.film]} />
494
+ )}
495
+
496
+ {/* Background sonar around the whole canvas (subtle) */}
497
+ <div style={{ opacity: 0.55 }}>
498
+ <SonarRings accent={C.claude} secondary={C.vector} />
499
+ </div>
500
+
501
+ {/* Eyebrow pill — top */}
502
+ <div
503
+ style={{
504
+ position: "absolute",
505
+ top: SAFE_TOP - 50,
506
+ left: 0,
507
+ right: 0,
508
+ display: "flex",
509
+ justifyContent: "center",
510
+ opacity: eyebrowOp,
511
+ transform: `translateY(${eyebrowY - (1 - eyebrowSpring) * 6}px)`,
512
+ }}
513
+ >
514
+ <EyebrowPill dot={C.claude}>Watch full video</EyebrowPill>
515
+ </div>
516
+
517
+ {/* Hero block — 3 stacked lines, centered horizontally */}
518
+ <div
519
+ style={{
520
+ position: "absolute",
521
+ top: 400,
522
+ left: 0,
523
+ right: 0,
524
+ display: "flex",
525
+ flexDirection: "column",
526
+ alignItems: "center",
527
+ gap: 6,
528
+ }}
529
+ >
530
+ <HeroLine text="Catch the" startFrame={line1Start} fontSize={96} fontWeight={800} />
531
+ <HeroLine text="full video" startFrame={line2Start} fontSize={132} fontWeight={800} letterSpacing="-0.045em" />
532
+ <HeroLine
533
+ text="below"
534
+ startFrame={line3Start}
535
+ fontSize={168}
536
+ fontWeight={800}
537
+ color={C.claude}
538
+ italic
539
+ letterSpacing="-0.05em"
540
+ emphasisOvershoot
541
+ />
542
+ </div>
543
+
544
+ {/* Glass play disc + dual sonar (B3 onwards) */}
545
+ {frame >= BEATS.b3 && <GlassPlayDisc startFrame={BEATS.b3} />}
546
+
547
+ {/* Sub-line below the disc */}
548
+ <div
549
+ style={{
550
+ position: "absolute",
551
+ top: 1100,
552
+ left: 0,
553
+ right: 0,
554
+ textAlign: "center",
555
+ }}
556
+ >
557
+ <HeroLine
558
+ text="Dive into this custom skill"
559
+ startFrame={subStart}
560
+ fontSize={42}
561
+ fontWeight={600}
562
+ letterSpacing="-0.01em"
563
+ color={C.inkSoft}
564
+ perWordDelay={3}
565
+ duration={18}
566
+ />
567
+ </div>
568
+
569
+ {/* B5 "Start building." italic kicker */}
570
+ {frame >= BEATS.b5 && (
571
+ <div
572
+ style={{
573
+ position: "absolute",
574
+ top: 1230,
575
+ left: 0,
576
+ right: 0,
577
+ textAlign: "center",
578
+ fontFamily: FONT,
579
+ fontSize: 56,
580
+ fontWeight: 700,
581
+ fontStyle: "italic",
582
+ color: C.ink,
583
+ letterSpacing: "-0.02em",
584
+ opacity: kickerOp,
585
+ transform: `translateY(${kickerY}px) scale(${kickerScale})`,
586
+ }}
587
+ >
588
+ Start building.
589
+ </div>
590
+ )}
591
+
592
+ {/* B4 description pill (bottom) */}
593
+ {frame >= BEATS.b4 && <DescriptionPill startFrame={BEATS.b4} />}
594
+
595
+ {/* Slow inward vignette */}
596
+ <Vignette />
597
+ </AbsoluteFill>
598
+ );
599
+ };