@devinilabs/reelstack 1.3.1 → 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,908 @@
1
+ /**
2
+ * REFERENCE — CodeDropReel (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/CodeDropReel.tsx
15
+ * Bundled at: 2026-05-12T19:40:04.829Z
16
+ */
17
+ import {
18
+ useCurrentFrame,
19
+ useVideoConfig,
20
+ interpolate,
21
+ spring,
22
+ AbsoluteFill,
23
+ Img,
24
+ staticFile,
25
+ } from "remotion";
26
+ import { ds } from "./designSystem";
27
+
28
+ // ═══════════════════════════════════════════════════════════════
29
+ // TOKENS — matched to ClaudeDispatchReel for family consistency
30
+ // ═══════════════════════════════════════════════════════════════
31
+
32
+ const FONT = ds.font.sans;
33
+ const MONO = ds.font.mono;
34
+
35
+ const C = {
36
+ bg: "#0a0a0b",
37
+ surface: "#1a1a1d",
38
+ border: "rgba(255,255,255,0.08)",
39
+ borderLoud: "rgba(255,255,255,0.14)",
40
+ fg: "#f5f5f7",
41
+ fgSoft: "#d1d1d6",
42
+ fgMuted: "#8e8e93",
43
+ fgDim: "#5a5a60",
44
+ claude: "#D4663A",
45
+ claudeSoft: "#e07a54",
46
+ claudeDim: "rgba(212,102,58,0.15)",
47
+ danger: "#e25822",
48
+ dangerSoft: "#ff6a45",
49
+ safe: "#4fc46a",
50
+ safeDim: "rgba(79,196,106,0.12)",
51
+ } as const;
52
+
53
+ // Blank 0-20s (user overlays website clips), Claude pitch 20-26s, CTA 26-37s.
54
+ const BLANK_END_SEC = 20;
55
+ const BUILD_START_SEC = 20;
56
+ const BUILD_END_SEC = 26;
57
+ const CTA_START_SEC = 26;
58
+ const CTA_END_SEC = 37;
59
+
60
+ // ═══════════════════════════════════════════════════════════════
61
+ // BACKGROUND — same drifting spotlights + grid as previous reels
62
+ // ═══════════════════════════════════════════════════════════════
63
+
64
+ const Background: React.FC<{ frame: number }> = ({ frame }) => {
65
+ const drift = (Math.sin(frame * 0.004) + 1) * 0.5;
66
+ const drift2 = (Math.cos(frame * 0.003) + 1) * 0.5;
67
+ const vignettePulse = 0.35 + Math.sin(frame * 0.008) * 0.04;
68
+
69
+ return (
70
+ <AbsoluteFill>
71
+ <div style={{ width: "100%", height: "100%", background: C.bg }} />
72
+ <div
73
+ style={{
74
+ position: "absolute",
75
+ inset: 0,
76
+ background: `radial-gradient(ellipse 800px 900px at ${20 + drift * 60}% ${15 + drift2 * 40}%, rgba(212,102,58,0.14) 0%, transparent 60%)`,
77
+ }}
78
+ />
79
+ <div
80
+ style={{
81
+ position: "absolute",
82
+ inset: 0,
83
+ background: `radial-gradient(ellipse 600px 800px at ${80 - drift * 50}% ${70 + drift2 * 25}%, rgba(79,196,106,0.06) 0%, transparent 55%)`,
84
+ }}
85
+ />
86
+ <div
87
+ style={{
88
+ position: "absolute",
89
+ inset: 0,
90
+ backgroundImage: `
91
+ linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px),
92
+ linear-gradient(90deg, rgba(255,255,255,0.035) 1px, transparent 1px)
93
+ `,
94
+ backgroundSize: "72px 72px",
95
+ maskImage:
96
+ "radial-gradient(ellipse at 50% 50%, black 30%, transparent 85%)",
97
+ WebkitMaskImage:
98
+ "radial-gradient(ellipse at 50% 50%, black 30%, transparent 85%)",
99
+ }}
100
+ />
101
+ <div
102
+ style={{
103
+ position: "absolute",
104
+ inset: 0,
105
+ opacity: 0.06,
106
+ mixBlendMode: "overlay",
107
+ 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>")`,
108
+ }}
109
+ />
110
+ <div
111
+ style={{
112
+ position: "absolute",
113
+ inset: 0,
114
+ background: `radial-gradient(ellipse at 50% 50%, transparent 40%, rgba(0,0,0,${vignettePulse}) 100%)`,
115
+ }}
116
+ />
117
+ </AbsoluteFill>
118
+ );
119
+ };
120
+
121
+ // ═══════════════════════════════════════════════════════════════
122
+ // SAFE ZONE — IG reel: reserve top 290px + bottom 430px
123
+ // ═══════════════════════════════════════════════════════════════
124
+
125
+ const SafeZone: React.FC<{ children: React.ReactNode }> = ({ children }) => (
126
+ <div
127
+ style={{
128
+ position: "absolute",
129
+ top: 290,
130
+ left: 0,
131
+ right: 0,
132
+ bottom: 430,
133
+ }}
134
+ >
135
+ {children}
136
+ </div>
137
+ );
138
+
139
+ // ═══════════════════════════════════════════════════════════════
140
+ // SPRING HELPER (shared by all scenes)
141
+ // ═══════════════════════════════════════════════════════════════
142
+
143
+ const gs = (
144
+ frame: number,
145
+ fps: number,
146
+ delaySec: number,
147
+ kind: "bouncy" | "snappy" | "gentle" | "glass" = "snappy",
148
+ ) =>
149
+ spring({
150
+ frame,
151
+ fps,
152
+ delay: Math.round(delaySec * fps),
153
+ config: ds.spring[kind],
154
+ });
155
+
156
+ // ═══════════════════════════════════════════════════════════════
157
+ // BUILD SCENE (20s → 26s) — "Build Within an Hour using Claude Skill"
158
+ // ═══════════════════════════════════════════════════════════════
159
+
160
+ const BuildScene: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
161
+ const startF = BUILD_START_SEC * fps;
162
+ const endF = BUILD_END_SEC * fps;
163
+ if (frame < startF || frame >= endF) return null;
164
+ const localF = frame - startF;
165
+ const totalLocal = endF - startF;
166
+
167
+ // Scene fade in/out
168
+ const sceneOp = interpolate(
169
+ localF,
170
+ [0, 0.4 * fps, totalLocal - 0.5 * fps, totalLocal],
171
+ [0, 1, 1, 0],
172
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
173
+ );
174
+
175
+ // Logo entry — gentle scale + glow pulse
176
+ const logoSpring = gs(localF, fps, 0, "glass");
177
+ const logoScale = interpolate(logoSpring, [0, 1], [0.6, 1]);
178
+ const logoOp = interpolate(logoSpring, [0, 0.4], [0, 1], {
179
+ extrapolateLeft: "clamp",
180
+ extrapolateRight: "clamp",
181
+ });
182
+ const logoGlow = 0.3 + Math.sin(localF * 0.08) * 0.15;
183
+
184
+ // "POWERED BY CLAUDE" badge
185
+ const badgeSpring = gs(localF, fps, 0.3, "snappy");
186
+ const badgeOp = interpolate(badgeSpring, [0, 0.4], [0, 1], {
187
+ extrapolateLeft: "clamp",
188
+ extrapolateRight: "clamp",
189
+ });
190
+ const badgeY = interpolate(badgeSpring, [0, 1], [12, 0]);
191
+
192
+ // Headline word reveal: "Build Within An Hour"
193
+ const headWords = ["Build", "Within", "An", "Hour"];
194
+
195
+ // Subhead
196
+ const subSpring = gs(localF, fps, 1.8, "gentle");
197
+ const subOp = interpolate(subSpring, [0, 0.4], [0, 1], {
198
+ extrapolateLeft: "clamp",
199
+ extrapolateRight: "clamp",
200
+ });
201
+ const subY = interpolate(subSpring, [0, 1], [22, 0]);
202
+
203
+ // Stat chips entry
204
+ const chipDelays = [2.4, 2.6, 2.8];
205
+
206
+ // Highlight chip pulse on "Hour"
207
+ const hourPulse = 1 + Math.sin(localF * 0.12) * 0.04;
208
+
209
+ return (
210
+ <AbsoluteFill style={{ opacity: sceneOp }}>
211
+ <SafeZone>
212
+ {/* ── CLAUDE LOGO + BADGE ─────────────────── */}
213
+ <div
214
+ style={{
215
+ position: "absolute",
216
+ top: 0,
217
+ width: "100%",
218
+ display: "flex",
219
+ flexDirection: "column",
220
+ alignItems: "center",
221
+ gap: 22,
222
+ }}
223
+ >
224
+ {/* Logo with halo */}
225
+ <div
226
+ style={{
227
+ position: "relative",
228
+ opacity: logoOp,
229
+ transform: `scale(${logoScale})`,
230
+ width: 160,
231
+ height: 160,
232
+ display: "flex",
233
+ alignItems: "center",
234
+ justifyContent: "center",
235
+ }}
236
+ >
237
+ <div
238
+ style={{
239
+ position: "absolute",
240
+ inset: -30,
241
+ background: `radial-gradient(circle, rgba(212,102,58,${logoGlow}) 0%, transparent 60%)`,
242
+ filter: "blur(12px)",
243
+ }}
244
+ />
245
+ <div
246
+ style={{
247
+ position: "relative",
248
+ width: 160,
249
+ height: 160,
250
+ borderRadius: 36,
251
+ background: `linear-gradient(135deg, #1a1a1d, #0f0f12)`,
252
+ border: `1.5px solid rgba(212,102,58,0.4)`,
253
+ display: "flex",
254
+ alignItems: "center",
255
+ justifyContent: "center",
256
+ boxShadow: `0 20px 50px -10px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.08)`,
257
+ overflow: "hidden",
258
+ }}
259
+ >
260
+ <Img
261
+ src={staticFile("claude-ai-icon.webp")}
262
+ style={{
263
+ width: 130,
264
+ height: 130,
265
+ objectFit: "contain",
266
+ filter: "drop-shadow(0 0 14px rgba(212,102,58,0.55))",
267
+ }}
268
+ />
269
+ </div>
270
+ </div>
271
+
272
+ {/* Claude Code badge */}
273
+ <div
274
+ style={{
275
+ opacity: badgeOp,
276
+ transform: `translateY(${badgeY}px)`,
277
+ padding: "12px 26px",
278
+ borderRadius: 9999,
279
+ background: "rgba(212,102,58,0.14)",
280
+ border: "1.5px solid rgba(212,102,58,0.5)",
281
+ fontFamily: MONO,
282
+ fontSize: 26,
283
+ fontWeight: 700,
284
+ color: C.claudeSoft,
285
+ letterSpacing: "0.08em",
286
+ textTransform: "uppercase",
287
+ }}
288
+ >
289
+ Claude Code
290
+ </div>
291
+ </div>
292
+
293
+ {/* ── HEADLINE — "Build Within An Hour" ───────── */}
294
+ <div
295
+ style={{
296
+ position: "absolute",
297
+ top: 410,
298
+ width: "100%",
299
+ textAlign: "center",
300
+ padding: "0 50px",
301
+ display: "flex",
302
+ flexWrap: "wrap",
303
+ gap: "0 0.32em",
304
+ justifyContent: "center",
305
+ fontFamily: FONT,
306
+ fontSize: 96,
307
+ fontWeight: 700,
308
+ color: C.fg,
309
+ letterSpacing: "-0.04em",
310
+ lineHeight: 1.0,
311
+ }}
312
+ >
313
+ {headWords.map((w, i) => {
314
+ const s = gs(localF, fps, 0.7 + i * 0.1, "bouncy");
315
+ const y = interpolate(s, [0, 1], [50, 0]);
316
+ const op = interpolate(s, [0, 0.4], [0, 1], {
317
+ extrapolateLeft: "clamp",
318
+ extrapolateRight: "clamp",
319
+ });
320
+ const isHour = w === "Hour";
321
+ return (
322
+ <span
323
+ key={`${w}-${i}`}
324
+ style={{
325
+ display: "inline-block",
326
+ transform: `translateY(${y}px)${isHour ? ` scale(${hourPulse})` : ""}`,
327
+ opacity: op,
328
+ color: isHour ? C.claude : C.fg,
329
+ textShadow: isHour
330
+ ? `0 0 24px rgba(212,102,58,0.5)`
331
+ : "none",
332
+ }}
333
+ >
334
+ {w}
335
+ </span>
336
+ );
337
+ })}
338
+ </div>
339
+
340
+ {/* ── SUBHEAD ─────────────────────────────────── */}
341
+ <div
342
+ style={{
343
+ position: "absolute",
344
+ top: 590,
345
+ width: "100%",
346
+ textAlign: "center",
347
+ padding: "0 70px",
348
+ opacity: subOp,
349
+ transform: `translateY(${subY}px)`,
350
+ fontFamily: FONT,
351
+ fontSize: 36,
352
+ fontWeight: 500,
353
+ color: C.fgSoft,
354
+ letterSpacing: "-0.025em",
355
+ lineHeight: 1.2,
356
+ }}
357
+ >
358
+ using a custom-created{" "}
359
+ <span
360
+ style={{
361
+ color: C.claudeSoft,
362
+ fontWeight: 700,
363
+ background: `linear-gradient(135deg, ${C.claude}, ${C.claudeSoft})`,
364
+ WebkitBackgroundClip: "text",
365
+ backgroundClip: "text",
366
+ WebkitTextFillColor: "transparent",
367
+ }}
368
+ >
369
+ Claude Skill
370
+ </span>
371
+ </div>
372
+
373
+ {/* ── STAT CHIPS ──────────────────────────────── */}
374
+ <div
375
+ style={{
376
+ position: "absolute",
377
+ top: 740,
378
+ width: "100%",
379
+ display: "flex",
380
+ justifyContent: "center",
381
+ gap: 14,
382
+ padding: "0 30px",
383
+ flexWrap: "wrap",
384
+ }}
385
+ >
386
+ {[
387
+ { icon: "⏱", label: "1 HOUR", color: C.claudeSoft },
388
+ { icon: "⚡", label: "ZERO BOILERPLATE", color: C.fg },
389
+ { icon: "🚀", label: "SHIPPABLE", color: C.safe },
390
+ ].map((chip, i) => {
391
+ const s = gs(localF, fps, chipDelays[i], "snappy");
392
+ const op = interpolate(s, [0, 0.4], [0, 1], {
393
+ extrapolateLeft: "clamp",
394
+ extrapolateRight: "clamp",
395
+ });
396
+ const y = interpolate(s, [0, 1], [16, 0]);
397
+ return (
398
+ <div
399
+ key={chip.label}
400
+ style={{
401
+ opacity: op,
402
+ transform: `translateY(${y}px)`,
403
+ padding: "10px 18px",
404
+ borderRadius: 12,
405
+ background: "rgba(255,255,255,0.04)",
406
+ border: `1px solid ${C.border}`,
407
+ display: "flex",
408
+ alignItems: "center",
409
+ gap: 8,
410
+ fontFamily: MONO,
411
+ fontSize: 18,
412
+ fontWeight: 600,
413
+ color: chip.color,
414
+ letterSpacing: "0.06em",
415
+ }}
416
+ >
417
+ <span style={{ fontSize: 20 }}>{chip.icon}</span>
418
+ {chip.label}
419
+ </div>
420
+ );
421
+ })}
422
+ </div>
423
+ </SafeZone>
424
+ </AbsoluteFill>
425
+ );
426
+ };
427
+
428
+ // ═══════════════════════════════════════════════════════════════
429
+ // CTA SCENE (26s → 37s) — Like · Follow · Comment "AI"
430
+ // ═══════════════════════════════════════════════════════════════
431
+
432
+ const CTAScene: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
433
+ const startF = CTA_START_SEC * fps;
434
+ const endF = CTA_END_SEC * fps;
435
+ const localF = Math.max(0, frame - startF);
436
+
437
+ const sceneOp = interpolate(
438
+ frame,
439
+ [startF, startF + 0.4 * fps, endF - 0.3 * fps, endF],
440
+ [0, 1, 1, 1],
441
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
442
+ );
443
+ if (frame < startF) return null;
444
+
445
+ // Heading word reveal
446
+ const headingWords = ["Want", "this", "website's", "full", "code?"];
447
+ const subSpring = gs(localF, fps, 1.1, "gentle");
448
+ const subOp = interpolate(subSpring, [0, 0.4], [0, 1], {
449
+ extrapolateLeft: "clamp",
450
+ extrapolateRight: "clamp",
451
+ });
452
+ const subY = interpolate(subSpring, [0, 1], [20, 0]);
453
+
454
+ // Action rows — stagger in
455
+ const rowDelays = [1.6, 1.9, 2.2];
456
+ const rows = rowDelays.map((d) => {
457
+ const s = gs(localF, fps, d, "bouncy");
458
+ return {
459
+ op: interpolate(s, [0, 0.4], [0, 1], {
460
+ extrapolateLeft: "clamp",
461
+ extrapolateRight: "clamp",
462
+ }),
463
+ x: interpolate(s, [0, 1], [-60, 0]),
464
+ scale: interpolate(s, [0, 0.6, 1], [0.92, 1.02, 1]),
465
+ };
466
+ });
467
+
468
+ // "AI" typed into the comment row (3.2 → 3.9s)
469
+ const typed = Math.floor(
470
+ interpolate(localF, [3.2 * fps, 3.9 * fps], [0, 2], {
471
+ extrapolateLeft: "clamp",
472
+ extrapolateRight: "clamp",
473
+ }),
474
+ );
475
+ const aiText = "AI".slice(0, typed);
476
+ const cursorVis = Math.floor(localF / 10) % 2 === 0;
477
+
478
+ // Heart pulse when comment lands (4.0s)
479
+ const heartPulse = gs(localF, fps, 4.0, "bouncy");
480
+ const heartScale = interpolate(heartPulse, [0, 0.5, 1], [1, 1.35, 1.08]);
481
+ const heartFilled = localF > 4.0 * fps;
482
+
483
+ // Follow pill activates (4.3s)
484
+ const followActive = localF > 4.3 * fps;
485
+
486
+ // Bottom reward pill (4.8s)
487
+ const pillSpring = gs(localF, fps, 4.8, "glass");
488
+ const pillY = interpolate(pillSpring, [0, 1], [80, 0]);
489
+ const pillOp = interpolate(pillSpring, [0, 0.35], [0, 1], {
490
+ extrapolateLeft: "clamp",
491
+ extrapolateRight: "clamp",
492
+ });
493
+ const pillGlow = 0.5 + Math.sin(localF * 0.12) * 0.2;
494
+
495
+ // YouTube tutorial tag (5.3s)
496
+ const ytSpring = gs(localF, fps, 5.3, "gentle");
497
+ const ytOp = interpolate(ytSpring, [0, 0.4], [0, 1], {
498
+ extrapolateLeft: "clamp",
499
+ extrapolateRight: "clamp",
500
+ });
501
+ const ytY = interpolate(ytSpring, [0, 1], [16, 0]);
502
+
503
+ return (
504
+ <AbsoluteFill style={{ opacity: sceneOp }}>
505
+ <SafeZone>
506
+ {/* ── HEADING ─────────────────────────────────── */}
507
+ <div
508
+ style={{
509
+ position: "absolute",
510
+ top: 0,
511
+ width: "100%",
512
+ textAlign: "center",
513
+ padding: "0 60px",
514
+ }}
515
+ >
516
+ <div
517
+ style={{
518
+ fontFamily: FONT,
519
+ fontSize: 72,
520
+ fontWeight: 700,
521
+ color: C.fg,
522
+ letterSpacing: "-0.035em",
523
+ lineHeight: 1.05,
524
+ display: "flex",
525
+ flexWrap: "wrap",
526
+ gap: "0 0.32em",
527
+ justifyContent: "center",
528
+ }}
529
+ >
530
+ {headingWords.map((w, i) => {
531
+ const s = gs(localF, fps, 0.1 + i * 0.08, "snappy");
532
+ const y = interpolate(s, [0, 1], [40, 0]);
533
+ const op = interpolate(s, [0, 0.4], [0, 1], {
534
+ extrapolateLeft: "clamp",
535
+ extrapolateRight: "clamp",
536
+ });
537
+ const isFinal = w === "code?";
538
+ return (
539
+ <span
540
+ key={`${w}-${i}`}
541
+ style={{
542
+ display: "inline-block",
543
+ transform: `translateY(${y}px)`,
544
+ opacity: op,
545
+ color: isFinal ? C.claude : C.fg,
546
+ }}
547
+ >
548
+ {w}
549
+ </span>
550
+ );
551
+ })}
552
+ </div>
553
+ {(() => {
554
+ const sweep = (localF * 1.1) % 260 - 30; // -30 → 230 loop
555
+ const glow = 0.35 + Math.sin(localF * 0.08) * 0.12;
556
+ const glowRadius = 16 + Math.sin(localF * 0.08) * 6;
557
+ return (
558
+ <div
559
+ style={{
560
+ position: "relative",
561
+ marginTop: 24,
562
+ display: "inline-block",
563
+ opacity: subOp,
564
+ transform: `translateY(${subY}px)`,
565
+ }}
566
+ >
567
+ {/* Soft radial glow behind the text */}
568
+ <div
569
+ style={{
570
+ position: "absolute",
571
+ inset: "-40px -80px",
572
+ background: `radial-gradient(ellipse at center, rgba(212,102,58,${0.18 + Math.sin(localF * 0.06) * 0.05}) 0%, transparent 65%)`,
573
+ pointerEvents: "none",
574
+ filter: "blur(8px)",
575
+ }}
576
+ />
577
+ {/* Text with animated sheen — gradient stays white, sheen band brightens to warm highlight */}
578
+ <div
579
+ style={{
580
+ position: "relative",
581
+ fontFamily: MONO,
582
+ fontSize: 32,
583
+ fontWeight: 700,
584
+ letterSpacing: "0.03em",
585
+ textTransform: "uppercase",
586
+ textShadow: `0 0 ${glowRadius}px rgba(212,102,58,${glow}), 0 0 40px rgba(212,102,58,0.15)`,
587
+ backgroundImage: `linear-gradient(105deg, #f5f5f7 0%, #f5f5f7 ${sweep - 12}%, #ffe9d6 ${sweep}%, #f5f5f7 ${sweep + 12}%, #f5f5f7 100%)`,
588
+ WebkitBackgroundClip: "text",
589
+ backgroundClip: "text",
590
+ WebkitTextFillColor: "transparent",
591
+ color: "transparent",
592
+ }}
593
+ >
594
+ + THE ENTIRE SKILL PACK
595
+ </div>
596
+ </div>
597
+ );
598
+ })()}
599
+ </div>
600
+
601
+ {/* ── ACTION ROWS ─────────────────────────────── */}
602
+ <div
603
+ style={{
604
+ position: "absolute",
605
+ top: 320,
606
+ left: 0,
607
+ right: 0,
608
+ display: "flex",
609
+ flexDirection: "column",
610
+ gap: 18,
611
+ padding: "0 60px",
612
+ }}
613
+ >
614
+ {/* LIKE row */}
615
+ <ActionRow
616
+ anim={rows[0]}
617
+ active={heartFilled}
618
+ labelIdle="Like"
619
+ labelActive="Liked"
620
+ accent={C.danger}
621
+ icon={
622
+ <svg
623
+ width={58}
624
+ height={58}
625
+ viewBox="0 0 24 24"
626
+ fill={heartFilled ? C.danger : "none"}
627
+ stroke={heartFilled ? C.danger : C.fg}
628
+ strokeWidth="2"
629
+ style={{ transform: `scale(${heartScale})` }}
630
+ >
631
+ <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" />
632
+ </svg>
633
+ }
634
+ />
635
+
636
+ {/* FOLLOW row */}
637
+ <ActionRow
638
+ anim={rows[1]}
639
+ active={followActive}
640
+ labelIdle="Follow"
641
+ labelActive="Following"
642
+ accent={C.claude}
643
+ icon={
644
+ <svg
645
+ width={58}
646
+ height={58}
647
+ viewBox="0 0 24 24"
648
+ fill="none"
649
+ stroke={followActive ? C.claude : C.fg}
650
+ strokeWidth="2"
651
+ strokeLinejoin="round"
652
+ strokeLinecap="round"
653
+ >
654
+ <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
655
+ <circle cx="9" cy="7" r="4" />
656
+ <line x1="19" y1="8" x2="19" y2="14" />
657
+ <line x1="22" y1="11" x2="16" y2="11" />
658
+ </svg>
659
+ }
660
+ />
661
+
662
+ {/* COMMENT row — types "AI" */}
663
+ <ActionRow
664
+ anim={rows[2]}
665
+ active={typed > 0}
666
+ labelIdle="Comment"
667
+ accent={C.safe}
668
+ icon={
669
+ <svg
670
+ width={58}
671
+ height={58}
672
+ viewBox="0 0 24 24"
673
+ fill="none"
674
+ stroke={typed > 0 ? C.safe : C.fg}
675
+ strokeWidth="2"
676
+ strokeLinejoin="round"
677
+ strokeLinecap="round"
678
+ >
679
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
680
+ </svg>
681
+ }
682
+ trailing={
683
+ <div
684
+ style={{
685
+ fontFamily: MONO,
686
+ fontSize: 38,
687
+ fontWeight: 600,
688
+ color: typed > 0 ? C.safe : C.fgDim,
689
+ letterSpacing: "0.04em",
690
+ minWidth: 90,
691
+ textAlign: "right",
692
+ }}
693
+ >
694
+ {aiText || "_"}
695
+ {typed < 2 && cursorVis && (
696
+ <span style={{ color: C.fgMuted }}>|</span>
697
+ )}
698
+ </div>
699
+ }
700
+ />
701
+ </div>
702
+
703
+ {/* ── YOUTUBE TUTORIAL TAG — animated glassy border ───── */}
704
+ <div
705
+ style={{
706
+ position: "absolute",
707
+ bottom: 150,
708
+ left: "50%",
709
+ transform: `translate(-50%, ${ytY}px)`,
710
+ opacity: ytOp,
711
+ padding: 3,
712
+ borderRadius: 9999,
713
+ background: `conic-gradient(from ${(localF * 3) % 360}deg, rgba(255,255,255,0.06), rgba(255,255,255,0.55), rgba(212,102,58,0.8), rgba(255,255,255,0.5), rgba(79,196,106,0.45), rgba(255,255,255,0.06))`,
714
+ boxShadow: `0 18px 50px -12px rgba(0,0,0,0.7), 0 0 60px -10px rgba(212,102,58,${0.15 + Math.sin(localF * 0.1) * 0.08})`,
715
+ }}
716
+ >
717
+ <div
718
+ style={{
719
+ display: "flex",
720
+ alignItems: "center",
721
+ gap: 22,
722
+ padding: "26px 46px",
723
+ background: `linear-gradient(135deg, rgba(20,20,22,0.92), rgba(26,26,29,0.92))`,
724
+ borderRadius: 9999,
725
+ backdropFilter: "blur(14px)",
726
+ position: "relative",
727
+ overflow: "hidden",
728
+ }}
729
+ >
730
+ {/* subtle inner sheen */}
731
+ <div
732
+ style={{
733
+ position: "absolute",
734
+ inset: 0,
735
+ background: `linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 40%, transparent 60%, rgba(255,255,255,0.02) 100%)`,
736
+ pointerEvents: "none",
737
+ }}
738
+ />
739
+ {/* YouTube icon */}
740
+ <svg
741
+ width="58"
742
+ height="58"
743
+ viewBox="0 0 24 24"
744
+ fill="#FF0033"
745
+ style={{ position: "relative", flexShrink: 0 }}
746
+ >
747
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.546 15.568V8.432L15.818 12l-6.272 3.568z" />
748
+ </svg>
749
+ <span
750
+ style={{
751
+ position: "relative",
752
+ fontFamily: FONT,
753
+ fontSize: 34,
754
+ fontWeight: 600,
755
+ color: C.fgSoft,
756
+ letterSpacing: "-0.02em",
757
+ whiteSpace: "nowrap",
758
+ }}
759
+ >
760
+ Check Bio, Full tutorial{" "}
761
+ <span style={{ fontWeight: 700, color: C.fg }}>@DeviniLabs</span>
762
+ </span>
763
+ </div>
764
+ </div>
765
+
766
+ {/* ── REWARD PILL ─────────────────────────────── */}
767
+ <div
768
+ style={{
769
+ position: "absolute",
770
+ bottom: 6,
771
+ left: "50%",
772
+ transform: `translate(-50%, ${pillY}px)`,
773
+ opacity: pillOp,
774
+ padding: "20px 36px",
775
+ background: `linear-gradient(135deg, ${C.claude}, ${C.claudeSoft})`,
776
+ borderRadius: 9999,
777
+ display: "flex",
778
+ alignItems: "center",
779
+ gap: 14,
780
+ boxShadow: `0 24px 50px -10px rgba(212,102,58,${0.45 + pillGlow * 0.15}), 0 0 80px rgba(212,102,58,${0.25 + pillGlow * 0.2})`,
781
+ }}
782
+ >
783
+ <svg
784
+ width="30"
785
+ height="30"
786
+ viewBox="0 0 24 24"
787
+ fill="none"
788
+ stroke="#fff"
789
+ strokeWidth="2.6"
790
+ strokeLinejoin="round"
791
+ strokeLinecap="round"
792
+ >
793
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
794
+ </svg>
795
+ <span
796
+ style={{
797
+ fontFamily: FONT,
798
+ fontSize: 28,
799
+ fontWeight: 700,
800
+ color: "#fff",
801
+ letterSpacing: "-0.02em",
802
+ }}
803
+ >
804
+ Like, Follow & Comment{" "}
805
+ <span
806
+ style={{
807
+ fontFamily: MONO,
808
+ background: "rgba(255,255,255,0.2)",
809
+ padding: "2px 12px",
810
+ borderRadius: 8,
811
+ fontSize: 26,
812
+ }}
813
+ >
814
+ AI
815
+ </span>
816
+ </span>
817
+ </div>
818
+ </SafeZone>
819
+ </AbsoluteFill>
820
+ );
821
+ };
822
+
823
+ // ═══════════════════════════════════════════════════════════════
824
+ // ACTION ROW — single card for Like / Follow / Comment
825
+ // ═══════════════════════════════════════════════════════════════
826
+
827
+ const ActionRow: React.FC<{
828
+ anim: { op: number; x: number; scale: number };
829
+ active: boolean;
830
+ labelIdle: string;
831
+ labelActive?: string;
832
+ accent: string;
833
+ icon: React.ReactNode;
834
+ trailing?: React.ReactNode;
835
+ }> = ({ anim, active, labelIdle, labelActive, accent, icon, trailing }) => {
836
+ return (
837
+ <div
838
+ style={{
839
+ opacity: anim.op,
840
+ transform: `translateX(${anim.x}px) scale(${anim.scale})`,
841
+ background: active
842
+ ? `linear-gradient(135deg, ${accent}22, ${C.surface})`
843
+ : C.surface,
844
+ border: `1.5px solid ${active ? accent : C.border}`,
845
+ borderRadius: 22,
846
+ padding: "22px 28px",
847
+ display: "flex",
848
+ alignItems: "center",
849
+ gap: 24,
850
+ boxShadow: active
851
+ ? `0 0 40px -8px ${accent}66, inset 0 1px 0 rgba(255,255,255,0.04)`
852
+ : `inset 0 1px 0 rgba(255,255,255,0.04)`,
853
+ transition: "none",
854
+ }}
855
+ >
856
+ <div
857
+ style={{
858
+ width: 84,
859
+ height: 84,
860
+ borderRadius: 20,
861
+ background: active ? `${accent}14` : "rgba(255,255,255,0.04)",
862
+ border: `1px solid ${active ? `${accent}55` : C.border}`,
863
+ display: "flex",
864
+ alignItems: "center",
865
+ justifyContent: "center",
866
+ flexShrink: 0,
867
+ }}
868
+ >
869
+ {icon}
870
+ </div>
871
+ <div
872
+ style={{
873
+ fontFamily: FONT,
874
+ fontSize: 44,
875
+ fontWeight: 700,
876
+ color: active ? C.fg : C.fgSoft,
877
+ letterSpacing: "-0.03em",
878
+ flex: 1,
879
+ }}
880
+ >
881
+ {active && labelActive ? labelActive : labelIdle}
882
+ </div>
883
+ {trailing}
884
+ </div>
885
+ );
886
+ };
887
+
888
+ // ═══════════════════════════════════════════════════════════════
889
+ // MAIN
890
+ // ═══════════════════════════════════════════════════════════════
891
+
892
+ export const CodeDropReel: React.FC = () => {
893
+ const frame = useCurrentFrame();
894
+ const { fps } = useVideoConfig();
895
+
896
+ return (
897
+ <AbsoluteFill style={{ backgroundColor: C.bg, fontFamily: FONT }}>
898
+ <Background frame={frame} />
899
+ {/*
900
+ 0 → 20s: blank dark canvas. User layers website video clips on top.
901
+ 20 → 26s: "Build Within an Hour" Claude pitch screen.
902
+ 26 → 37s: CTA — Like, Follow, Comment AI.
903
+ */}
904
+ <BuildScene frame={frame} fps={fps} />
905
+ <CTAScene frame={frame} fps={fps} />
906
+ </AbsoluteFill>
907
+ );
908
+ };