agentreel 0.3.4 → 0.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.
package/src/CastVideo.tsx CHANGED
@@ -20,19 +20,18 @@ const TERM_BG = "#282a36";
20
20
  const TITLE_BAR = "#1e1f29";
21
21
  const CURSOR_COLOR = "#f8f8f2";
22
22
 
23
- const TITLE_DUR = 2.5;
24
- const TERMINAL_HIGHLIGHT_DUR = 4.5;
25
- const BROWSER_HIGHLIGHT_DUR = 7.0;
26
- const TRANSITION_DUR = 0.5;
27
- const END_DUR = 3.5;
23
+ // Mode-specific timing (synced with Root.tsx calculateMetadata)
24
+ const REEL_TIMING = { title: 2.5, termHighlight: 4.5, browserHighlight: 7.0, transition: 0.5, end: 3.5 };
25
+ const DEMO_TIMING = { title: 2.0, termHighlight: 12.0, browserHighlight: 10.0, transition: 0.4, end: 3.0 };
28
26
 
29
27
  const VIEWPORT_W = 1280;
30
28
  const VIEWPORT_H = 800;
31
29
  const VIDEO_AREA_W = 880;
32
30
  const VIDEO_AREA_H = 550; // 880 * 10/16
33
31
 
34
- function getHighlightDuration(h: Highlight): number {
35
- return h.videoSrc ? BROWSER_HIGHLIGHT_DUR : TERMINAL_HIGHLIGHT_DUR;
32
+ function getHighlightDuration(h: Highlight, mode: "reel" | "demo" = "reel"): number {
33
+ const t = mode === "demo" ? DEMO_TIMING : REEL_TIMING;
34
+ return h.videoSrc ? t.browserHighlight : t.termHighlight;
36
35
  }
37
36
 
38
37
  const SANS =
@@ -87,17 +86,21 @@ export const CastVideo: React.FC<CastProps> = ({
87
86
  endText,
88
87
  endUrl,
89
88
  gradient,
89
+ mode: rawMode,
90
90
  }) => {
91
91
  const frame = useCurrentFrame();
92
92
  const { fps, durationInFrames } = useVideoConfig();
93
+ const mode = rawMode || "reel";
94
+ const isDemo = mode === "demo";
95
+ const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
93
96
  const g = gradient || ["#0f0f1a", "#1a0f2e"];
94
97
 
95
- const titleFrames = Math.round(TITLE_DUR * fps);
96
- const endFrames = Math.round(END_DUR * fps);
98
+ const titleFrames = Math.round(timing.title * fps);
99
+ const endFrames = Math.round(timing.end * fps);
97
100
 
98
101
  // Compute per-highlight durations and cumulative offsets
99
102
  const hlDurations = highlights.map((h) =>
100
- Math.round(getHighlightDuration(h) * fps)
103
+ Math.round(getHighlightDuration(h, mode) * fps)
101
104
  );
102
105
  const hlOffsets: number[] = [];
103
106
  let cumulative = 0;
@@ -107,9 +110,11 @@ export const CastVideo: React.FC<CastProps> = ({
107
110
  }
108
111
 
109
112
  // Animated gradient — hue rotates slowly over time
110
- const gradAngle = interpolate(frame, [0, durationInFrames], [125, 200], {
111
- extrapolateRight: "clamp",
112
- });
113
+ const gradAngle = isDemo
114
+ ? 145 // static angle for demo
115
+ : interpolate(frame, [0, durationInFrames], [125, 200], {
116
+ extrapolateRight: "clamp",
117
+ });
113
118
 
114
119
  return (
115
120
  <AbsoluteFill
@@ -118,17 +123,18 @@ export const CastVideo: React.FC<CastProps> = ({
118
123
  backgroundSize: "200% 200%",
119
124
  }}
120
125
  >
121
- {/* Subtle animated glow blobs in background */}
122
- <AnimatedBackground frame={frame} duration={durationInFrames} />
126
+ {/* Subtle animated glow blobs reel only */}
127
+ {!isDemo && <AnimatedBackground frame={frame} duration={durationInFrames} />}
123
128
 
124
- <MusicTrack />
129
+ {/* Music — reel only */}
130
+ {!isDemo && <MusicTrack />}
125
131
 
126
132
  <Sequence durationInFrames={titleFrames}>
127
- <TitleCard title={title} subtitle={subtitle} />
133
+ <TitleCard title={title} subtitle={subtitle} isDemo={isDemo} />
128
134
  </Sequence>
129
135
 
130
136
  {highlights.map((h, i) => {
131
- const dur = getHighlightDuration(h);
137
+ const dur = getHighlightDuration(h, mode);
132
138
  return (
133
139
  <Sequence
134
140
  key={i}
@@ -142,6 +148,7 @@ export const CastVideo: React.FC<CastProps> = ({
142
148
  total={highlights.length}
143
149
  transition={TRANSITIONS[i % TRANSITIONS.length]}
144
150
  durationSec={dur}
151
+ isDemo={isDemo}
145
152
  />
146
153
  ) : (
147
154
  <HighlightClip
@@ -150,6 +157,7 @@ export const CastVideo: React.FC<CastProps> = ({
150
157
  total={highlights.length}
151
158
  transition={TRANSITIONS[i % TRANSITIONS.length]}
152
159
  durationSec={dur}
160
+ isDemo={isDemo}
153
161
  />
154
162
  )}
155
163
  </Sequence>
@@ -160,7 +168,7 @@ export const CastVideo: React.FC<CastProps> = ({
160
168
  from={titleFrames + cumulative}
161
169
  durationInFrames={endFrames}
162
170
  >
163
- <EndCard text={endText || title} url={endUrl} />
171
+ <EndCard text={endText || title} url={endUrl} isDemo={isDemo} />
164
172
  </Sequence>
165
173
  </AbsoluteFill>
166
174
  );
@@ -408,12 +416,14 @@ const TextOverlay: React.FC<{ text: string; durationSec: number }> = ({
408
416
 
409
417
  // ─── Title Card ───────────────────────────────────────────
410
418
 
411
- const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
419
+ const TitleCard: React.FC<{ title: string; subtitle?: string; isDemo?: boolean }> = ({
412
420
  title,
413
421
  subtitle,
422
+ isDemo,
414
423
  }) => {
415
424
  const frame = useCurrentFrame();
416
425
  const { fps } = useVideoConfig();
426
+ const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
417
427
 
418
428
  const titleSpring = spring({ fps, frame, config: { damping: 14 } });
419
429
  const subSpring = spring({
@@ -423,11 +433,11 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
423
433
  });
424
434
  const fadeOut = interpolate(
425
435
  frame,
426
- [fps * (TITLE_DUR - TRANSITION_DUR), fps * TITLE_DUR],
436
+ [fps * (timing.title - timing.transition), fps * timing.title],
427
437
  [1, 0],
428
438
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
429
439
  );
430
- const titleZoom = interpolate(frame, [0, fps * TITLE_DUR], [1, 1.08], {
440
+ const titleZoom = isDemo ? 1 : interpolate(frame, [0, fps * timing.title], [1, 1.08], {
431
441
  extrapolateLeft: "clamp",
432
442
  extrapolateRight: "clamp",
433
443
  });
@@ -449,7 +459,7 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
449
459
  <div
450
460
  style={{
451
461
  fontFamily: SANS,
452
- fontSize: 76,
462
+ fontSize: isDemo ? 56 : 76,
453
463
  fontWeight: 800,
454
464
  color: WHITE,
455
465
  letterSpacing: -3,
@@ -463,7 +473,7 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
463
473
  transform: `translateY(${interpolate(subSpring, [0, 1], [20, 0])}px)`,
464
474
  opacity: subSpring,
465
475
  fontFamily: SANS,
466
- fontSize: 28,
476
+ fontSize: isDemo ? 24 : 28,
467
477
  color: DIM,
468
478
  marginTop: 16,
469
479
  letterSpacing: 1,
@@ -485,82 +495,79 @@ const HighlightClip: React.FC<{
485
495
  total: number;
486
496
  transition: TransitionStyle;
487
497
  durationSec: number;
488
- }> = ({ highlight, index, total, transition, durationSec }) => {
498
+ isDemo?: boolean;
499
+ }> = ({ highlight, index, total, transition, durationSec, isDemo }) => {
489
500
  const frame = useCurrentFrame();
490
- const { fps } = useVideoConfig();
501
+ const { fps, width } = useVideoConfig();
502
+ const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
491
503
 
492
- // Entry animation — varies per clip
504
+ // Entry animation — simpler in demo mode
493
505
  const enterSpring = spring({
494
506
  fps,
495
507
  frame,
496
- config: { damping: 18, stiffness: 80 },
508
+ config: isDemo ? { damping: 22, stiffness: 120 } : { damping: 18, stiffness: 80 },
497
509
  });
498
- const entry = getEntryTransform(transition, enterSpring);
510
+ const entry = isDemo
511
+ ? { scale: 1, x: 0, y: 0 } // no transform in demo
512
+ : getEntryTransform(transition, enterSpring);
499
513
 
500
514
  // Fade transitions
501
- const fadeIn = interpolate(frame, [0, fps * TRANSITION_DUR], [0, 1], {
515
+ const fadeIn = interpolate(frame, [0, fps * timing.transition], [0, 1], {
502
516
  extrapolateLeft: "clamp",
503
517
  extrapolateRight: "clamp",
504
518
  });
505
519
  const fadeOut = interpolate(
506
520
  frame,
507
- [fps * (durationSec - TRANSITION_DUR), fps * durationSec],
521
+ [fps * (durationSec - timing.transition), fps * durationSec],
508
522
  [1, 0],
509
523
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
510
524
  );
511
525
  const opacity = Math.min(fadeIn, fadeOut);
512
526
 
513
- // Zoom in/out cycle
514
- const zoomIn = interpolate(
515
- frame,
516
- [fps * 0.8, fps * 2.0],
517
- [1, 1.12],
518
- {
519
- extrapolateLeft: "clamp",
520
- extrapolateRight: "clamp",
521
- easing: Easing.out(Easing.cubic),
522
- }
523
- );
524
- const zoomOut = interpolate(
525
- frame,
526
- [fps * 2.5, fps * (durationSec - 0.5)],
527
- [1.12, 1.02],
528
- {
529
- extrapolateLeft: "clamp",
530
- extrapolateRight: "clamp",
531
- easing: Easing.inOut(Easing.cubic),
532
- }
533
- );
534
- const zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
527
+ // Zoom/pan — reel only
528
+ let zoom = 1;
529
+ let panY = 0;
530
+ if (!isDemo) {
531
+ const zoomIn = interpolate(frame, [fps * 0.8, fps * 2.0], [1, 1.12], {
532
+ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic),
533
+ });
534
+ const zoomOut = interpolate(frame, [fps * 2.5, fps * (durationSec - 0.5)], [1.12, 1.02], {
535
+ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic),
536
+ });
537
+ zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
538
+ panY = interpolate(frame, [fps * 0.8, fps * 2.0, fps * (durationSec - 1.0)], [0, -15, 5], {
539
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
540
+ });
541
+ }
535
542
 
536
- // Vertical pan
537
- const panY = interpolate(
538
- frame,
539
- [fps * 0.8, fps * 2.0, fps * (durationSec - 1.0)],
540
- [0, -15, 5],
541
- { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
542
- );
543
+ // Line timing — faster reveal in demo (more lines to show)
544
+ const lineDelay = isDemo ? fps * 0.08 : fps * 0.15;
545
+ const firstLineFrame = isDemo ? fps * 0.25 : fps * 0.35;
543
546
 
544
- // Line timing
545
- const lineDelay = fps * 0.15;
546
- const firstLineFrame = fps * 0.35;
547
+ const lines = highlight.lines || [];
547
548
 
548
549
  // Cursor tracking
549
- const lastVisibleLineIdx = highlight.lines.findIndex((_, i) => {
550
+ const lastVisibleLineIdx = lines.findIndex((_, i) => {
550
551
  return frame < firstLineFrame + (i + 1) * lineDelay;
551
552
  });
552
553
  const cursorLineIdx =
553
554
  lastVisibleLineIdx === -1
554
- ? highlight.lines.length - 1
555
+ ? lines.length - 1
555
556
  : Math.max(0, lastVisibleLineIdx - 1);
556
557
 
558
+ // Terminal sizing — wider in demo mode (landscape)
559
+ const termWidth = isDemo ? width - 160 : 820;
560
+ const termFontSize = isDemo ? 14 : 16;
561
+ const termMinHeight = isDemo ? 500 : 280;
562
+ const termPadding = isDemo ? "16px 20px" : "20px 24px";
563
+
557
564
  return (
558
565
  <AbsoluteFill style={{ opacity }}>
559
- {/* Label + progress dots */}
566
+ {/* Chapter label */}
560
567
  <div
561
568
  style={{
562
569
  position: "absolute",
563
- top: 45,
570
+ top: isDemo ? 24 : 45,
564
571
  left: 0,
565
572
  width: "100%",
566
573
  textAlign: "center",
@@ -570,36 +577,31 @@ const HighlightClip: React.FC<{
570
577
  <span
571
578
  style={{
572
579
  fontFamily: MONO,
573
- fontSize: 13,
580
+ fontSize: isDemo ? 15 : 13,
574
581
  color: ACCENT,
575
- letterSpacing: 4,
582
+ letterSpacing: isDemo ? 3 : 4,
576
583
  textTransform: "uppercase",
577
- opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
584
+ opacity: interpolate(enterSpring, [0, 1], [0, isDemo ? 0.8 : 0.6]),
578
585
  }}
579
586
  >
580
587
  {highlight.label}
581
588
  </span>
582
- <div
583
- style={{
584
- marginTop: 10,
585
- display: "flex",
586
- justifyContent: "center",
587
- gap: 8,
588
- }}
589
- >
590
- {Array.from({ length: total }).map((_, i) => (
591
- <div
592
- key={i}
593
- style={{
594
- width: i === index ? 24 : 8,
595
- height: 6,
596
- borderRadius: 3,
597
- backgroundColor:
598
- i === index ? ACCENT : "rgba(255,255,255,0.12)",
599
- }}
600
- />
601
- ))}
602
- </div>
589
+ {/* Progress dots — reel only */}
590
+ {!isDemo && (
591
+ <div style={{ marginTop: 10, display: "flex", justifyContent: "center", gap: 8 }}>
592
+ {Array.from({ length: total }).map((_, i) => (
593
+ <div
594
+ key={i}
595
+ style={{
596
+ width: i === index ? 24 : 8,
597
+ height: 6,
598
+ borderRadius: 3,
599
+ backgroundColor: i === index ? ACCENT : "rgba(255,255,255,0.12)",
600
+ }}
601
+ />
602
+ ))}
603
+ </div>
604
+ )}
603
605
  </div>
604
606
 
605
607
  {/* Terminal window */}
@@ -607,20 +609,23 @@ const HighlightClip: React.FC<{
607
609
  style={{
608
610
  justifyContent: "center",
609
611
  alignItems: "center",
610
- padding: 40,
611
- paddingTop: 100,
612
- paddingBottom: 100,
612
+ padding: isDemo ? 20 : 40,
613
+ paddingTop: isDemo ? 60 : 100,
614
+ paddingBottom: isDemo ? 40 : 100,
613
615
  }}
614
616
  >
615
617
  <div
616
618
  style={{
617
- transform: `scale(${entry.scale * zoom}) translate(${entry.x}px, ${entry.y + panY}px)`,
619
+ transform: isDemo
620
+ ? `scale(${enterSpring})`
621
+ : `scale(${entry.scale * zoom}) translate(${entry.x}px, ${entry.y + panY}px)`,
618
622
  transformOrigin: "center center",
619
- width: 820,
620
- borderRadius: 14,
623
+ width: termWidth,
624
+ borderRadius: isDemo ? 10 : 14,
621
625
  overflow: "hidden",
622
- boxShadow:
623
- "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
626
+ boxShadow: isDemo
627
+ ? "0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)"
628
+ : "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
624
629
  }}
625
630
  >
626
631
  {/* macOS title bar */}
@@ -633,30 +638,9 @@ const HighlightClip: React.FC<{
633
638
  gap: 8,
634
639
  }}
635
640
  >
636
- <div
637
- style={{
638
- width: 12,
639
- height: 12,
640
- borderRadius: 6,
641
- backgroundColor: "#ff5555",
642
- }}
643
- />
644
- <div
645
- style={{
646
- width: 12,
647
- height: 12,
648
- borderRadius: 6,
649
- backgroundColor: "#f1fa8c",
650
- }}
651
- />
652
- <div
653
- style={{
654
- width: 12,
655
- height: 12,
656
- borderRadius: 6,
657
- backgroundColor: "#50fa7b",
658
- }}
659
- />
641
+ <div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#ff5555" }} />
642
+ <div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#f1fa8c" }} />
643
+ <div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#50fa7b" }} />
660
644
  <div style={{ flex: 1 }} />
661
645
  </div>
662
646
 
@@ -664,11 +648,11 @@ const HighlightClip: React.FC<{
664
648
  <div
665
649
  style={{
666
650
  backgroundColor: TERM_BG,
667
- padding: "20px 24px",
668
- minHeight: 280,
651
+ padding: termPadding,
652
+ minHeight: termMinHeight,
669
653
  }}
670
654
  >
671
- {highlight.lines.map((line, lineIdx) => {
655
+ {lines.map((line, lineIdx) => {
672
656
  const lineFrame = firstLineFrame + lineIdx * lineDelay;
673
657
  const lineSpring = spring({
674
658
  fps,
@@ -676,19 +660,18 @@ const HighlightClip: React.FC<{
676
660
  config: { damping: 20, stiffness: 120 },
677
661
  });
678
662
  const lineOpacity = interpolate(lineSpring, [0, 1], [0, 1]);
679
- const lineX = interpolate(lineSpring, [0, 1], [12, 0]);
663
+ const lineX = isDemo ? 0 : interpolate(lineSpring, [0, 1], [12, 0]);
680
664
 
681
665
  // Strip leading "$ " from text — renderer adds its own $ prefix
682
666
  const cleanText = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
683
667
  let displayText = cleanText;
684
668
  let isTyping = false;
685
669
  if (line.isPrompt) {
686
- const typingEnd = lineFrame + fps * 0.6;
670
+ const typingDur = isDemo ? 0.8 : 0.6;
671
+ const typingEnd = lineFrame + fps * typingDur;
687
672
  if (frame < typingEnd) {
688
673
  const progress = interpolate(
689
- frame,
690
- [lineFrame, typingEnd],
691
- [0, 1],
674
+ frame, [lineFrame, typingEnd], [0, 1],
692
675
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
693
676
  );
694
677
  const chars = Math.floor(progress * cleanText.length);
@@ -697,14 +680,11 @@ const HighlightClip: React.FC<{
697
680
  }
698
681
  }
699
682
 
700
- const isZoomed = highlight.zoomLine === lineIdx;
683
+ const isZoomed = !isDemo && highlight.zoomLine === lineIdx;
701
684
  const lineZoom = isZoomed
702
- ? interpolate(
703
- frame,
704
- [lineFrame + fps * 0.5, lineFrame + fps * 1],
705
- [1, 1.05],
706
- { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
707
- )
685
+ ? interpolate(frame, [lineFrame + fps * 0.5, lineFrame + fps * 1], [1, 1.05], {
686
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
687
+ })
708
688
  : 1;
709
689
 
710
690
  const showCursor = lineIdx === cursorLineIdx;
@@ -717,8 +697,8 @@ const HighlightClip: React.FC<{
717
697
  transform: `translateX(${lineX}px) scale(${lineZoom})`,
718
698
  transformOrigin: "left center",
719
699
  fontFamily: MONO,
720
- fontSize: 16,
721
- lineHeight: 1.7,
700
+ fontSize: termFontSize,
701
+ lineHeight: isDemo ? 1.6 : 1.7,
722
702
  color: line.dim ? DIM : line.color || WHITE,
723
703
  fontWeight: line.bold ? 700 : 400,
724
704
  whiteSpace: "pre",
@@ -738,11 +718,11 @@ const HighlightClip: React.FC<{
738
718
  </div>
739
719
  </AbsoluteFill>
740
720
 
741
- {/* Mouse pointer — appears at start of each clip */}
742
- <MousePointer />
721
+ {/* Mouse pointer — reel only */}
722
+ {!isDemo && <MousePointer />}
743
723
 
744
- {/* Text overlay */}
745
- {highlight.overlay && (
724
+ {/* Text overlay — reel only */}
725
+ {!isDemo && highlight.overlay && (
746
726
  <TextOverlay text={highlight.overlay} durationSec={durationSec} />
747
727
  )}
748
728
  </AbsoluteFill>
@@ -757,7 +737,8 @@ const BrowserHighlightClip: React.FC<{
757
737
  total: number;
758
738
  transition: TransitionStyle;
759
739
  durationSec: number;
760
- }> = ({ highlight, index, total, transition, durationSec }) => {
740
+ isDemo?: boolean;
741
+ }> = ({ highlight, index, total, transition, durationSec, isDemo }) => {
761
742
  const frame = useCurrentFrame();
762
743
  const { fps } = useVideoConfig();
763
744
 
@@ -768,57 +749,50 @@ const BrowserHighlightClip: React.FC<{
768
749
  });
769
750
  const entry = getEntryTransform(transition, enterSpring);
770
751
 
771
- const fadeIn = interpolate(frame, [0, fps * TRANSITION_DUR], [0, 1], {
752
+ const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
753
+
754
+ const fadeIn = interpolate(frame, [0, fps * timing.transition], [0, 1], {
772
755
  extrapolateLeft: "clamp",
773
756
  extrapolateRight: "clamp",
774
757
  });
775
758
  const fadeOut = interpolate(
776
759
  frame,
777
- [fps * (durationSec - TRANSITION_DUR), fps * durationSec],
760
+ [fps * (durationSec - timing.transition), fps * durationSec],
778
761
  [1, 0],
779
762
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
780
763
  );
781
764
  const opacity = Math.min(fadeIn, fadeOut);
782
765
 
783
- // Focal zoom — applied to video content only, not browser chrome
766
+ // Focal zoom — reel only
784
767
  const fx = highlight.focusX ?? 0.5;
785
768
  const fy = highlight.focusY ?? 0.5;
786
769
 
787
- const focalZoomIn = interpolate(frame, [fps * 1.0, fps * 3.0], [1, 1.15], {
788
- extrapolateLeft: "clamp",
789
- extrapolateRight: "clamp",
790
- easing: Easing.out(Easing.cubic),
791
- });
792
- const focalZoomOut = interpolate(
793
- frame,
794
- [fps * 3.5, fps * (durationSec - 0.5)],
795
- [1.15, 1.02],
796
- {
797
- extrapolateLeft: "clamp",
798
- extrapolateRight: "clamp",
799
- easing: Easing.inOut(Easing.cubic),
800
- }
801
- );
802
- const focalZoom = frame < fps * 3.5 ? focalZoomIn : focalZoomOut;
803
-
804
- // Entry pan
805
- const panY = interpolate(
806
- frame,
807
- [fps * 1.0, fps * 3.0, fps * (durationSec - 1.0)],
808
- [0, -10, 5],
809
- { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
810
- );
770
+ let focalZoom = 1;
771
+ let panY = 0;
772
+ if (!isDemo) {
773
+ const focalZoomIn = interpolate(frame, [fps * 1.0, fps * 3.0], [1, 1.15], {
774
+ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic),
775
+ });
776
+ const focalZoomOut = interpolate(frame, [fps * 3.5, fps * (durationSec - 0.5)], [1.15, 1.02], {
777
+ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic),
778
+ });
779
+ focalZoom = frame < fps * 3.5 ? focalZoomIn : focalZoomOut;
780
+ panY = interpolate(frame, [fps * 1.0, fps * 3.0, fps * (durationSec - 1.0)], [0, -10, 5], {
781
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
782
+ });
783
+ }
811
784
 
812
785
  const videoSrc = highlight.videoSrc!;
813
786
  const startFrom = Math.round((highlight.videoStartSec || 0) * fps);
787
+ const browserWidth = isDemo ? 1600 : 880;
814
788
 
815
789
  return (
816
790
  <AbsoluteFill style={{ opacity }}>
817
- {/* Label + progress dots */}
791
+ {/* Chapter label */}
818
792
  <div
819
793
  style={{
820
794
  position: "absolute",
821
- top: 45,
795
+ top: isDemo ? 24 : 45,
822
796
  left: 0,
823
797
  width: "100%",
824
798
  textAlign: "center",
@@ -828,36 +802,30 @@ const BrowserHighlightClip: React.FC<{
828
802
  <span
829
803
  style={{
830
804
  fontFamily: MONO,
831
- fontSize: 13,
805
+ fontSize: isDemo ? 15 : 13,
832
806
  color: ACCENT,
833
- letterSpacing: 4,
807
+ letterSpacing: isDemo ? 3 : 4,
834
808
  textTransform: "uppercase",
835
- opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
809
+ opacity: interpolate(enterSpring, [0, 1], [0, isDemo ? 0.8 : 0.6]),
836
810
  }}
837
811
  >
838
812
  {highlight.label}
839
813
  </span>
840
- <div
841
- style={{
842
- marginTop: 10,
843
- display: "flex",
844
- justifyContent: "center",
845
- gap: 8,
846
- }}
847
- >
848
- {Array.from({ length: total }).map((_, i) => (
849
- <div
850
- key={i}
851
- style={{
852
- width: i === index ? 24 : 8,
853
- height: 6,
854
- borderRadius: 3,
855
- backgroundColor:
856
- i === index ? ACCENT : "rgba(255,255,255,0.12)",
857
- }}
858
- />
859
- ))}
860
- </div>
814
+ {!isDemo && (
815
+ <div style={{ marginTop: 10, display: "flex", justifyContent: "center", gap: 8 }}>
816
+ {Array.from({ length: total }).map((_, i) => (
817
+ <div
818
+ key={i}
819
+ style={{
820
+ width: i === index ? 24 : 8,
821
+ height: 6,
822
+ borderRadius: 3,
823
+ backgroundColor: i === index ? ACCENT : "rgba(255,255,255,0.12)",
824
+ }}
825
+ />
826
+ ))}
827
+ </div>
828
+ )}
861
829
  </div>
862
830
 
863
831
  {/* Browser window */}
@@ -865,20 +833,23 @@ const BrowserHighlightClip: React.FC<{
865
833
  style={{
866
834
  justifyContent: "center",
867
835
  alignItems: "center",
868
- padding: 40,
869
- paddingTop: 100,
870
- paddingBottom: 100,
836
+ padding: isDemo ? 20 : 40,
837
+ paddingTop: isDemo ? 60 : 100,
838
+ paddingBottom: isDemo ? 40 : 100,
871
839
  }}
872
840
  >
873
841
  <div
874
842
  style={{
875
- transform: `scale(${entry.scale}) translate(${entry.x}px, ${entry.y + panY}px)`,
843
+ transform: isDemo
844
+ ? `scale(${enterSpring})`
845
+ : `scale(${entry.scale}) translate(${entry.x}px, ${entry.y + panY}px)`,
876
846
  transformOrigin: "center center",
877
- width: 880,
878
- borderRadius: 14,
847
+ width: browserWidth,
848
+ borderRadius: isDemo ? 10 : 14,
879
849
  overflow: "hidden",
880
- boxShadow:
881
- "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
850
+ boxShadow: isDemo
851
+ ? "0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)"
852
+ : "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
882
853
  }}
883
854
  >
884
855
  {/* Browser chrome */}
@@ -946,8 +917,8 @@ const BrowserHighlightClip: React.FC<{
946
917
  </div>
947
918
  </AbsoluteFill>
948
919
 
949
- {/* Text overlay */}
950
- {highlight.overlay && (
920
+ {/* Text overlay — reel only */}
921
+ {!isDemo && highlight.overlay && (
951
922
  <TextOverlay text={highlight.overlay} durationSec={durationSec} />
952
923
  )}
953
924
  </AbsoluteFill>
@@ -1080,9 +1051,10 @@ const BrowserCursor: React.FC<{
1080
1051
 
1081
1052
  // ─── End Card (CTA) ───────────────────────────────────────
1082
1053
 
1083
- const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1054
+ const EndCard: React.FC<{ text: string; url?: string; isDemo?: boolean }> = ({ text, url, isDemo }) => {
1084
1055
  const frame = useCurrentFrame();
1085
1056
  const { fps } = useVideoConfig();
1057
+ const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
1086
1058
 
1087
1059
  const cmdSpring = spring({ fps, frame, config: { damping: 14 } });
1088
1060
  const urlSpring = spring({
@@ -1095,7 +1067,7 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1095
1067
  frame: Math.max(0, frame - 18),
1096
1068
  config: { damping: 14 },
1097
1069
  });
1098
- const endZoom = interpolate(frame, [0, fps * END_DUR], [1.05, 1], {
1070
+ const endZoom = isDemo ? 1 : interpolate(frame, [0, fps * timing.end], [1.05, 1], {
1099
1071
  extrapolateLeft: "clamp",
1100
1072
  extrapolateRight: "clamp",
1101
1073
  easing: Easing.out(Easing.cubic),
@@ -1117,8 +1089,8 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1117
1089
  >
1118
1090
  <div
1119
1091
  style={{
1120
- fontFamily: MONO,
1121
- fontSize: 30,
1092
+ fontFamily: isDemo ? SANS : MONO,
1093
+ fontSize: isDemo ? 28 : 30,
1122
1094
  color: WHITE,
1123
1095
  padding: "18px 36px",
1124
1096
  borderRadius: 14,
@@ -1127,9 +1099,9 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1127
1099
  boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
1128
1100
  }}
1129
1101
  >
1130
- <span style={{ color: ACCENT }}>$ </span>
1102
+ {!isDemo && <span style={{ color: ACCENT }}>$ </span>}
1131
1103
  {text}
1132
- <Cursor visible blink />
1104
+ {!isDemo && <Cursor visible blink />}
1133
1105
  </div>
1134
1106
  </div>
1135
1107
 
@@ -1140,7 +1112,7 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1140
1112
  opacity: urlSpring,
1141
1113
  transform: `translateY(${interpolate(urlSpring, [0, 1], [15, 0])}px)`,
1142
1114
  fontFamily: SANS,
1143
- fontSize: 20,
1115
+ fontSize: isDemo ? 18 : 20,
1144
1116
  color: DIM,
1145
1117
  letterSpacing: 0.5,
1146
1118
  }}