@devinilabs/reelstack 1.3.2 → 1.4.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.
@@ -0,0 +1,609 @@
1
+ /**
2
+ * REFERENCE — ResourcesCTAReel (Family: dark)
3
+ *
4
+ * Canonical example of the dark 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/ResourcesCTAReel.tsx
15
+ * Bundled at: 2026-05-12T19:40:04.832Z
16
+ */
17
+ import React from "react";
18
+ import {
19
+ useCurrentFrame,
20
+ useVideoConfig,
21
+ interpolate,
22
+ AbsoluteFill,
23
+ } from "remotion";
24
+ import { gsap } from "gsap";
25
+ import { ds } from "./designSystem";
26
+
27
+ // ═══════════════════════════════════════════════════════════════
28
+ // TOKENS
29
+ // ═══════════════════════════════════════════════════════════════
30
+
31
+ const FONT = ds.font.sans;
32
+ const MONO = ds.font.mono;
33
+
34
+ const C = {
35
+ bg: "#0a0a0b",
36
+ bgLift: "#141416",
37
+ surface: "#1a1a1d",
38
+ surfaceLift: "#222226",
39
+ border: "rgba(255,255,255,0.08)",
40
+ borderLoud: "rgba(255,255,255,0.14)",
41
+ fg: "#f5f5f7",
42
+ fgSoft: "#d1d1d6",
43
+ fgMuted: "#8e8e93",
44
+ fgDim: "#5a5a60",
45
+ yt: "#FF0033",
46
+ ytDeep: "#cc0029",
47
+ ytSoft: "#ff5577",
48
+ ytGlow: "rgba(255,0,51,0.45)",
49
+ gold: "#F0C23A",
50
+ } as const;
51
+
52
+ // ═══════════════════════════════════════════════════════════════
53
+ // GSAP EASES — industry-standard curves via gsap.parseEase
54
+ // ═══════════════════════════════════════════════════════════════
55
+
56
+ const ease = {
57
+ expoOut: gsap.parseEase("expo.out"),
58
+ power4Out: gsap.parseEase("power4.out"),
59
+ power3Out: gsap.parseEase("power3.out"),
60
+ power2InOut: gsap.parseEase("power2.inOut"),
61
+ backOut: gsap.parseEase("back.out(2)"),
62
+ backOutSoft: gsap.parseEase("back.out(1.4)"),
63
+ elasticOut: gsap.parseEase("elastic.out(1, 0.55)"),
64
+ circOut: gsap.parseEase("circ.out"),
65
+ };
66
+
67
+ // Tween driven by gsap ease — maps local frames to an output range
68
+ const tween = (
69
+ frame: number,
70
+ fps: number,
71
+ delaySec: number,
72
+ durSec: number,
73
+ from: number,
74
+ to: number,
75
+ easeFn: (t: number) => number = ease.expoOut,
76
+ ) => {
77
+ const start = delaySec * fps;
78
+ const end = start + durSec * fps;
79
+ return interpolate(frame, [start, end], [from, to], {
80
+ easing: easeFn,
81
+ extrapolateLeft: "clamp",
82
+ extrapolateRight: "clamp",
83
+ });
84
+ };
85
+
86
+ // ═══════════════════════════════════════════════════════════════
87
+ // BACKGROUND — drifting spotlights, grid, grain, vignette
88
+ // ═══════════════════════════════════════════════════════════════
89
+
90
+ const Background: React.FC<{ frame: number }> = ({ frame }) => {
91
+ const drift = (Math.sin(frame * 0.004) + 1) * 0.5;
92
+ const drift2 = (Math.cos(frame * 0.003) + 1) * 0.5;
93
+ const vignettePulse = 0.38 + Math.sin(frame * 0.008) * 0.04;
94
+
95
+ return (
96
+ <AbsoluteFill>
97
+ <div style={{ width: "100%", height: "100%", background: C.bg }} />
98
+ <div
99
+ style={{
100
+ position: "absolute",
101
+ inset: 0,
102
+ background: `radial-gradient(ellipse 820px 920px at ${20 + drift * 55}% ${18 + drift2 * 36}%, rgba(255,0,51,0.15) 0%, transparent 62%)`,
103
+ }}
104
+ />
105
+ <div
106
+ style={{
107
+ position: "absolute",
108
+ inset: 0,
109
+ background: `radial-gradient(ellipse 620px 820px at ${82 - drift * 48}% ${72 + drift2 * 22}%, rgba(255,100,130,0.08) 0%, transparent 55%)`,
110
+ }}
111
+ />
112
+ <div
113
+ style={{
114
+ position: "absolute",
115
+ inset: 0,
116
+ backgroundImage: `
117
+ linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px),
118
+ linear-gradient(90deg, rgba(255,255,255,0.035) 1px, transparent 1px)
119
+ `,
120
+ backgroundSize: "72px 72px",
121
+ maskImage:
122
+ "radial-gradient(ellipse at 50% 50%, black 30%, transparent 85%)",
123
+ WebkitMaskImage:
124
+ "radial-gradient(ellipse at 50% 50%, black 30%, transparent 85%)",
125
+ }}
126
+ />
127
+ <div
128
+ style={{
129
+ position: "absolute",
130
+ inset: 0,
131
+ opacity: 0.06,
132
+ mixBlendMode: "overlay",
133
+ backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9'/></filter><rect width='200' height='200' filter='url(%23n)' opacity='0.7'/></svg>")`,
134
+ }}
135
+ />
136
+ <div
137
+ style={{
138
+ position: "absolute",
139
+ inset: 0,
140
+ background: `radial-gradient(ellipse at 50% 50%, transparent 42%, rgba(0,0,0,${vignettePulse}) 100%)`,
141
+ }}
142
+ />
143
+ </AbsoluteFill>
144
+ );
145
+ };
146
+
147
+ // ═══════════════════════════════════════════════════════════════
148
+ // SAFE ZONE — YouTube Shorts: top ~280px, bottom ~410px
149
+ // ═══════════════════════════════════════════════════════════════
150
+
151
+ const SafeZone: React.FC<{ children: React.ReactNode }> = ({ children }) => (
152
+ <div
153
+ style={{
154
+ position: "absolute",
155
+ top: 280,
156
+ left: 60,
157
+ right: 140,
158
+ bottom: 410,
159
+ }}
160
+ >
161
+ {children}
162
+ </div>
163
+ );
164
+
165
+ // ═══════════════════════════════════════════════════════════════
166
+ // MAIN — 7s Resources + Like/Subscribe CTA
167
+ // ═══════════════════════════════════════════════════════════════
168
+
169
+ export const ResourcesCTAReel: React.FC = () => {
170
+ const frame = useCurrentFrame();
171
+ const { fps } = useVideoConfig();
172
+ const f = frame;
173
+
174
+ // ── Top eyebrow badge ─────────────────────────────────
175
+ const badgeOp = tween(f, fps, 0.05, 0.45, 0, 1, ease.expoOut);
176
+ const badgeY = tween(f, fps, 0.05, 0.6, -30, 0, ease.power4Out);
177
+
178
+ // ── Hero word reveal — "FOR · RESOURCES" ──────────────
179
+ const heroWords = ["FOR", "RESOURCES"];
180
+ const heroDelays = [0.2, 0.35];
181
+
182
+ // ── Secondary "check the" ─────────────────────────────
183
+ const subOp = tween(f, fps, 0.85, 0.45, 0, 1, ease.expoOut);
184
+ const subY = tween(f, fps, 0.85, 0.6, 20, 0, ease.power3Out);
185
+
186
+ // ── "PINNED COMMENT" gradient pop ─────────────────────
187
+ const pinnedScale = tween(f, fps, 1.05, 0.9, 0.6, 1, ease.backOut);
188
+ const pinnedOp = tween(f, fps, 1.05, 0.4, 0, 1, ease.expoOut);
189
+ const pinnedGlow = 0.35 + Math.sin(f * 0.12) * 0.15;
190
+
191
+ // ── Comment bubble icon ───────────────────────────────
192
+ const bubbleScale = tween(f, fps, 1.5, 0.7, 0, 1, ease.elasticOut);
193
+ const bubbleOp = tween(f, fps, 1.5, 0.3, 0, 1, ease.expoOut);
194
+ const bubbleBob = Math.sin(f * 0.14) * 6;
195
+
196
+ // ── Down arrow pointing to pinned area ────────────────
197
+ const arrowOp = tween(f, fps, 2.1, 0.4, 0, 1, ease.expoOut);
198
+ const arrowBounce = Math.sin(f * 0.18) * 10;
199
+
200
+ // ── Divider ───────────────────────────────────────────
201
+ const dividerScale = tween(f, fps, 2.3, 0.6, 0, 1, ease.power4Out);
202
+
203
+ // ── Action pills ──────────────────────────────────────
204
+ const likeOp = tween(f, fps, 2.7, 0.45, 0, 1, ease.expoOut);
205
+ const likeY = tween(f, fps, 2.7, 0.7, 30, 0, ease.backOutSoft);
206
+
207
+ const subscribeOp = tween(f, fps, 3.0, 0.45, 0, 1, ease.expoOut);
208
+ const subscribeY = tween(f, fps, 3.0, 0.7, 30, 0, ease.backOutSoft);
209
+
210
+ // Heart fill + pop at 3.9s
211
+ const heartFilled = f > 3.9 * fps;
212
+ const heartPop = tween(f, fps, 3.9, 0.55, 0, 1, ease.elasticOut);
213
+ const heartScale = heartFilled ? 1 + heartPop * 0.35 - heartPop * 0.25 : 1;
214
+ const heartBurstOp = tween(f, fps, 3.9, 0.8, 1, 0, ease.expoOut);
215
+
216
+ // Subscribe activates at 4.4s — bell ring + label flip
217
+ const subActive = f > 4.4 * fps;
218
+ const bellRingT = tween(f, fps, 4.4, 0.9, 0, 1, ease.power2InOut);
219
+ const bellTilt = Math.sin(bellRingT * Math.PI * 4) * (1 - bellRingT) * 14;
220
+ const subPulse = 0.45 + Math.sin(f * 0.15) * 0.2;
221
+
222
+ return (
223
+ <AbsoluteFill style={{ backgroundColor: C.bg, fontFamily: FONT }}>
224
+ <Background frame={f} />
225
+
226
+ <SafeZone>
227
+ {/* ── EYEBROW BADGE ─────────────────────────────── */}
228
+ <div
229
+ style={{
230
+ position: "absolute",
231
+ top: 0,
232
+ width: "100%",
233
+ display: "flex",
234
+ justifyContent: "center",
235
+ opacity: badgeOp,
236
+ transform: `translateY(${badgeY}px)`,
237
+ }}
238
+ >
239
+ <div
240
+ style={{
241
+ display: "inline-flex",
242
+ alignItems: "center",
243
+ gap: 12,
244
+ padding: "12px 22px",
245
+ borderRadius: 999,
246
+ background: "rgba(255,0,51,0.08)",
247
+ border: `1.5px solid ${C.yt}`,
248
+ boxShadow: `0 0 24px rgba(255,0,51,0.25)`,
249
+ }}
250
+ >
251
+ <div
252
+ style={{
253
+ width: 10,
254
+ height: 10,
255
+ borderRadius: "50%",
256
+ background: C.yt,
257
+ boxShadow: `0 0 12px ${C.yt}`,
258
+ }}
259
+ />
260
+ <span
261
+ style={{
262
+ fontFamily: MONO,
263
+ fontSize: 22,
264
+ fontWeight: 600,
265
+ color: C.fgSoft,
266
+ letterSpacing: "0.18em",
267
+ textTransform: "uppercase",
268
+ }}
269
+ >
270
+ Pinned Comment
271
+ </span>
272
+ </div>
273
+ </div>
274
+
275
+ {/* ── HERO HEADLINE — "FOR RESOURCES" ───────────── */}
276
+ <div
277
+ style={{
278
+ position: "absolute",
279
+ top: 90,
280
+ width: "100%",
281
+ display: "flex",
282
+ flexDirection: "column",
283
+ alignItems: "center",
284
+ gap: 4,
285
+ fontFamily: FONT,
286
+ }}
287
+ >
288
+ {heroWords.map((w, i) => {
289
+ const op = tween(f, fps, heroDelays[i], 0.45, 0, 1, ease.expoOut);
290
+ const y = tween(f, fps, heroDelays[i], 0.7, 60, 0, ease.power4Out);
291
+ const scale = tween(
292
+ f,
293
+ fps,
294
+ heroDelays[i],
295
+ 0.8,
296
+ 0.82,
297
+ 1,
298
+ ease.backOutSoft,
299
+ );
300
+ const isAccent = i === 1;
301
+ return (
302
+ <span
303
+ key={w}
304
+ style={{
305
+ display: "inline-block",
306
+ opacity: op,
307
+ transform: `translateY(${y}px) scale(${scale})`,
308
+ fontSize: isAccent ? 138 : 64,
309
+ fontWeight: isAccent ? 800 : 600,
310
+ letterSpacing: "-0.045em",
311
+ lineHeight: 0.95,
312
+ color: isAccent ? "transparent" : C.fgSoft,
313
+ background: isAccent
314
+ ? `linear-gradient(135deg, ${C.fg} 0%, ${C.fg} 40%, ${C.ytSoft} 100%)`
315
+ : undefined,
316
+ WebkitBackgroundClip: isAccent ? "text" : undefined,
317
+ backgroundClip: isAccent ? "text" : undefined,
318
+ WebkitTextFillColor: isAccent ? "transparent" : undefined,
319
+ filter: isAccent
320
+ ? `drop-shadow(0 0 40px rgba(255,255,255,0.15))`
321
+ : undefined,
322
+ textTransform: i === 0 ? "uppercase" : "none",
323
+ }}
324
+ >
325
+ {w}
326
+ </span>
327
+ );
328
+ })}
329
+ </div>
330
+
331
+ {/* ── "check the" ───────────────────────────────── */}
332
+ <div
333
+ style={{
334
+ position: "absolute",
335
+ top: 320,
336
+ width: "100%",
337
+ textAlign: "center",
338
+ opacity: subOp,
339
+ transform: `translateY(${subY}px)`,
340
+ fontFamily: FONT,
341
+ fontSize: 34,
342
+ fontWeight: 500,
343
+ color: C.fgMuted,
344
+ letterSpacing: "-0.01em",
345
+ }}
346
+ >
347
+ check the
348
+ </div>
349
+
350
+ {/* ── "PINNED COMMENT" gradient ─────────────────── */}
351
+ <div
352
+ style={{
353
+ position: "absolute",
354
+ top: 370,
355
+ width: "100%",
356
+ display: "flex",
357
+ alignItems: "center",
358
+ justifyContent: "center",
359
+ gap: 20,
360
+ opacity: pinnedOp,
361
+ transform: `scale(${pinnedScale})`,
362
+ }}
363
+ >
364
+ {/* Comment bubble icon */}
365
+ <div
366
+ style={{
367
+ width: 88,
368
+ height: 88,
369
+ borderRadius: 20,
370
+ background: `linear-gradient(135deg, ${C.yt}, ${C.ytDeep})`,
371
+ display: "flex",
372
+ alignItems: "center",
373
+ justifyContent: "center",
374
+ boxShadow: `0 14px 34px -8px ${C.ytGlow}, inset 0 1px 0 rgba(255,255,255,0.25)`,
375
+ opacity: bubbleOp,
376
+ transform: `scale(${bubbleScale}) translateY(${bubbleBob}px)`,
377
+ flexShrink: 0,
378
+ }}
379
+ >
380
+ <svg
381
+ width="50"
382
+ height="50"
383
+ viewBox="0 0 24 24"
384
+ fill="none"
385
+ stroke="#fff"
386
+ strokeWidth="2.4"
387
+ strokeLinejoin="round"
388
+ strokeLinecap="round"
389
+ >
390
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
391
+ </svg>
392
+ {/* Pin dot */}
393
+ <div
394
+ style={{
395
+ position: "absolute",
396
+ top: -6,
397
+ right: -6,
398
+ width: 22,
399
+ height: 22,
400
+ borderRadius: "50%",
401
+ background: C.gold,
402
+ border: `3px solid ${C.bg}`,
403
+ boxShadow: `0 0 14px rgba(240,194,58,0.6)`,
404
+ }}
405
+ />
406
+ </div>
407
+
408
+ <span
409
+ style={{
410
+ fontFamily: FONT,
411
+ fontSize: 78,
412
+ fontWeight: 800,
413
+ letterSpacing: "-0.045em",
414
+ lineHeight: 1,
415
+ background: `linear-gradient(135deg, ${C.yt}, ${C.ytSoft})`,
416
+ WebkitBackgroundClip: "text",
417
+ backgroundClip: "text",
418
+ WebkitTextFillColor: "transparent",
419
+ textTransform: "uppercase",
420
+ filter: `drop-shadow(0 0 28px rgba(255,0,51,${pinnedGlow}))`,
421
+ }}
422
+ >
423
+ Pinned
424
+ </span>
425
+ </div>
426
+
427
+ {/* ── Down arrow hint ───────────────────────────── */}
428
+ <div
429
+ style={{
430
+ position: "absolute",
431
+ top: 520,
432
+ width: "100%",
433
+ display: "flex",
434
+ justifyContent: "center",
435
+ opacity: arrowOp,
436
+ transform: `translateY(${arrowBounce}px)`,
437
+ }}
438
+ >
439
+ <svg
440
+ width={48}
441
+ height={48}
442
+ viewBox="0 0 24 24"
443
+ fill="none"
444
+ stroke={C.fgMuted}
445
+ strokeWidth="2.4"
446
+ strokeLinecap="round"
447
+ strokeLinejoin="round"
448
+ >
449
+ <path d="M12 5v14M5 12l7 7 7-7" />
450
+ </svg>
451
+ </div>
452
+
453
+ {/* ── DIVIDER ───────────────────────────────────── */}
454
+ <div
455
+ style={{
456
+ position: "absolute",
457
+ top: 620,
458
+ width: "100%",
459
+ display: "flex",
460
+ justifyContent: "center",
461
+ }}
462
+ >
463
+ <div
464
+ style={{
465
+ height: 2,
466
+ width: `${dividerScale * 60}%`,
467
+ background: `linear-gradient(90deg, transparent, ${C.borderLoud}, transparent)`,
468
+ transformOrigin: "center",
469
+ }}
470
+ />
471
+ </div>
472
+
473
+ {/* ── ACTION PILLS — Like + Subscribe ───────────── */}
474
+ <div
475
+ style={{
476
+ position: "absolute",
477
+ bottom: 20,
478
+ left: 0,
479
+ right: 0,
480
+ display: "flex",
481
+ flexDirection: "column",
482
+ gap: 20,
483
+ }}
484
+ >
485
+ {/* LIKE pill */}
486
+ <div
487
+ style={{
488
+ position: "relative",
489
+ opacity: likeOp,
490
+ transform: `translateY(${likeY}px)`,
491
+ padding: "22px 30px",
492
+ borderRadius: 22,
493
+ background: heartFilled
494
+ ? `linear-gradient(135deg, rgba(255,0,51,0.16), ${C.surface})`
495
+ : C.surface,
496
+ border: `1.5px solid ${heartFilled ? C.yt : C.borderLoud}`,
497
+ display: "flex",
498
+ alignItems: "center",
499
+ gap: 22,
500
+ boxShadow: heartFilled
501
+ ? `0 0 36px -10px ${C.ytGlow}, inset 0 1px 0 rgba(255,255,255,0.05)`
502
+ : `inset 0 1px 0 rgba(255,255,255,0.04)`,
503
+ }}
504
+ >
505
+ {/* Heart burst ring on fill */}
506
+ {heartFilled && (
507
+ <div
508
+ style={{
509
+ position: "absolute",
510
+ left: 38,
511
+ top: "50%",
512
+ width: 90,
513
+ height: 90,
514
+ marginTop: -45,
515
+ borderRadius: "50%",
516
+ border: `2.5px solid ${C.yt}`,
517
+ opacity: heartBurstOp * 0.6,
518
+ transform: `scale(${0.5 + (1 - heartBurstOp) * 1.2})`,
519
+ pointerEvents: "none",
520
+ }}
521
+ />
522
+ )}
523
+ <svg
524
+ width={56}
525
+ height={56}
526
+ viewBox="0 0 24 24"
527
+ fill={heartFilled ? C.yt : "none"}
528
+ stroke={heartFilled ? C.yt : C.fg}
529
+ strokeWidth="2"
530
+ strokeLinejoin="round"
531
+ strokeLinecap="round"
532
+ style={{
533
+ transform: `scale(${heartScale})`,
534
+ flexShrink: 0,
535
+ filter: heartFilled
536
+ ? `drop-shadow(0 0 14px ${C.ytGlow})`
537
+ : undefined,
538
+ }}
539
+ >
540
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
541
+ </svg>
542
+ <span
543
+ style={{
544
+ fontFamily: FONT,
545
+ fontSize: 42,
546
+ fontWeight: 700,
547
+ color: C.fg,
548
+ letterSpacing: "-0.03em",
549
+ flex: 1,
550
+ }}
551
+ >
552
+ {heartFilled ? "Liked" : "Like"}
553
+ </span>
554
+ </div>
555
+
556
+ {/* SUBSCRIBE pill */}
557
+ <div
558
+ style={{
559
+ opacity: subscribeOp,
560
+ transform: `translateY(${subscribeY}px)`,
561
+ padding: "24px 30px",
562
+ borderRadius: 22,
563
+ background: subActive
564
+ ? `linear-gradient(135deg, ${C.yt}, ${C.ytSoft})`
565
+ : `linear-gradient(135deg, ${C.yt}, ${C.ytDeep})`,
566
+ border: `1.5px solid ${subActive ? C.ytSoft : C.yt}`,
567
+ display: "flex",
568
+ alignItems: "center",
569
+ gap: 22,
570
+ boxShadow: `0 20px 44px -12px rgba(255,0,51,${subPulse}), 0 0 60px rgba(255,0,51,${subPulse * 0.4})`,
571
+ }}
572
+ >
573
+ <svg
574
+ width={56}
575
+ height={56}
576
+ viewBox="0 0 24 24"
577
+ fill="none"
578
+ stroke="#fff"
579
+ strokeWidth="2.4"
580
+ strokeLinejoin="round"
581
+ strokeLinecap="round"
582
+ style={{
583
+ transform: `rotate(${bellTilt}deg)`,
584
+ transformOrigin: "50% 20%",
585
+ flexShrink: 0,
586
+ }}
587
+ >
588
+ <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
589
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
590
+ </svg>
591
+ <span
592
+ style={{
593
+ fontFamily: FONT,
594
+ fontSize: 44,
595
+ fontWeight: 800,
596
+ color: "#fff",
597
+ letterSpacing: "-0.03em",
598
+ flex: 1,
599
+ textTransform: "uppercase",
600
+ }}
601
+ >
602
+ {subActive ? "Subscribed" : "Subscribe"}
603
+ </span>
604
+ </div>
605
+ </div>
606
+ </SafeZone>
607
+ </AbsoluteFill>
608
+ );
609
+ };