@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,1460 @@
1
+ /**
2
+ * REFERENCE — SkillsReel (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/SkillsReel.tsx
15
+ * Bundled at: 2026-05-12T19:40:04.834Z
16
+ */
17
+ import {
18
+ useCurrentFrame,
19
+ useVideoConfig,
20
+ interpolate,
21
+ spring,
22
+ Easing,
23
+ AbsoluteFill,
24
+ } from "remotion";
25
+ import { ds } from "./designSystem";
26
+
27
+ // ═══════════════════════════════════════════════════════════════
28
+ // CONSTANTS — all values sourced from designSystem.ts
29
+ // ═══════════════════════════════════════════════════════════════
30
+
31
+ const FONT = ds.font.sans;
32
+
33
+ // Scene time ranges [startSec, endSec]
34
+ const SCENES = {
35
+ hook: [0, 5.5] as const,
36
+ powers: [5, 11] as const,
37
+ reveal: [10.5, 17.5] as const,
38
+ cats: [17, 23] as const,
39
+ compat: [22.5, 28.5] as const,
40
+ setup: [28, 34.5] as const,
41
+ cta: [34, 42] as const,
42
+ };
43
+
44
+ const CATEGORIES = [
45
+ { icon: "🎨", label: "UI Design" },
46
+ { icon: "🧪", label: "App Testing" },
47
+ { icon: "📈", label: "SEO" },
48
+ { icon: "✍️", label: "Writing" },
49
+ { icon: "🐛", label: "Debugging" },
50
+ { icon: "🚀", label: "Deploy" },
51
+ ];
52
+
53
+ const PLATFORMS = [
54
+ { name: "Claude Code", icon: "⌨️" },
55
+ { name: "Codex", icon: "📦" },
56
+ { name: "Desktop App", icon: "🖥️" },
57
+ { name: "Any Editor", icon: "✏️" },
58
+ ];
59
+
60
+ // ═══════════════════════════════════════════════════════════════
61
+ // UTILITIES
62
+ // ═══════════════════════════════════════════════════════════════
63
+
64
+ const sceneOp = (
65
+ frame: number,
66
+ fps: number,
67
+ range: readonly [number, number],
68
+ fadeIn = 0.5,
69
+ fadeOut = 0.5,
70
+ ) =>
71
+ interpolate(
72
+ frame,
73
+ [
74
+ range[0] * fps,
75
+ (range[0] + fadeIn) * fps,
76
+ (range[1] - fadeOut) * fps,
77
+ range[1] * fps,
78
+ ],
79
+ [0, 1, 1, 0],
80
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
81
+ );
82
+
83
+ const lf = (frame: number, fps: number, startSec: number) =>
84
+ Math.max(0, frame - startSec * fps);
85
+
86
+ interface SceneProps {
87
+ frame: number;
88
+ fps: number;
89
+ }
90
+
91
+ // ═══════════════════════════════════════════════════════════════
92
+ // CLAUDE LOGO — accurate 12-ray organic starburst
93
+ // ═══════════════════════════════════════════════════════════════
94
+
95
+ const CLAUDE_COLOR = ds.color.claude;
96
+
97
+ // 12 rays: [angle°, lengthRatio, widthRatio] — organic spacing & sizing
98
+ const RAYS: [number, number, number][] = [
99
+ [0, 0.44, 0.055],
100
+ [30, 0.35, 0.048],
101
+ [58, 0.41, 0.052],
102
+ [88, 0.35, 0.048],
103
+ [118, 0.43, 0.054],
104
+ [148, 0.35, 0.048],
105
+ [178, 0.41, 0.052],
106
+ [208, 0.35, 0.048],
107
+ [238, 0.43, 0.053],
108
+ [266, 0.35, 0.048],
109
+ [295, 0.40, 0.052],
110
+ [325, 0.36, 0.048],
111
+ ];
112
+
113
+ /** Just the starburst — for use on dark backgrounds */
114
+ const ClaudeStarburst: React.FC<{
115
+ size: number;
116
+ color?: string;
117
+ rotation?: number;
118
+ }> = ({ size, color = CLAUDE_COLOR, rotation = 0 }) => {
119
+ const c = size / 2;
120
+ return (
121
+ <svg
122
+ width={size}
123
+ height={size}
124
+ viewBox={`0 0 ${size} ${size}`}
125
+ style={{ transform: `rotate(${rotation}deg)` }}
126
+ >
127
+ {RAYS.map(([angle, len, w], i) => (
128
+ <ellipse
129
+ key={i}
130
+ cx={c}
131
+ cy={c - size * len * 0.46}
132
+ rx={size * w}
133
+ ry={size * len * 0.46}
134
+ fill={color}
135
+ transform={`rotate(${angle} ${c} ${c})`}
136
+ />
137
+ ))}
138
+ <circle cx={c} cy={c} r={size * 0.05} fill={color} />
139
+ </svg>
140
+ );
141
+ };
142
+
143
+ /** Full app icon — white rounded card + starburst + "Claude" text */
144
+ const ClaudeIcon: React.FC<{
145
+ size: number;
146
+ rotation?: number;
147
+ }> = ({ size, rotation = 0 }) => {
148
+ const starSize = size * 0.52;
149
+ return (
150
+ <div
151
+ style={{
152
+ width: size,
153
+ height: size,
154
+ position: "relative",
155
+ }}
156
+ >
157
+ {/* White rounded-square card */}
158
+ <div
159
+ style={{
160
+ position: "absolute",
161
+ inset: 0,
162
+ borderRadius: size * 0.22,
163
+ background: "linear-gradient(180deg, #FFFFFF 0%, #F4F4F8 100%)",
164
+ boxShadow: ds.shadow.cardLight,
165
+ }}
166
+ />
167
+ {/* Starburst — upper area */}
168
+ <div
169
+ style={{
170
+ position: "absolute",
171
+ top: size * 0.1,
172
+ left: "50%",
173
+ transform: "translate(-50%, 0)",
174
+ }}
175
+ >
176
+ <ClaudeStarburst size={starSize} rotation={rotation} />
177
+ </div>
178
+ {/* "Claude" text */}
179
+ <div
180
+ style={{
181
+ position: "absolute",
182
+ bottom: size * 0.1,
183
+ width: "100%",
184
+ textAlign: "center",
185
+ fontFamily: "Georgia, 'Times New Roman', serif",
186
+ fontSize: size * 0.13,
187
+ fontWeight: 400,
188
+ color: "#1A1A1A",
189
+ letterSpacing: 0.5,
190
+ }}
191
+ >
192
+ Claude
193
+ </div>
194
+ </div>
195
+ );
196
+ };
197
+
198
+ // ═══════════════════════════════════════════════════════════════
199
+ // BACKGROUND
200
+ // ═══════════════════════════════════════════════════════════════
201
+
202
+ const Background: React.FC<{ frame: number }> = ({ frame }) => (
203
+ <AbsoluteFill>
204
+ <div
205
+ style={{
206
+ width: "100%",
207
+ height: "100%",
208
+ background: ds.gradient.bgDark,
209
+ }}
210
+ />
211
+ {Array.from({ length: 20 }).map((_, i) => {
212
+ const seed = (i + 1) * 137.508;
213
+ const bx = (seed * 1.1) % 100;
214
+ const by = (seed * 2.3) % 100;
215
+ const spd = 0.12 + (i % 5) * 0.04;
216
+ const sz = 2 + (i % 3) * 1.5;
217
+ const op = 0.06 + (i % 4) * 0.03;
218
+ return (
219
+ <div
220
+ key={i}
221
+ style={{
222
+ position: "absolute",
223
+ left: `${bx + Math.sin(frame * 0.006 + seed) * 2.5}%`,
224
+ top: `${((by + frame * spd * 0.08) % 105) - 2.5}%`,
225
+ width: sz,
226
+ height: sz,
227
+ borderRadius: "50%",
228
+ background:
229
+ i % 2 === 0
230
+ ? `rgba(98,95,255,${op})`
231
+ : `rgba(212,102,58,${op})`,
232
+ }}
233
+ />
234
+ );
235
+ })}
236
+ </AbsoluteFill>
237
+ );
238
+
239
+ // ═══════════════════════════════════════════════════════════════
240
+ // SCENE 1: HOOK
241
+ // ═══════════════════════════════════════════════════════════════
242
+
243
+ const HookScene: React.FC<SceneProps> = ({ frame, fps }) => {
244
+ const opacity = sceneOp(frame, fps, SCENES.hook);
245
+ if (opacity === 0) return null;
246
+
247
+ const f = lf(frame, fps, SCENES.hook[0]);
248
+
249
+ // Logo entrance
250
+ const logoScale = spring({
251
+ frame: f,
252
+ fps,
253
+ config: { damping: 12, stiffness: 80 },
254
+ });
255
+ const logoRot = interpolate(f, [0, 1.5 * fps], [-20, 0], {
256
+ extrapolateLeft: "clamp",
257
+ extrapolateRight: "clamp",
258
+ easing: Easing.out(Easing.quad),
259
+ });
260
+ const logoGlow = interpolate(f, [0, 1 * fps], [0, 0.35], {
261
+ extrapolateLeft: "clamp",
262
+ extrapolateRight: "clamp",
263
+ });
264
+
265
+ // "Using Claude Code?"
266
+ const t1Op = interpolate(f, [0.8 * fps, 1.4 * fps], [0, 1], {
267
+ extrapolateLeft: "clamp",
268
+ extrapolateRight: "clamp",
269
+ });
270
+ const t1Y = interpolate(f, [0.8 * fps, 1.4 * fps], [40, 0], {
271
+ extrapolateLeft: "clamp",
272
+ extrapolateRight: "clamp",
273
+ easing: Easing.out(Easing.quad),
274
+ });
275
+
276
+ // "You NEED Skills."
277
+ const t2Spring = spring({
278
+ frame: f,
279
+ fps,
280
+ delay: Math.round(1.8 * fps),
281
+ config: { damping: 10, stiffness: 100 },
282
+ });
283
+ const t2Op = interpolate(f, [1.8 * fps, 2 * fps], [0, 1], {
284
+ extrapolateLeft: "clamp",
285
+ extrapolateRight: "clamp",
286
+ });
287
+
288
+ // Gentle pulse on "Skills" after it appears
289
+ const pulseScale =
290
+ t2Spring > 0.95
291
+ ? 1 + Math.sin((f / fps) * 3) * 0.02
292
+ : interpolate(t2Spring, [0, 1], [1.4, 1]);
293
+
294
+ return (
295
+ <AbsoluteFill style={{ opacity }}>
296
+ {/* Claude App Icon */}
297
+ <div
298
+ style={{
299
+ position: "absolute",
300
+ top: "26%",
301
+ left: "50%",
302
+ transform: `translate(-50%, -50%) scale(${logoScale})`,
303
+ }}
304
+ >
305
+ <div style={{ position: "relative" }}>
306
+ <ClaudeIcon size={220} rotation={logoRot} />
307
+ {/* Glow behind icon */}
308
+ <div
309
+ style={{
310
+ position: "absolute",
311
+ inset: -60,
312
+ borderRadius: "50%",
313
+ background: `radial-gradient(circle, rgba(212,102,58,${logoGlow}) 0%, transparent 70%)`,
314
+ filter: "blur(30px)",
315
+ zIndex: -1,
316
+ }}
317
+ />
318
+ </div>
319
+ </div>
320
+
321
+ {/* Text */}
322
+ <div
323
+ style={{
324
+ position: "absolute",
325
+ top: "48%",
326
+ width: "100%",
327
+ textAlign: "center",
328
+ padding: "0 60px",
329
+ }}
330
+ >
331
+ <div
332
+ style={{
333
+ fontFamily: FONT,
334
+ fontSize: 54,
335
+ fontWeight: 600,
336
+ color: ds.color.fg,
337
+ opacity: t1Op,
338
+ transform: `translateY(${t1Y}px)`,
339
+ lineHeight: 1.2,
340
+ }}
341
+ >
342
+ Using Claude Code?
343
+ </div>
344
+ <div
345
+ style={{
346
+ fontFamily: FONT,
347
+ fontSize: 72,
348
+ fontWeight: ds.text.bold,
349
+ color: ds.color.claude,
350
+ marginTop: 32,
351
+ opacity: t2Op,
352
+ transform: `scale(${pulseScale})`,
353
+ textShadow: "0 0 40px rgba(224,122,95,0.4)",
354
+ }}
355
+ >
356
+ You NEED Skills.
357
+ </div>
358
+ </div>
359
+ </AbsoluteFill>
360
+ );
361
+ };
362
+
363
+ // ═══════════════════════════════════════════════════════════════
364
+ // SCENE 2: SUPERPOWERS
365
+ // ═══════════════════════════════════════════════════════════════
366
+
367
+ const PowersScene: React.FC<SceneProps> = ({ frame, fps }) => {
368
+ const opacity = sceneOp(frame, fps, SCENES.powers);
369
+ if (opacity === 0) return null;
370
+
371
+ const f = lf(frame, fps, SCENES.powers[0]);
372
+
373
+ // "Think of skills as"
374
+ const t1Op = interpolate(f, [0.5 * fps, 1 * fps], [0, 1], {
375
+ extrapolateLeft: "clamp",
376
+ extrapolateRight: "clamp",
377
+ });
378
+ const t1Y = interpolate(f, [0.5 * fps, 1 * fps], [30, 0], {
379
+ extrapolateLeft: "clamp",
380
+ extrapolateRight: "clamp",
381
+ easing: Easing.out(Easing.quad),
382
+ });
383
+
384
+ // "SUPERPOWERS"
385
+ const bigSpring = spring({
386
+ frame: f,
387
+ fps,
388
+ delay: Math.round(1 * fps),
389
+ config: { damping: 10, stiffness: 90 },
390
+ });
391
+ const bigScale = interpolate(bigSpring, [0, 1], [0.4, 1]);
392
+ const bigGlow = interpolate(bigSpring, [0, 1], [0, 0.5]);
393
+
394
+ // "for your AI"
395
+ const t3Op = interpolate(f, [1.8 * fps, 2.3 * fps], [0, 1], {
396
+ extrapolateLeft: "clamp",
397
+ extrapolateRight: "clamp",
398
+ });
399
+ const t3Y = interpolate(f, [1.8 * fps, 2.3 * fps], [20, 0], {
400
+ extrapolateLeft: "clamp",
401
+ extrapolateRight: "clamp",
402
+ easing: Easing.out(Easing.quad),
403
+ });
404
+
405
+ // Glass card
406
+ const cardSpring = spring({
407
+ frame: f,
408
+ fps,
409
+ delay: Math.round(2.5 * fps),
410
+ config: { damping: 12, stiffness: 80 },
411
+ });
412
+ const cardScale = interpolate(cardSpring, [0, 1], [0.8, 1]);
413
+ const cardOp = interpolate(f, [2.5 * fps, 3 * fps], [0, 1], {
414
+ extrapolateLeft: "clamp",
415
+ extrapolateRight: "clamp",
416
+ });
417
+
418
+ return (
419
+ <AbsoluteFill style={{ opacity }}>
420
+ <div
421
+ style={{
422
+ position: "absolute",
423
+ top: "20%",
424
+ width: "100%",
425
+ textAlign: "center",
426
+ padding: "0 60px",
427
+ }}
428
+ >
429
+ <div
430
+ style={{
431
+ fontFamily: FONT,
432
+ fontSize: 32,
433
+ fontWeight: 500,
434
+ color: ds.color.fgMuted,
435
+ opacity: t1Op,
436
+ transform: `translateY(${t1Y}px)`,
437
+ letterSpacing: 3,
438
+ textTransform: "uppercase",
439
+ }}
440
+ >
441
+ Think of skills as
442
+ </div>
443
+
444
+ <div style={{ position: "relative", display: "inline-block" }}>
445
+ <div
446
+ style={{
447
+ fontFamily: FONT,
448
+ fontSize: 84,
449
+ fontWeight: ds.text.bold,
450
+ color: ds.color.fg,
451
+ marginTop: 20,
452
+ transform: `scale(${bigScale})`,
453
+ opacity: interpolate(bigSpring, [0, 0.2], [0, 1], {
454
+ extrapolateLeft: "clamp",
455
+ extrapolateRight: "clamp",
456
+ }),
457
+ letterSpacing: -2,
458
+ }}
459
+ >
460
+ Superpowers
461
+ </div>
462
+ {/* Glow behind text */}
463
+ <div
464
+ style={{
465
+ position: "absolute",
466
+ inset: "-40px -60px",
467
+ borderRadius: "50%",
468
+ background: `radial-gradient(ellipse, rgba(139,92,246,${bigGlow * 0.6}) 0%, transparent 70%)`,
469
+ filter: "blur(40px)",
470
+ zIndex: -1,
471
+ }}
472
+ />
473
+ </div>
474
+
475
+ <div
476
+ style={{
477
+ fontFamily: FONT,
478
+ fontSize: 38,
479
+ fontWeight: 500,
480
+ color: ds.color.fgMuted,
481
+ marginTop: 16,
482
+ opacity: t3Op,
483
+ transform: `translateY(${t3Y}px)`,
484
+ }}
485
+ >
486
+ for your AI
487
+ </div>
488
+ </div>
489
+
490
+ {/* Glass card */}
491
+ <div
492
+ style={{
493
+ position: "absolute",
494
+ bottom: "22%",
495
+ left: "50%",
496
+ transform: `translate(-50%, 0) scale(${cardScale})`,
497
+ opacity: cardOp,
498
+ ...ds.glass.light,
499
+ borderRadius: ds.radius["2xl"],
500
+ boxShadow: ds.shadow.card,
501
+ padding: "40px 56px",
502
+ textAlign: "center",
503
+ }}
504
+ >
505
+ <div
506
+ style={{
507
+ fontFamily: FONT,
508
+ fontSize: 34,
509
+ fontWeight: 600,
510
+ color: ds.color.fg,
511
+ lineHeight: 1.6,
512
+ }}
513
+ >
514
+ Install once{" "}
515
+ <span style={{ color: ds.color.fgMuted, fontSize: 28 }}>→</span>
516
+ <br />
517
+ <span style={{ color: ds.color.claude }}>Works forever</span>
518
+ </div>
519
+ </div>
520
+ </AbsoluteFill>
521
+ );
522
+ };
523
+
524
+ // ═══════════════════════════════════════════════════════════════
525
+ // SCENE 3: SKILL.SH REVEAL
526
+ // ═══════════════════════════════════════════════════════════════
527
+
528
+ const RevealScene: React.FC<SceneProps> = ({ frame, fps }) => {
529
+ const opacity = sceneOp(frame, fps, SCENES.reveal);
530
+ if (opacity === 0) return null;
531
+
532
+ const f = lf(frame, fps, SCENES.reveal[0]);
533
+
534
+ // Light burst
535
+ const burstOp = interpolate(
536
+ f,
537
+ [0.3 * fps, 0.8 * fps, 1.8 * fps],
538
+ [0, 0.7, 0],
539
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
540
+ );
541
+
542
+ // "skill.sh" reveal
543
+ const nameSpring = spring({
544
+ frame: f,
545
+ fps,
546
+ delay: Math.round(0.6 * fps),
547
+ config: { damping: 12, stiffness: 80 },
548
+ });
549
+ const nameScale = interpolate(nameSpring, [0, 1], [0.3, 1]);
550
+ const nameOp = interpolate(f, [0.6 * fps, 1 * fps], [0, 1], {
551
+ extrapolateLeft: "clamp",
552
+ extrapolateRight: "clamp",
553
+ });
554
+ const nameGlow = interpolate(nameSpring, [0, 1], [0, 0.6]);
555
+
556
+ // Counter 0 → 1000
557
+ const count = Math.round(
558
+ interpolate(f, [1.5 * fps, 3 * fps], [0, 1000], {
559
+ extrapolateLeft: "clamp",
560
+ extrapolateRight: "clamp",
561
+ easing: Easing.out(Easing.quad),
562
+ }),
563
+ );
564
+ const countOp = interpolate(f, [1.5 * fps, 2 * fps], [0, 1], {
565
+ extrapolateLeft: "clamp",
566
+ extrapolateRight: "clamp",
567
+ });
568
+
569
+ // "Free Claude Skills"
570
+ const subOp = interpolate(f, [2.8 * fps, 3.3 * fps], [0, 1], {
571
+ extrapolateLeft: "clamp",
572
+ extrapolateRight: "clamp",
573
+ });
574
+ const subY = interpolate(f, [2.8 * fps, 3.3 * fps], [20, 0], {
575
+ extrapolateLeft: "clamp",
576
+ extrapolateRight: "clamp",
577
+ easing: Easing.out(Easing.quad),
578
+ });
579
+
580
+ // "Completely free" tag
581
+ const tagSpring = spring({
582
+ frame: f,
583
+ fps,
584
+ delay: Math.round(3.5 * fps),
585
+ config: { damping: 12, stiffness: 100 },
586
+ });
587
+ const tagScale = interpolate(tagSpring, [0, 1], [0.6, 1]);
588
+
589
+ return (
590
+ <AbsoluteFill style={{ opacity }}>
591
+ {/* Light burst */}
592
+ <div
593
+ style={{
594
+ position: "absolute",
595
+ top: "32%",
596
+ left: "50%",
597
+ width: 400,
598
+ height: 400,
599
+ transform: "translate(-50%, -50%)",
600
+ borderRadius: "50%",
601
+ background:
602
+ "radial-gradient(circle, rgba(139,92,246,0.8) 0%, rgba(99,102,241,0.3) 40%, transparent 70%)",
603
+ opacity: burstOp,
604
+ filter: "blur(20px)",
605
+ }}
606
+ />
607
+
608
+ {/* "skill.sh" text */}
609
+ <div
610
+ style={{
611
+ position: "absolute",
612
+ top: "28%",
613
+ width: "100%",
614
+ textAlign: "center",
615
+ }}
616
+ >
617
+ <div
618
+ style={{
619
+ position: "relative",
620
+ display: "inline-block",
621
+ }}
622
+ >
623
+ <div
624
+ style={{
625
+ fontFamily: FONT,
626
+ fontSize: 96,
627
+ fontWeight: ds.text.bold,
628
+ letterSpacing: -2,
629
+ opacity: nameOp,
630
+ transform: `scale(${nameScale})`,
631
+ background: ds.gradient.accentText,
632
+ WebkitBackgroundClip: "text",
633
+ WebkitTextFillColor: "transparent",
634
+ backgroundClip: "text",
635
+ }}
636
+ >
637
+ skill.sh
638
+ </div>
639
+ {/* Glow */}
640
+ <div
641
+ style={{
642
+ position: "absolute",
643
+ inset: "-50px -80px",
644
+ borderRadius: "50%",
645
+ background: `radial-gradient(ellipse, rgba(139,92,246,${nameGlow * 0.5}) 0%, transparent 70%)`,
646
+ filter: "blur(40px)",
647
+ zIndex: -1,
648
+ }}
649
+ />
650
+ </div>
651
+ </div>
652
+
653
+ {/* Counter */}
654
+ <div
655
+ style={{
656
+ position: "absolute",
657
+ top: "48%",
658
+ width: "100%",
659
+ textAlign: "center",
660
+ opacity: countOp,
661
+ }}
662
+ >
663
+ <div
664
+ style={{
665
+ fontFamily: FONT,
666
+ fontSize: 120,
667
+ fontWeight: ds.text.bold,
668
+ color: ds.color.fg,
669
+ lineHeight: 1,
670
+ }}
671
+ >
672
+ {count.toLocaleString()}+
673
+ </div>
674
+ <div
675
+ style={{
676
+ fontFamily: FONT,
677
+ fontSize: 32,
678
+ fontWeight: 500,
679
+ color: ds.color.fgMuted,
680
+ marginTop: 16,
681
+ opacity: subOp,
682
+ transform: `translateY(${subY}px)`,
683
+ letterSpacing: 2,
684
+ }}
685
+ >
686
+ Free Claude Skills
687
+ </div>
688
+ </div>
689
+
690
+ {/* "Completely free" badge */}
691
+ <div
692
+ style={{
693
+ position: "absolute",
694
+ bottom: "22%",
695
+ left: "50%",
696
+ transform: `translate(-50%, 0) scale(${tagScale})`,
697
+ opacity: interpolate(tagSpring, [0, 0.3], [0, 1], {
698
+ extrapolateLeft: "clamp",
699
+ extrapolateRight: "clamp",
700
+ }),
701
+ background: "rgba(52,211,153,0.15)",
702
+ border: "1px solid rgba(52,211,153,0.3)",
703
+ borderRadius: 100,
704
+ padding: "16px 40px",
705
+ }}
706
+ >
707
+ <div
708
+ style={{
709
+ fontFamily: FONT,
710
+ fontSize: 28,
711
+ fontWeight: 700,
712
+ color: ds.color.success,
713
+ letterSpacing: 1,
714
+ }}
715
+ >
716
+ Completely free
717
+ </div>
718
+ </div>
719
+ </AbsoluteFill>
720
+ );
721
+ };
722
+
723
+ // ═══════════════════════════════════════════════════════════════
724
+ // SCENE 4: CATEGORIES
725
+ // ═══════════════════════════════════════════════════════════════
726
+
727
+ const CategoriesScene: React.FC<SceneProps> = ({ frame, fps }) => {
728
+ const opacity = sceneOp(frame, fps, SCENES.cats);
729
+ if (opacity === 0) return null;
730
+
731
+ const f = lf(frame, fps, SCENES.cats[0]);
732
+
733
+ // Header
734
+ const headerOp = interpolate(f, [0.3 * fps, 0.8 * fps], [0, 1], {
735
+ extrapolateLeft: "clamp",
736
+ extrapolateRight: "clamp",
737
+ });
738
+ const headerY = interpolate(f, [0.3 * fps, 0.8 * fps], [30, 0], {
739
+ extrapolateLeft: "clamp",
740
+ extrapolateRight: "clamp",
741
+ easing: Easing.out(Easing.quad),
742
+ });
743
+
744
+ return (
745
+ <AbsoluteFill style={{ opacity }}>
746
+ {/* Header */}
747
+ <div
748
+ style={{
749
+ position: "absolute",
750
+ top: "10%",
751
+ width: "100%",
752
+ textAlign: "center",
753
+ padding: "0 60px",
754
+ opacity: headerOp,
755
+ transform: `translateY(${headerY}px)`,
756
+ }}
757
+ >
758
+ <div
759
+ style={{
760
+ fontFamily: FONT,
761
+ fontSize: 52,
762
+ fontWeight: ds.text.bold,
763
+ color: ds.color.fg,
764
+ lineHeight: 1.2,
765
+ }}
766
+ >
767
+ Skills for{" "}
768
+ <span style={{ color: ds.color.accent }}>everything</span>
769
+ </div>
770
+ </div>
771
+
772
+ {/* Category cards - 2 column grid */}
773
+ <div
774
+ style={{
775
+ position: "absolute",
776
+ top: "24%",
777
+ left: "50%",
778
+ transform: "translate(-50%, 0)",
779
+ display: "grid",
780
+ gridTemplateColumns: "1fr 1fr",
781
+ gap: 20,
782
+ padding: "0 48px",
783
+ width: "100%",
784
+ maxWidth: 900,
785
+ }}
786
+ >
787
+ {CATEGORIES.map((cat, i) => {
788
+ const delay = Math.round((0.8 + i * 0.15) * fps);
789
+ const cardSpring = spring({
790
+ frame: f,
791
+ fps,
792
+ delay,
793
+ config: { damping: 12, stiffness: 100 },
794
+ });
795
+ const fromX = i % 2 === 0 ? -80 : 80;
796
+ const x = interpolate(cardSpring, [0, 1], [fromX, 0]);
797
+ const cardOp = interpolate(f, [delay / fps * fps, delay + 0.3 * fps], [0, 1], {
798
+ extrapolateLeft: "clamp",
799
+ extrapolateRight: "clamp",
800
+ });
801
+
802
+ return (
803
+ <div
804
+ key={cat.label}
805
+ style={{
806
+ ...ds.glass.light,
807
+ borderRadius: ds.radius["2xl"],
808
+ boxShadow: ds.shadow.card,
809
+ padding: "28px 24px",
810
+ display: "flex",
811
+ alignItems: "center",
812
+ gap: 16,
813
+ opacity: cardOp,
814
+ transform: `translateX(${x}px)`,
815
+ }}
816
+ >
817
+ <div style={{ fontSize: 40, lineHeight: 1 }}>{cat.icon}</div>
818
+ <div
819
+ style={{
820
+ fontFamily: FONT,
821
+ fontSize: 26,
822
+ fontWeight: 600,
823
+ color: ds.color.fg,
824
+ }}
825
+ >
826
+ {cat.label}
827
+ </div>
828
+ </div>
829
+ );
830
+ })}
831
+ </div>
832
+
833
+ {/* "Whatever you're working on" */}
834
+ <div
835
+ style={{
836
+ position: "absolute",
837
+ bottom: "12%",
838
+ width: "100%",
839
+ textAlign: "center",
840
+ opacity: interpolate(f, [3 * fps, 3.5 * fps], [0, 1], {
841
+ extrapolateLeft: "clamp",
842
+ extrapolateRight: "clamp",
843
+ }),
844
+ transform: `translateY(${interpolate(f, [3 * fps, 3.5 * fps], [20, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })}px)`,
845
+ }}
846
+ >
847
+ <div
848
+ style={{
849
+ fontFamily: FONT,
850
+ fontSize: 28,
851
+ fontWeight: 500,
852
+ color: ds.color.fgMuted,
853
+ fontStyle: "italic",
854
+ }}
855
+ >
856
+ Whatever you're working on...
857
+ <br />
858
+ <span style={{ color: ds.color.claude, fontStyle: "normal", fontWeight: 600 }}>
859
+ there's a skill for it
860
+ </span>
861
+ </div>
862
+ </div>
863
+ </AbsoluteFill>
864
+ );
865
+ };
866
+
867
+ // ═══════════════════════════════════════════════════════════════
868
+ // SCENE 5: WORKS EVERYWHERE
869
+ // ═══════════════════════════════════════════════════════════════
870
+
871
+ const CompatScene: React.FC<SceneProps> = ({ frame, fps }) => {
872
+ const opacity = sceneOp(frame, fps, SCENES.compat);
873
+ if (opacity === 0) return null;
874
+
875
+ const f = lf(frame, fps, SCENES.compat[0]);
876
+
877
+ // Header
878
+ const headerOp = interpolate(f, [0.3 * fps, 0.8 * fps], [0, 1], {
879
+ extrapolateLeft: "clamp",
880
+ extrapolateRight: "clamp",
881
+ });
882
+
883
+ return (
884
+ <AbsoluteFill style={{ opacity }}>
885
+ {/* Header */}
886
+ <div
887
+ style={{
888
+ position: "absolute",
889
+ top: "14%",
890
+ width: "100%",
891
+ textAlign: "center",
892
+ padding: "0 60px",
893
+ opacity: headerOp,
894
+ }}
895
+ >
896
+ <div
897
+ style={{
898
+ fontFamily: FONT,
899
+ fontSize: 52,
900
+ fontWeight: ds.text.bold,
901
+ color: ds.color.fg,
902
+ }}
903
+ >
904
+ Works{" "}
905
+ <span style={{ color: ds.color.accentViolet }}>everywhere</span>
906
+ </div>
907
+ </div>
908
+
909
+ {/* Platform list */}
910
+ <div
911
+ style={{
912
+ position: "absolute",
913
+ top: "30%",
914
+ left: "50%",
915
+ transform: "translate(-50%, 0)",
916
+ display: "flex",
917
+ flexDirection: "column",
918
+ gap: 24,
919
+ width: "80%",
920
+ maxWidth: 700,
921
+ }}
922
+ >
923
+ {PLATFORMS.map((plat, i) => {
924
+ const delay = Math.round((0.8 + i * 0.25) * fps);
925
+ const itemSpring = spring({
926
+ frame: f,
927
+ fps,
928
+ delay,
929
+ config: { damping: 14, stiffness: 100 },
930
+ });
931
+ const x = interpolate(itemSpring, [0, 1], [-60, 0]);
932
+ const itemOp = interpolate(f, [delay / fps * fps, delay + 0.3 * fps], [0, 1], {
933
+ extrapolateLeft: "clamp",
934
+ extrapolateRight: "clamp",
935
+ });
936
+
937
+ // Checkmark appears after item lands
938
+ const checkDelay = delay + Math.round(0.3 * fps);
939
+ const checkSpring = spring({
940
+ frame: f,
941
+ fps,
942
+ delay: checkDelay,
943
+ config: { damping: 8, stiffness: 120 },
944
+ });
945
+ const checkScale = interpolate(checkSpring, [0, 1], [0, 1]);
946
+
947
+ return (
948
+ <div
949
+ key={plat.name}
950
+ style={{
951
+ ...ds.glass.light,
952
+ borderRadius: ds.radius.xl,
953
+ boxShadow: ds.shadow.card,
954
+ padding: "24px 32px",
955
+ display: "flex",
956
+ alignItems: "center",
957
+ justifyContent: "space-between",
958
+ opacity: itemOp,
959
+ transform: `translateX(${x}px)`,
960
+ }}
961
+ >
962
+ <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
963
+ <div style={{ fontSize: 36 }}>{plat.icon}</div>
964
+ <div
965
+ style={{
966
+ fontFamily: FONT,
967
+ fontSize: 30,
968
+ fontWeight: 600,
969
+ color: ds.color.fg,
970
+ }}
971
+ >
972
+ {plat.name}
973
+ </div>
974
+ </div>
975
+ <div
976
+ style={{
977
+ width: 40,
978
+ height: 40,
979
+ borderRadius: 12,
980
+ background: "rgba(52,211,153,0.2)",
981
+ border: "1px solid rgba(52,211,153,0.4)",
982
+ display: "flex",
983
+ alignItems: "center",
984
+ justifyContent: "center",
985
+ transform: `scale(${checkScale})`,
986
+ }}
987
+ >
988
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
989
+ <path
990
+ d="M4 10L8 14L16 6"
991
+ stroke={ds.color.success}
992
+ strokeWidth="2.5"
993
+ strokeLinecap="round"
994
+ strokeLinejoin="round"
995
+ />
996
+ </svg>
997
+ </div>
998
+ </div>
999
+ );
1000
+ })}
1001
+ </div>
1002
+
1003
+ {/* "It just works" */}
1004
+ <div
1005
+ style={{
1006
+ position: "absolute",
1007
+ bottom: "12%",
1008
+ width: "100%",
1009
+ textAlign: "center",
1010
+ opacity: interpolate(f, [3.5 * fps, 4 * fps], [0, 1], {
1011
+ extrapolateLeft: "clamp",
1012
+ extrapolateRight: "clamp",
1013
+ }),
1014
+ }}
1015
+ >
1016
+ <div
1017
+ style={{
1018
+ fontFamily: FONT,
1019
+ fontSize: 30,
1020
+ fontWeight: 600,
1021
+ color: ds.color.fgMuted,
1022
+ }}
1023
+ >
1024
+ It just works.
1025
+ </div>
1026
+ </div>
1027
+ </AbsoluteFill>
1028
+ );
1029
+ };
1030
+
1031
+ // ═══════════════════════════════════════════════════════════════
1032
+ // SCENE 6: EASY SETUP
1033
+ // ═══════════════════════════════════════════════════════════════
1034
+
1035
+ const STEPS = [
1036
+ { num: "1", text: "Pick a skill", sub: "Browse 1,000+ options" },
1037
+ { num: "2", text: "Copy the command", sub: "One-line install" },
1038
+ { num: "3", text: "You're done", sub: "AI knows it forever" },
1039
+ ];
1040
+
1041
+ const SetupScene: React.FC<SceneProps> = ({ frame, fps }) => {
1042
+ const opacity = sceneOp(frame, fps, SCENES.setup);
1043
+ if (opacity === 0) return null;
1044
+
1045
+ const f = lf(frame, fps, SCENES.setup[0]);
1046
+
1047
+ // Header
1048
+ const headerOp = interpolate(f, [0.3 * fps, 0.8 * fps], [0, 1], {
1049
+ extrapolateLeft: "clamp",
1050
+ extrapolateRight: "clamp",
1051
+ });
1052
+ const headerY = interpolate(f, [0.3 * fps, 0.8 * fps], [30, 0], {
1053
+ extrapolateLeft: "clamp",
1054
+ extrapolateRight: "clamp",
1055
+ easing: Easing.out(Easing.quad),
1056
+ });
1057
+
1058
+ // Big checkmark at end
1059
+ const allStepsDone = f > 4 * fps;
1060
+ const bigCheckSpring = spring({
1061
+ frame: f,
1062
+ fps,
1063
+ delay: Math.round(4 * fps),
1064
+ config: { damping: 8, stiffness: 100 },
1065
+ });
1066
+ const bigCheckScale = interpolate(bigCheckSpring, [0, 1], [0, 1]);
1067
+
1068
+ return (
1069
+ <AbsoluteFill style={{ opacity }}>
1070
+ {/* Header */}
1071
+ <div
1072
+ style={{
1073
+ position: "absolute",
1074
+ top: "10%",
1075
+ width: "100%",
1076
+ textAlign: "center",
1077
+ opacity: headerOp,
1078
+ transform: `translateY(${headerY}px)`,
1079
+ }}
1080
+ >
1081
+ <div
1082
+ style={{
1083
+ fontFamily: FONT,
1084
+ fontSize: 52,
1085
+ fontWeight: ds.text.bold,
1086
+ color: ds.color.fg,
1087
+ }}
1088
+ >
1089
+ Setup in{" "}
1090
+ <span style={{ color: ds.color.claude }}>3 steps</span>
1091
+ </div>
1092
+ </div>
1093
+
1094
+ {/* Steps */}
1095
+ <div
1096
+ style={{
1097
+ position: "absolute",
1098
+ top: "25%",
1099
+ left: "50%",
1100
+ transform: "translate(-50%, 0)",
1101
+ display: "flex",
1102
+ flexDirection: "column",
1103
+ gap: 28,
1104
+ width: "85%",
1105
+ maxWidth: 750,
1106
+ }}
1107
+ >
1108
+ {STEPS.map((step, i) => {
1109
+ const delay = Math.round((1 + i * 0.6) * fps);
1110
+ const stepSpring = spring({
1111
+ frame: f,
1112
+ fps,
1113
+ delay,
1114
+ config: { damping: 12, stiffness: 90 },
1115
+ });
1116
+ const y = interpolate(stepSpring, [0, 1], [60, 0]);
1117
+ const stepOp = interpolate(f, [delay / fps * fps, delay + 0.3 * fps], [0, 1], {
1118
+ extrapolateLeft: "clamp",
1119
+ extrapolateRight: "clamp",
1120
+ });
1121
+
1122
+ const isLast = i === STEPS.length - 1;
1123
+ const numBg = isLast
1124
+ ? "rgba(52,211,153,0.2)"
1125
+ : `rgba(139,92,246,0.2)`;
1126
+ const numBorder = isLast
1127
+ ? "rgba(52,211,153,0.4)"
1128
+ : "rgba(139,92,246,0.4)";
1129
+ const numColor = isLast ? ds.color.success : ds.color.accent;
1130
+
1131
+ return (
1132
+ <div
1133
+ key={step.num}
1134
+ style={{
1135
+ ...ds.glass.light,
1136
+ borderRadius: ds.radius["2xl"],
1137
+ boxShadow: ds.shadow.card,
1138
+ padding: "28px 32px",
1139
+ display: "flex",
1140
+ alignItems: "center",
1141
+ gap: 24,
1142
+ opacity: stepOp,
1143
+ transform: `translateY(${y}px)`,
1144
+ }}
1145
+ >
1146
+ {/* Step number */}
1147
+ <div
1148
+ style={{
1149
+ width: 56,
1150
+ height: 56,
1151
+ borderRadius: 16,
1152
+ background: numBg,
1153
+ border: `1.5px solid ${numBorder}`,
1154
+ display: "flex",
1155
+ alignItems: "center",
1156
+ justifyContent: "center",
1157
+ fontFamily: FONT,
1158
+ fontSize: 28,
1159
+ fontWeight: ds.text.bold,
1160
+ color: numColor,
1161
+ flexShrink: 0,
1162
+ }}
1163
+ >
1164
+ {isLast ? "✓" : step.num}
1165
+ </div>
1166
+ <div>
1167
+ <div
1168
+ style={{
1169
+ fontFamily: FONT,
1170
+ fontSize: 28,
1171
+ fontWeight: 700,
1172
+ color: ds.color.fg,
1173
+ }}
1174
+ >
1175
+ {step.text}
1176
+ </div>
1177
+ <div
1178
+ style={{
1179
+ fontFamily: FONT,
1180
+ fontSize: 20,
1181
+ fontWeight: 500,
1182
+ color: ds.color.fgMuted,
1183
+ marginTop: 4,
1184
+ }}
1185
+ >
1186
+ {step.sub}
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ );
1191
+ })}
1192
+ </div>
1193
+
1194
+ {/* Big checkmark celebration */}
1195
+ {allStepsDone && (
1196
+ <div
1197
+ style={{
1198
+ position: "absolute",
1199
+ bottom: "12%",
1200
+ left: "50%",
1201
+ transform: `translate(-50%, 0) scale(${bigCheckScale})`,
1202
+ textAlign: "center",
1203
+ }}
1204
+ >
1205
+ <div
1206
+ style={{
1207
+ fontFamily: FONT,
1208
+ fontSize: 36,
1209
+ fontWeight: 700,
1210
+ color: ds.color.success,
1211
+ }}
1212
+ >
1213
+ That's it. You're good.
1214
+ </div>
1215
+ </div>
1216
+ )}
1217
+ </AbsoluteFill>
1218
+ );
1219
+ };
1220
+
1221
+ // ═══════════════════════════════════════════════════════════════
1222
+ // SCENE 7: CTA
1223
+ // ═══════════════════════════════════════════════════════════════
1224
+
1225
+ const CTAScene: React.FC<SceneProps> = ({ frame, fps }) => {
1226
+ const opacity = sceneOp(frame, fps, SCENES.cta);
1227
+ if (opacity === 0) return null;
1228
+
1229
+ const f = lf(frame, fps, SCENES.cta[0]);
1230
+
1231
+ // "Try it now"
1232
+ const trySpring = spring({
1233
+ frame: f,
1234
+ fps,
1235
+ delay: Math.round(0.3 * fps),
1236
+ config: { damping: 12, stiffness: 90 },
1237
+ });
1238
+ const tryScale = interpolate(trySpring, [0, 1], [0.7, 1]);
1239
+ const tryOp = interpolate(f, [0.3 * fps, 0.7 * fps], [0, 1], {
1240
+ extrapolateLeft: "clamp",
1241
+ extrapolateRight: "clamp",
1242
+ });
1243
+
1244
+ // "skill.sh" URL
1245
+ const urlSpring = spring({
1246
+ frame: f,
1247
+ fps,
1248
+ delay: Math.round(1 * fps),
1249
+ config: { damping: 10, stiffness: 80 },
1250
+ });
1251
+ const urlScale = interpolate(urlSpring, [0, 1], [0.5, 1]);
1252
+ const urlGlow = interpolate(urlSpring, [0, 1], [0, 0.5]);
1253
+
1254
+ // "Link in comments"
1255
+ const linkOp = interpolate(f, [2 * fps, 2.5 * fps], [0, 1], {
1256
+ extrapolateLeft: "clamp",
1257
+ extrapolateRight: "clamp",
1258
+ });
1259
+ const linkY = interpolate(f, [2 * fps, 2.5 * fps], [20, 0], {
1260
+ extrapolateLeft: "clamp",
1261
+ extrapolateRight: "clamp",
1262
+ easing: Easing.out(Easing.quad),
1263
+ });
1264
+
1265
+ // Claude logo pulse
1266
+ const logoPulse = 0.9 + Math.sin((f / fps) * 2.5) * 0.1;
1267
+ const logoOp = interpolate(f, [2.5 * fps, 3 * fps], [0, 1], {
1268
+ extrapolateLeft: "clamp",
1269
+ extrapolateRight: "clamp",
1270
+ });
1271
+
1272
+ // Arrow bounce
1273
+ const arrowY = Math.sin((f / fps) * 3) * 8;
1274
+
1275
+ return (
1276
+ <AbsoluteFill style={{ opacity }}>
1277
+ {/* Ambient glow */}
1278
+ <div
1279
+ style={{
1280
+ position: "absolute",
1281
+ top: "35%",
1282
+ left: "50%",
1283
+ width: 600,
1284
+ height: 600,
1285
+ transform: "translate(-50%, -50%)",
1286
+ borderRadius: "50%",
1287
+ background: `radial-gradient(circle, rgba(139,92,246,${urlGlow * 0.25}) 0%, transparent 60%)`,
1288
+ filter: "blur(50px)",
1289
+ }}
1290
+ />
1291
+
1292
+ {/* "Try it now" */}
1293
+ <div
1294
+ style={{
1295
+ position: "absolute",
1296
+ top: "16%",
1297
+ width: "100%",
1298
+ textAlign: "center",
1299
+ opacity: tryOp,
1300
+ transform: `scale(${tryScale})`,
1301
+ }}
1302
+ >
1303
+ <div
1304
+ style={{
1305
+ fontFamily: FONT,
1306
+ fontSize: 44,
1307
+ fontWeight: 600,
1308
+ color: ds.color.fgMuted,
1309
+ letterSpacing: 2,
1310
+ }}
1311
+ >
1312
+ Try it now
1313
+ </div>
1314
+ </div>
1315
+
1316
+ {/* "skill.sh" big */}
1317
+ <div
1318
+ style={{
1319
+ position: "absolute",
1320
+ top: "30%",
1321
+ width: "100%",
1322
+ textAlign: "center",
1323
+ }}
1324
+ >
1325
+ <div style={{ position: "relative", display: "inline-block" }}>
1326
+ <div
1327
+ style={{
1328
+ fontFamily: FONT,
1329
+ fontSize: 110,
1330
+ fontWeight: ds.text.bold,
1331
+ letterSpacing: -3,
1332
+ transform: `scale(${urlScale})`,
1333
+ opacity: interpolate(urlSpring, [0, 0.2], [0, 1], {
1334
+ extrapolateLeft: "clamp",
1335
+ extrapolateRight: "clamp",
1336
+ }),
1337
+ background: ds.gradient.accentText,
1338
+ WebkitBackgroundClip: "text",
1339
+ WebkitTextFillColor: "transparent",
1340
+ backgroundClip: "text",
1341
+ }}
1342
+ >
1343
+ skill.sh
1344
+ </div>
1345
+ <div
1346
+ style={{
1347
+ position: "absolute",
1348
+ inset: "-60px -100px",
1349
+ borderRadius: "50%",
1350
+ background: `radial-gradient(ellipse, rgba(139,92,246,${urlGlow * 0.4}) 0%, transparent 70%)`,
1351
+ filter: "blur(50px)",
1352
+ zIndex: -1,
1353
+ }}
1354
+ />
1355
+ </div>
1356
+ </div>
1357
+
1358
+ {/* Separator line */}
1359
+ <div
1360
+ style={{
1361
+ position: "absolute",
1362
+ top: "52%",
1363
+ left: "50%",
1364
+ width: 200,
1365
+ height: 2,
1366
+ background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.06), transparent)",
1367
+ transform: `translate(-50%, 0) scaleX(${interpolate(urlSpring, [0, 1], [0, 1])})`,
1368
+ transformOrigin: "center",
1369
+ willChange: "transform",
1370
+ }}
1371
+ />
1372
+
1373
+ {/* "Link in comments" + arrow */}
1374
+ <div
1375
+ style={{
1376
+ position: "absolute",
1377
+ top: "58%",
1378
+ width: "100%",
1379
+ textAlign: "center",
1380
+ opacity: linkOp,
1381
+ transform: `translateY(${linkY}px)`,
1382
+ }}
1383
+ >
1384
+ <div
1385
+ style={{
1386
+ fontFamily: FONT,
1387
+ fontSize: 36,
1388
+ fontWeight: 600,
1389
+ color: ds.color.fg,
1390
+ }}
1391
+ >
1392
+ Link in comments
1393
+ </div>
1394
+ <div
1395
+ style={{
1396
+ fontSize: 48,
1397
+ marginTop: 16,
1398
+ transform: `translateY(${arrowY}px)`,
1399
+ }}
1400
+ >
1401
+ 👇
1402
+ </div>
1403
+ </div>
1404
+
1405
+ {/* Claude logo at bottom */}
1406
+ <div
1407
+ style={{
1408
+ position: "absolute",
1409
+ bottom: "10%",
1410
+ left: "50%",
1411
+ transform: `translate(-50%, 0) scale(${logoPulse})`,
1412
+ opacity: logoOp,
1413
+ }}
1414
+ >
1415
+ <ClaudeStarburst size={80} />
1416
+ <div
1417
+ style={{
1418
+ fontFamily: FONT,
1419
+ fontSize: 18,
1420
+ fontWeight: 600,
1421
+ color: ds.color.fgMuted,
1422
+ textAlign: "center",
1423
+ marginTop: 12,
1424
+ letterSpacing: 2,
1425
+ textTransform: "uppercase",
1426
+ }}
1427
+ >
1428
+ Powered by Claude
1429
+ </div>
1430
+ </div>
1431
+ </AbsoluteFill>
1432
+ );
1433
+ };
1434
+
1435
+ // ═══════════════════════════════════════════════════════════════
1436
+ // MAIN COMPOSITION
1437
+ // ═══════════════════════════════════════════════════════════════
1438
+
1439
+ export const SkillsReel: React.FC = () => {
1440
+ const frame = useCurrentFrame();
1441
+ const { fps } = useVideoConfig();
1442
+
1443
+ return (
1444
+ <AbsoluteFill
1445
+ style={{
1446
+ backgroundColor: ds.color.bg,
1447
+ fontFamily: FONT,
1448
+ }}
1449
+ >
1450
+ <Background frame={frame} />
1451
+ <HookScene frame={frame} fps={fps} />
1452
+ <PowersScene frame={frame} fps={fps} />
1453
+ <RevealScene frame={frame} fps={fps} />
1454
+ <CategoriesScene frame={frame} fps={fps} />
1455
+ <CompatScene frame={frame} fps={fps} />
1456
+ <SetupScene frame={frame} fps={fps} />
1457
+ <CTAScene frame={frame} fps={fps} />
1458
+ </AbsoluteFill>
1459
+ );
1460
+ };