@bendyline/squisq-react 2.11.10 → 2.11.12

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LinearDocView
3
- } from "./chunk-CLP3VWJ2.js";
3
+ } from "./chunk-P3FJ3YKS.js";
4
4
 
5
5
  // src/PaginatedDocView.tsx
6
6
  import {
@@ -9,7 +9,7 @@ import {
9
9
  TreeLayer,
10
10
  VideoLayer,
11
11
  getTransitionClass
12
- } from "./chunk-W6MKRWL6.js";
12
+ } from "./chunk-WHVSQ42V.js";
13
13
  import {
14
14
  useAutoSurface
15
15
  } from "./chunk-TT6ENR6T.js";
@@ -24,6 +24,7 @@ import {
24
24
  // src/BlockRenderer.tsx
25
25
  import { useId, useLayoutEffect, useRef, useState } from "react";
26
26
  import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
27
+ import { isGeometryBoundTemplate } from "@bendyline/squisq/doc";
27
28
 
28
29
  // src/utils/textFit.ts
29
30
  function largestFittingTextScale(fits, maximum = 2) {
@@ -138,6 +139,7 @@ function BlockRenderer({
138
139
  const clipId = `vb-clip-${instanceId}-${block.id}`;
139
140
  const svgRef = useRef(null);
140
141
  const [textScale, setTextScale] = useState(1);
142
+ const growText = growTextToFit && !isGeometryBoundTemplate(block.template);
141
143
  useLayoutEffect(() => {
142
144
  const svg = svgRef.current;
143
145
  let cancelled = false;
@@ -145,7 +147,7 @@ function BlockRenderer({
145
147
  const measure = () => {
146
148
  if (cancelled) return;
147
149
  const targets = Array.from(svg.querySelectorAll("[data-squisq-text-fit]"));
148
- if (!growTextToFit || targets.length === 0) {
150
+ if (!growText || targets.length === 0) {
149
151
  applyTextScale(targets, 1);
150
152
  setTextScale(1);
151
153
  return;
@@ -175,7 +177,7 @@ function BlockRenderer({
175
177
  return () => {
176
178
  cancelled = true;
177
179
  };
178
- }, [block.layers, growTextToFit, viewport.height, viewport.width]);
180
+ }, [block.layers, growText, viewport.height, viewport.width]);
179
181
  return /* @__PURE__ */ jsxs(
180
182
  "svg",
181
183
  {
@@ -186,7 +188,7 @@ function BlockRenderer({
186
188
  preserveAspectRatio: "xMidYMid meet",
187
189
  overflow: "hidden",
188
190
  "data-block-id": block.id,
189
- "data-squisq-text-scale": growTextToFit ? textScale : void 0,
191
+ "data-squisq-text-scale": growText ? textScale : void 0,
190
192
  children: [
191
193
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
192
194
  /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx(
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-CLP3VWJ2.js";
4
+ } from "./chunk-P3FJ3YKS.js";
5
5
  import {
6
6
  isCaptureDrivenVideo,
7
7
  seekVideoToFrame
8
- } from "./chunk-W6MKRWL6.js";
8
+ } from "./chunk-WHVSQ42V.js";
9
9
  import {
10
10
  useAudioSync,
11
11
  useDocPlayback,
12
12
  useMediaSchedule,
13
13
  useViewportOrientation
14
- } from "./chunk-MVJQL2W2.js";
14
+ } from "./chunk-U2FIZWAL.js";
15
15
  import {
16
16
  useAutoSurface
17
17
  } from "./chunk-TT6ENR6T.js";
@@ -451,16 +451,26 @@ function useMediaClipDurations(schedule, basePath, mediaProviderOverride) {
451
451
  import { useMemo as useMemo2 } from "react";
452
452
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
453
453
  import { jsx as jsx2 } from "react/jsx-runtime";
454
- function positionStyle(position) {
454
+ function positionStyle(position, portrait) {
455
455
  switch (position) {
456
456
  case "top":
457
- return { top: "8%" };
457
+ return { top: portrait ? "12%" : "8%" };
458
458
  case "center":
459
459
  return { top: "50%", transform: "translateY(-50%)" };
460
460
  default:
461
- return { bottom: "18%" };
461
+ return { bottom: portrait ? "24%" : "16%" };
462
462
  }
463
463
  }
464
+ function contrastText(hex) {
465
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
466
+ if (!m) return "#ffffff";
467
+ const n = parseInt(m[1], 16);
468
+ const r = n >> 16 & 255;
469
+ const g = n >> 8 & 255;
470
+ const b = n & 255;
471
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
472
+ return luma > 150 ? "#111111" : "#ffffff";
473
+ }
464
474
  var TARGET_CHUNK_SIZE = 4;
465
475
  var MIN_CHUNK_SIZE = 2;
466
476
  var MAX_CHUNK_SIZE = 6;
@@ -526,7 +536,7 @@ function SocialCaptionOverlay({
526
536
  className: "social-caption-overlay",
527
537
  style: {
528
538
  position: "absolute",
529
- ...positionStyle(position),
539
+ ...positionStyle(position, (viewport?.height ?? 0) > (viewport?.width ?? 1)),
530
540
  left: 0,
531
541
  right: 0,
532
542
  zIndex: 50,
@@ -579,22 +589,25 @@ function SocialCaptionOverlay({
579
589
  }
580
590
  const primaryColor = theme?.colors?.primary ?? "#5b9bd5";
581
591
  const fontFamily = theme?.typography?.titleFont ? resolveFontFamily(theme.typography.titleFont, '"PT Serif", Georgia, serif') : '"PT Serif", Georgia, serif';
592
+ const viewportWidth = viewport?.width ?? 1280;
582
593
  const viewportHeight = viewport?.height ?? 720;
583
- const baseFontSize = Math.round(viewportHeight * 0.055);
584
- const fontSize = Math.max(24, Math.min(72, baseFontSize));
594
+ const portrait = viewportHeight > viewportWidth;
595
+ const baseFontSize = Math.min(viewportHeight * 0.055, viewportWidth * 0.082);
596
+ const fontSize = Math.max(22, Math.min(96, Math.round(baseFontSize)));
585
597
  return /* @__PURE__ */ jsx2(
586
598
  "div",
587
599
  {
588
600
  className: "social-caption-overlay",
589
601
  style: {
590
602
  position: "absolute",
591
- ...positionStyle(position),
603
+ ...positionStyle(position, portrait),
592
604
  left: 0,
593
605
  right: 0,
594
606
  zIndex: 50,
595
607
  pointerEvents: "none",
596
- textAlign: "center",
597
- padding: "0 8%",
608
+ display: "flex",
609
+ justifyContent: "center",
610
+ padding: portrait ? "0 7%" : "0 10%",
598
611
  boxSizing: "border-box",
599
612
  opacity: 1,
600
613
  transition: "opacity 0.15s ease-in-out"
@@ -602,24 +615,44 @@ function SocialCaptionOverlay({
602
615
  children: /* @__PURE__ */ jsx2(
603
616
  "div",
604
617
  {
618
+ className: "social-caption-overlay__chunk",
605
619
  style: {
606
- display: "inline-block",
607
- lineHeight: 1.3
620
+ display: "inline-flex",
621
+ flexWrap: "wrap",
622
+ justifyContent: "center",
623
+ alignItems: "baseline",
624
+ maxWidth: "100%",
625
+ rowGap: "0.12em",
626
+ columnGap: "0.28em",
627
+ padding: "0.22em 0.55em",
628
+ borderRadius: "0.4em",
629
+ background: "rgba(10, 10, 12, 0.42)",
630
+ backdropFilter: "blur(8px)",
631
+ WebkitBackdropFilter: "blur(8px)",
632
+ fontFamily,
633
+ fontSize: `${fontSize}px`,
634
+ lineHeight: 1.18,
635
+ textTransform: "uppercase",
636
+ letterSpacing: "0.01em",
637
+ whiteSpace: "normal"
608
638
  },
609
639
  children: activeChunk.words.map((word, i) => {
610
640
  const isActive = i === activeWordIndex;
611
641
  return /* @__PURE__ */ jsx2(
612
642
  "span",
613
643
  {
644
+ className: isActive ? "social-caption-overlay__word is-active" : "social-caption-overlay__word",
614
645
  style: {
615
- fontFamily,
616
- fontSize: `${fontSize}px`,
617
- fontWeight: isActive ? 800 : 600,
618
- color: isActive ? primaryColor : "rgba(255, 255, 255, 0.9)",
619
- textShadow: "0 2px 8px rgba(0,0,0,0.7), 0 0 20px rgba(0,0,0,0.4)",
620
- marginRight: i < activeChunk.words.length - 1 ? "0.3em" : void 0,
621
- transition: "color 0.1s ease, font-weight 0.1s ease",
622
- textTransform: "uppercase"
646
+ display: "inline-block",
647
+ whiteSpace: "nowrap",
648
+ padding: "0 0.16em",
649
+ borderRadius: "0.18em",
650
+ fontWeight: 800,
651
+ color: isActive ? contrastText(primaryColor) : "rgba(255, 255, 255, 0.95)",
652
+ background: isActive ? primaryColor : "transparent",
653
+ textShadow: isActive ? "none" : "0 2px 6px rgba(0,0,0,0.75), 0 0 18px rgba(0,0,0,0.45)",
654
+ transform: isActive ? "scale(1.05)" : "none",
655
+ transition: "color 0.1s ease, background-color 0.1s ease, transform 0.1s ease"
623
656
  },
624
657
  children: word.text
625
658
  },
@@ -1457,6 +1490,7 @@ function DashboardView({
1457
1490
  },
1458
1491
  getRenderedTime: () => 0,
1459
1492
  getDuration: () => 0,
1493
+ getViewport: () => ({ width: activeViewport.width, height: activeViewport.height }),
1460
1494
  getBlocks: () => materialization.cells.map((cell) => ({
1461
1495
  id: String(cell.block.id),
1462
1496
  template: cell.block.template ?? "content",
@@ -1474,7 +1508,7 @@ function DashboardView({
1474
1508
  };
1475
1509
  onRenderAPIReady(api);
1476
1510
  return () => onRenderAPIReady(null);
1477
- }, [renderMode, onRenderAPIReady, materialization]);
1511
+ }, [activeViewport, renderMode, onRenderAPIReady, materialization]);
1478
1512
  const canvasDimensions = { width: activeViewport.width, height: activeViewport.height };
1479
1513
  const coverStyle = { position: "absolute", inset: 0 };
1480
1514
  const canvasStyle = {
@@ -2254,7 +2288,8 @@ import {
2254
2288
  isTemplateBlock as isTemplateBlock2,
2255
2289
  getCaptionAtTime as getCaptionAtTime2,
2256
2290
  resolveMediaSchedule,
2257
- getDocPlaybackDuration
2291
+ getDocPlaybackDuration,
2292
+ resolveMotionForDoc
2258
2293
  } from "@bendyline/squisq/schemas";
2259
2294
  import { applySurface, pipStyleVars } from "@bendyline/squisq/schemas";
2260
2295
 
@@ -2551,6 +2586,7 @@ function DocPlayerContent({
2551
2586
  coverSlidePlayback,
2552
2587
  coverVisible,
2553
2588
  theme,
2589
+ motion: motionProp,
2554
2590
  surface,
2555
2591
  captionStyle = "standard",
2556
2592
  captionPosition,
@@ -2753,6 +2789,12 @@ function DocPlayerContent({
2753
2789
  }),
2754
2790
  [resolvedPipStyle]
2755
2791
  );
2792
+ const hasAnchoredBlocks = useMemo5(
2793
+ () => doc.blocks.some(
2794
+ (block) => typeof block.sourceStartTime === "number"
2795
+ ),
2796
+ [doc.blocks]
2797
+ );
2756
2798
  const {
2757
2799
  currentBlock,
2758
2800
  currentBlockIndex,
@@ -2769,17 +2811,29 @@ function DocPlayerContent({
2769
2811
  } = useDocPlayback(doc, currentTime, {
2770
2812
  viewport: activeViewport,
2771
2813
  theme: effectiveTheme,
2814
+ motion: motionProp,
2772
2815
  onSeek: seekTo,
2816
+ // Offline capture may visit frames out of order; derive the crossfade
2817
+ // partner from block order rather than from what was rendered last.
2818
+ deterministicTransitions: renderMode,
2773
2819
  // A synthetic track is only the timer used by an unnarrated preview.
2774
2820
  // Narration pacing may compact short visual beats, but it must never
2775
2821
  // remove authored slides from the default loss-averse projection.
2776
- useAudioSegmentTiming: audioMode !== "synthetic"
2822
+ // Offline capture also runs on the synthetic clock, yet a narrated doc
2823
+ // (blocks anchored to its segments by `sourceStartTime`) still takes its
2824
+ // timeline from those segments there — the exporter muxes the audio
2825
+ // afterwards. Without them, block durations would simply stack and every
2826
+ // gap between anchored blocks would pull the rest earlier, so the render
2827
+ // would no longer match live playback.
2828
+ useAudioSegmentTiming: audioMode !== "synthetic" || renderMode && hasAnchoredBlocks
2777
2829
  });
2778
2830
  const coverBlock = useMemo5(() => {
2779
2831
  const startBlockConfig = doc.startBlock;
2780
2832
  if (!appearance.showCoverSlide) return null;
2781
2833
  if (!startBlockConfig) return null;
2782
- const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
2834
+ const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport, {
2835
+ motion: resolveMotionForDoc(doc, effectiveTheme, motionProp)
2836
+ });
2783
2837
  const layers = expandCoverBlock(startBlockConfig, context, appearance.coverSlideTemplate);
2784
2838
  return {
2785
2839
  id: "cover-block",
@@ -2791,7 +2845,8 @@ function DocPlayerContent({
2791
2845
  layers
2792
2846
  };
2793
2847
  }, [
2794
- doc.startBlock,
2848
+ doc,
2849
+ motionProp,
2795
2850
  activeViewport,
2796
2851
  effectiveTheme,
2797
2852
  appearance.showCoverSlide,
@@ -2839,7 +2894,7 @@ function DocPlayerContent({
2839
2894
  }, [isPlaying, coverBlock, renderMode, isSlideshowMode, appearance.coverSlideDuration]);
2840
2895
  useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
2841
2896
  useEffect7(() => () => clearTimeout(tapFeedbackTimer.current), []);
2842
- const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
2897
+ const showVideoCoverBlock = !isLinearMode && !!coverBlock && (coverForced || !isSlideshowMode && (coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay));
2843
2898
  const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
2844
2899
  const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
2845
2900
  const showCoverBlock = coverVisible === void 0 ? showVideoCoverBlock || showSlideshowCover : !!coverBlock && coverVisible;
@@ -2879,6 +2934,7 @@ function DocPlayerContent({
2879
2934
  seekTo: (time) => current().seekTo(time),
2880
2935
  getRenderedTime: () => current().getRenderedTime(),
2881
2936
  getDuration: () => current().getDuration(),
2937
+ getViewport: () => current().getViewport(),
2882
2938
  getBlocks: () => current().getBlocks(),
2883
2939
  getAudioSegments: () => current().getAudioSegments(),
2884
2940
  getCaptions: () => current().getCaptions(),
@@ -2908,27 +2964,42 @@ function DocPlayerContent({
2908
2964
  } else {
2909
2965
  await seekTo(time);
2910
2966
  }
2911
- let blockStartTime = 0;
2967
+ let activeIndex = -1;
2912
2968
  for (let i = expandedBlocks.length - 1; i >= 0; i--) {
2913
2969
  if (time >= expandedBlocks[i].startTime) {
2914
- blockStartTime = expandedBlocks[i].startTime;
2970
+ activeIndex = i;
2915
2971
  break;
2916
2972
  }
2917
2973
  }
2974
+ const blockStartTime = activeIndex >= 0 ? expandedBlocks[activeIndex].startTime : 0;
2975
+ const previousStartTime = activeIndex > 0 ? expandedBlocks[activeIndex - 1].startTime : blockStartTime;
2918
2976
  const elapsedMs = (time - blockStartTime) * 1e3;
2977
+ const previousElapsedMs = (time - previousStartTime) * 1e3;
2919
2978
  const selectVideoFrame = async (video, targetTime) => {
2920
2979
  const selector = renderMode ? renderVideoFrameSelectorRef.current : void 0;
2921
2980
  if (selector && await selector(video, targetTime)) return;
2922
2981
  await seekVideoToFrame(video, targetTime);
2923
2982
  };
2924
- (root.getAnimations?.() ?? []).forEach((anim) => {
2983
+ let seekedActiveAnimations = 0;
2984
+ (root.getAnimations?.({ subtree: true }) ?? []).forEach((anim) => {
2925
2985
  const target = anim.effect?.target;
2926
2986
  if (!target) return;
2927
- if (target.closest(".doc-player__block--active")) {
2928
- anim.currentTime = Math.max(0, elapsedMs);
2929
- } else if (target.closest(".doc-player__block--previous")) {
2930
- anim.currentTime = Math.max(0, elapsedMs);
2987
+ let position;
2988
+ if (target.closest(".doc-player__block--previous")) {
2989
+ position = target.classList.contains("block-svg") ? elapsedMs : previousElapsedMs;
2990
+ } else if (target.closest(".doc-player__block--active")) {
2991
+ position = elapsedMs;
2992
+ } else if (target.closest(".doc-player__block--cover")) {
2993
+ position = 0;
2994
+ } else {
2995
+ position = time * 1e3;
2931
2996
  }
2997
+ if (renderMode && typeof anim.pause === "function" && anim.playState !== "paused") {
2998
+ anim.pause();
2999
+ }
3000
+ anim.currentTime = Math.max(0, position);
3001
+ const end = anim.effect?.getComputedTiming?.().endTime;
3002
+ if (typeof end !== "number" || position < end) seekedActiveAnimations += 1;
2932
3003
  });
2933
3004
  const blockElapsed = time - blockStartTime;
2934
3005
  const videoSeekPromises = [];
@@ -2964,7 +3035,9 @@ function DocPlayerContent({
2964
3035
  const blocksUnchanged = previousBlocks !== null && previousBlocks.length === presentedBlocks.length && presentedBlocks.every(
2965
3036
  (block, index) => block.element === previousBlocks[index].element && block.className === previousBlocks[index].className
2966
3037
  );
2967
- if (!renderMode || !blocksUnchanged) await waitForVisualUpdate();
3038
+ if (!renderMode || !blocksUnchanged || seekedActiveAnimations > 0) {
3039
+ await waitForVisualUpdate();
3040
+ }
2968
3041
  };
2969
3042
  const getDuration = () => {
2970
3043
  const mediaDuration = getDocPlaybackDuration(doc);
@@ -3005,10 +3078,12 @@ function DocPlayerContent({
3005
3078
  return waitForVisualUpdate();
3006
3079
  };
3007
3080
  const hasCoverBlock = () => !!coverBlock;
3081
+ const getViewport = () => ({ width: activeViewport.width, height: activeViewport.height });
3008
3082
  const api = {
3009
3083
  seekTo: renderSeekTo,
3010
3084
  getRenderedTime: () => committedRenderTimeRef.current,
3011
3085
  getDuration,
3086
+ getViewport,
3012
3087
  getBlocks,
3013
3088
  getAudioSegments,
3014
3089
  getCaptions,
@@ -3031,7 +3106,8 @@ function DocPlayerContent({
3031
3106
  doc,
3032
3107
  externalAudioController,
3033
3108
  waitForRenderCommit,
3034
- isDashboardMode
3109
+ isDashboardMode,
3110
+ activeViewport
3035
3111
  ]);
3036
3112
  useEffect7(() => {
3037
3113
  if (isDashboardMode) return;
@@ -3445,7 +3521,7 @@ function DocPlayerContent({
3445
3521
  role: "region",
3446
3522
  "aria-label": "Document player",
3447
3523
  onKeyDown: renderMode ? void 0 : handleKeyDown,
3448
- className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
3524
+ className: `doc-player${renderMode ? " doc-player--render-mode" : ""}${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
3449
3525
  onClick: handleContainerClick,
3450
3526
  onPointerDown: swipe.onPointerDown,
3451
3527
  style: {
@@ -3503,7 +3579,7 @@ function DocPlayerContent({
3503
3579
  BlockRenderer,
3504
3580
  {
3505
3581
  block: previousBlock,
3506
- blockTime,
3582
+ blockTime: Math.max(0, currentTime - previousBlock.startTime),
3507
3583
  basePath,
3508
3584
  isExiting: true,
3509
3585
  transition: currentBlock?.transition,
@@ -3701,7 +3777,7 @@ function DocPlayerContent({
3701
3777
  }
3702
3778
  )
3703
3779
  ] }),
3704
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs6(
3780
+ !renderMode && !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs6(
3705
3781
  "div",
3706
3782
  {
3707
3783
  className: "doc-player__unavailable",
@@ -476,6 +476,7 @@ function useAudioSync(audioRef, track, basePath = "", enabled = true, mode = "me
476
476
 
477
477
  // src/hooks/useDocPlayback.ts
478
478
  import { useMemo as useMemo3, useCallback as useCallback2, useRef as useRef2 } from "react";
479
+ import { resolveMotionForDoc } from "@bendyline/squisq/schemas";
479
480
  import {
480
481
  DEFAULT_THEME,
481
482
  getBlockAtTime,
@@ -494,7 +495,9 @@ function useDocPlayback(script, currentTime, options = {}) {
494
495
  viewport = VIEWPORT_PRESETS.landscape,
495
496
  theme,
496
497
  onSeek,
497
- useAudioSegmentTiming = true
498
+ useAudioSegmentTiming = true,
499
+ deterministicTransitions = false,
500
+ motion: motionOverride
498
501
  } = options;
499
502
  const blocks = useMemo3(() => {
500
503
  if (!script?.blocks) {
@@ -518,6 +521,11 @@ function useDocPlayback(script, currentTime, options = {}) {
518
521
  viewport,
519
522
  persistentLayers,
520
523
  theme,
524
+ motion: resolveMotionForDoc(
525
+ { motion: script.motion, frontmatter: script.frontmatter },
526
+ resolvedTheme,
527
+ motionOverride
528
+ ),
521
529
  // Custom (user-defined) templates inlined into the doc's
522
530
  // frontmatter — see CustomTemplates.ts. Merged onto the
523
531
  // built-in registry so blocks annotated with `{[myhero]}`
@@ -535,6 +543,9 @@ function useDocPlayback(script, currentTime, options = {}) {
535
543
  script?.audio?.segments,
536
544
  script?.persistentLayers,
537
545
  script?.customTemplates,
546
+ script?.motion,
547
+ script?.frontmatter,
548
+ motionOverride,
538
549
  viewport,
539
550
  theme,
540
551
  useAudioSegmentTiming
@@ -577,7 +588,7 @@ function useDocPlayback(script, currentTime, options = {}) {
577
588
  lastRenderedBlockRef.current = currentBlock;
578
589
  const transitionDuration = currentBlock?.transition ? resolveTransitionDuration(currentBlock.transition) : 0;
579
590
  const isEntering = !!currentBlock && transitionDuration > 0 && blockTime < transitionDuration;
580
- const outgoingBlock = outgoingBlockRef.current;
591
+ const outgoingBlock = deterministicTransitions ? currentBlockIndex > 0 ? blocks[currentBlockIndex - 1] ?? null : null : outgoingBlockRef.current;
581
592
  const isExiting = isEntering && outgoingBlock != null && outgoingBlock.id !== currentBlock?.id;
582
593
  const previousBlock = isExiting ? outgoingBlock : null;
583
594
  const goToBlock = useCallback2(