@buildcores/render-client 1.1.0 → 1.2.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/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var React = require('react');
4
+ var react = require('react');
5
+ var framerMotion = require('framer-motion');
5
6
 
6
7
  // Helper to extract clientX from mouse or touch events
7
8
  const getClientX$1 = (e) => {
@@ -20,13 +21,13 @@ const calculateCircularFrame = (startFrame, deltaX, sensitivity, totalFrames) =>
20
21
  };
21
22
  const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
22
23
  const { mouseSensitivity = 0.1, touchSensitivity = 0.1, onFrameChange, } = options;
23
- const [isDragging, setIsDragging] = React.useState(false);
24
- const [dragStartX, setDragStartX] = React.useState(0);
25
- const [dragStartFrame, setDragStartFrame] = React.useState(0);
26
- const hasDragged = React.useRef(false);
27
- const currentFrame = React.useRef(0);
24
+ const [isDragging, setIsDragging] = react.useState(false);
25
+ const [dragStartX, setDragStartX] = react.useState(0);
26
+ const [dragStartFrame, setDragStartFrame] = react.useState(0);
27
+ const hasDragged = react.useRef(false);
28
+ const currentFrame = react.useRef(0);
28
29
  // Helper to start dragging (common logic for mouse and touch)
29
- const startDrag = React.useCallback((clientX, event) => {
30
+ const startDrag = react.useCallback((clientX, event) => {
30
31
  if (!canvasRef.current)
31
32
  return;
32
33
  setIsDragging(true);
@@ -36,7 +37,7 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
36
37
  event.preventDefault();
37
38
  }, [canvasRef]);
38
39
  // Helper to handle drag movement (common logic for mouse and touch)
39
- const handleDragMove = React.useCallback((clientX, sensitivity) => {
40
+ const handleDragMove = react.useCallback((clientX, sensitivity) => {
40
41
  if (!isDragging || !canvasRef.current)
41
42
  return;
42
43
  const deltaX = clientX - dragStartX;
@@ -50,29 +51,29 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
50
51
  return newFrame;
51
52
  }, [isDragging, dragStartX, dragStartFrame, totalFrames, onFrameChange]);
52
53
  // Helper to end dragging (common logic for mouse and touch)
53
- const endDrag = React.useCallback(() => {
54
+ const endDrag = react.useCallback(() => {
54
55
  setIsDragging(false);
55
56
  }, []);
56
- const handleMouseDown = React.useCallback((e) => {
57
+ const handleMouseDown = react.useCallback((e) => {
57
58
  startDrag(e.clientX, e.nativeEvent);
58
59
  }, [startDrag]);
59
- const handleTouchStart = React.useCallback((e) => {
60
+ const handleTouchStart = react.useCallback((e) => {
60
61
  startDrag(e.touches[0].clientX, e.nativeEvent);
61
62
  }, [startDrag]);
62
- const handleDocumentMouseMove = React.useCallback((e) => {
63
+ const handleDocumentMouseMove = react.useCallback((e) => {
63
64
  return handleDragMove(getClientX$1(e), mouseSensitivity);
64
65
  }, [handleDragMove, mouseSensitivity]);
65
- const handleDocumentTouchMove = React.useCallback((e) => {
66
+ const handleDocumentTouchMove = react.useCallback((e) => {
66
67
  return handleDragMove(getClientX$1(e), touchSensitivity);
67
68
  }, [handleDragMove, touchSensitivity]);
68
- const handleDocumentMouseUp = React.useCallback(() => {
69
+ const handleDocumentMouseUp = react.useCallback(() => {
69
70
  endDrag();
70
71
  }, [endDrag]);
71
- const handleDocumentTouchEnd = React.useCallback(() => {
72
+ const handleDocumentTouchEnd = react.useCallback(() => {
72
73
  endDrag();
73
74
  }, [endDrag]);
74
75
  // Add document-level event listeners when dragging starts
75
- React.useEffect(() => {
76
+ react.useEffect(() => {
76
77
  if (isDragging) {
77
78
  document.addEventListener("mousemove", handleDocumentMouseMove);
78
79
  document.addEventListener("mouseup", handleDocumentMouseUp);
@@ -104,191 +105,11 @@ const useSpriteScrubbing = (canvasRef, totalFrames, options = {}) => {
104
105
  };
105
106
  };
106
107
 
107
- /**
108
- * @public
109
- */
110
- const MotionConfigContext = React.createContext({
111
- transformPagePoint: (p) => p,
112
- isStatic: false,
113
- reducedMotion: "never",
114
- });
115
-
116
- /*#__NO_SIDE_EFFECTS__*/
117
- const noop = (any) => any;
118
-
119
- if (process.env.NODE_ENV !== "production") ;
120
-
121
- function createRenderStep(runNextFrame) {
122
- /**
123
- * We create and reuse two queues, one to queue jobs for the current frame
124
- * and one for the next. We reuse to avoid triggering GC after x frames.
125
- */
126
- let thisFrame = new Set();
127
- let nextFrame = new Set();
128
- /**
129
- * Track whether we're currently processing jobs in this step. This way
130
- * we can decide whether to schedule new jobs for this frame or next.
131
- */
132
- let isProcessing = false;
133
- let flushNextFrame = false;
134
- /**
135
- * A set of processes which were marked keepAlive when scheduled.
136
- */
137
- const toKeepAlive = new WeakSet();
138
- let latestFrameData = {
139
- delta: 0.0,
140
- timestamp: 0.0,
141
- isProcessing: false,
142
- };
143
- function triggerCallback(callback) {
144
- if (toKeepAlive.has(callback)) {
145
- step.schedule(callback);
146
- runNextFrame();
147
- }
148
- callback(latestFrameData);
149
- }
150
- const step = {
151
- /**
152
- * Schedule a process to run on the next frame.
153
- */
154
- schedule: (callback, keepAlive = false, immediate = false) => {
155
- const addToCurrentFrame = immediate && isProcessing;
156
- const queue = addToCurrentFrame ? thisFrame : nextFrame;
157
- if (keepAlive)
158
- toKeepAlive.add(callback);
159
- if (!queue.has(callback))
160
- queue.add(callback);
161
- return callback;
162
- },
163
- /**
164
- * Cancel the provided callback from running on the next frame.
165
- */
166
- cancel: (callback) => {
167
- nextFrame.delete(callback);
168
- toKeepAlive.delete(callback);
169
- },
170
- /**
171
- * Execute all schedule callbacks.
172
- */
173
- process: (frameData) => {
174
- latestFrameData = frameData;
175
- /**
176
- * If we're already processing we've probably been triggered by a flushSync
177
- * inside an existing process. Instead of executing, mark flushNextFrame
178
- * as true and ensure we flush the following frame at the end of this one.
179
- */
180
- if (isProcessing) {
181
- flushNextFrame = true;
182
- return;
183
- }
184
- isProcessing = true;
185
- [thisFrame, nextFrame] = [nextFrame, thisFrame];
186
- // Execute this frame
187
- thisFrame.forEach(triggerCallback);
188
- // Clear the frame so no callbacks remain. This is to avoid
189
- // memory leaks should this render step not run for a while.
190
- thisFrame.clear();
191
- isProcessing = false;
192
- if (flushNextFrame) {
193
- flushNextFrame = false;
194
- step.process(frameData);
195
- }
196
- },
197
- };
198
- return step;
199
- }
200
-
201
- const stepsOrder = [
202
- "read", // Read
203
- "resolveKeyframes", // Write/Read/Write/Read
204
- "update", // Compute
205
- "preRender", // Compute
206
- "render", // Write
207
- "postRender", // Compute
208
- ];
209
- const maxElapsed = 40;
210
- function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
211
- let runNextFrame = false;
212
- let useDefaultElapsed = true;
213
- const state = {
214
- delta: 0.0,
215
- timestamp: 0.0,
216
- isProcessing: false,
217
- };
218
- const flagRunNextFrame = () => (runNextFrame = true);
219
- const steps = stepsOrder.reduce((acc, key) => {
220
- acc[key] = createRenderStep(flagRunNextFrame);
221
- return acc;
222
- }, {});
223
- const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
224
- const processBatch = () => {
225
- const timestamp = performance.now();
226
- runNextFrame = false;
227
- state.delta = useDefaultElapsed
228
- ? 1000 / 60
229
- : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
230
- state.timestamp = timestamp;
231
- state.isProcessing = true;
232
- // Unrolled render loop for better per-frame performance
233
- read.process(state);
234
- resolveKeyframes.process(state);
235
- update.process(state);
236
- preRender.process(state);
237
- render.process(state);
238
- postRender.process(state);
239
- state.isProcessing = false;
240
- if (runNextFrame && allowKeepAlive) {
241
- useDefaultElapsed = false;
242
- scheduleNextBatch(processBatch);
243
- }
244
- };
245
- const wake = () => {
246
- runNextFrame = true;
247
- useDefaultElapsed = true;
248
- if (!state.isProcessing) {
249
- scheduleNextBatch(processBatch);
250
- }
251
- };
252
- const schedule = stepsOrder.reduce((acc, key) => {
253
- const step = steps[key];
254
- acc[key] = (process, keepAlive = false, immediate = false) => {
255
- if (!runNextFrame)
256
- wake();
257
- return step.schedule(process, keepAlive, immediate);
258
- };
259
- return acc;
260
- }, {});
261
- const cancel = (process) => {
262
- for (let i = 0; i < stepsOrder.length; i++) {
263
- steps[stepsOrder[i]].cancel(process);
264
- }
265
- };
266
- return { schedule, cancel, state, steps };
267
- }
268
-
269
- const { schedule: frame, cancel: cancelFrame} = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
270
-
271
- function useAnimationFrame(callback) {
272
- const initialTimestamp = React.useRef(0);
273
- const { isStatic } = React.useContext(MotionConfigContext);
274
- React.useEffect(() => {
275
- if (isStatic)
276
- return;
277
- const provideTimeSinceStart = ({ timestamp, delta }) => {
278
- if (!initialTimestamp.current)
279
- initialTimestamp.current = timestamp;
280
- callback(timestamp - initialTimestamp.current, delta);
281
- };
282
- frame.update(provideTimeSinceStart, true);
283
- return () => cancelFrame(provideTimeSinceStart);
284
- }, [callback]);
285
- }
286
-
287
108
  function useBouncePatternProgress(enabled = true) {
288
- const [value, setValue] = React.useState(0);
289
- const [isBouncing, setIsBouncing] = React.useState(false);
290
- const start = React.useRef(null);
291
- useAnimationFrame((t) => {
109
+ const [value, setValue] = react.useState(0);
110
+ const [isBouncing, setIsBouncing] = react.useState(false);
111
+ const start = react.useRef(null);
112
+ framerMotion.useAnimationFrame((t) => {
292
113
  if (!enabled) {
293
114
  // Reset animation when disabled
294
115
  if (start.current !== null) {
@@ -355,6 +176,9 @@ const renderBuildExperimental = async (request, config) => {
355
176
  const requestWithFormat = {
356
177
  ...request,
357
178
  format: request.format || "video", // Default to video format
179
+ // Include width and height if provided
180
+ ...(request.width !== undefined ? { width: request.width } : {}),
181
+ ...(request.height !== undefined ? { height: request.height } : {}),
358
182
  };
359
183
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
360
184
  method: "POST",
@@ -379,6 +203,9 @@ const createRenderBuildJob = async (request, config) => {
379
203
  parts: request.parts,
380
204
  // If provided, forward format; default handled server-side but we keep explicit default
381
205
  ...(request.format ? { format: request.format } : {}),
206
+ // Include width and height if provided
207
+ ...(request.width !== undefined ? { width: request.width } : {}),
208
+ ...(request.height !== undefined ? { height: request.height } : {}),
382
209
  };
383
210
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
384
211
  method: "POST",
@@ -439,6 +266,9 @@ const renderSpriteExperimental = async (request, config) => {
439
266
  const requestWithFormat = {
440
267
  ...request,
441
268
  format: "sprite",
269
+ // Include width and height if provided
270
+ ...(request.width !== undefined ? { width: request.width } : {}),
271
+ ...(request.height !== undefined ? { height: request.height } : {}),
442
272
  };
443
273
  const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
444
274
  method: "POST",
@@ -549,11 +379,11 @@ const arePartsEqual = (parts1, parts2) => {
549
379
  return true;
550
380
  };
551
381
  const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
552
- const [videoSrc, setVideoSrc] = React.useState(null);
553
- const [isRenderingBuild, setIsRenderingBuild] = React.useState(false);
554
- const [renderError, setRenderError] = React.useState(null);
555
- const previousPartsRef = React.useRef(null);
556
- const fetchRenderBuild = React.useCallback(async (currentParts) => {
382
+ const [videoSrc, setVideoSrc] = react.useState(null);
383
+ const [isRenderingBuild, setIsRenderingBuild] = react.useState(false);
384
+ const [renderError, setRenderError] = react.useState(null);
385
+ const previousPartsRef = react.useRef(null);
386
+ const fetchRenderBuild = react.useCallback(async (currentParts) => {
557
387
  try {
558
388
  setIsRenderingBuild(true);
559
389
  setRenderError(null);
@@ -588,7 +418,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
588
418
  }
589
419
  }, [apiConfig, onLoadStart, options?.mode]);
590
420
  // Effect to call API when parts content changes (using custom equality check)
591
- React.useEffect(() => {
421
+ react.useEffect(() => {
592
422
  const shouldFetch = previousPartsRef.current === null ||
593
423
  !arePartsEqual(previousPartsRef.current, parts);
594
424
  if (shouldFetch) {
@@ -597,7 +427,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
597
427
  }
598
428
  }, [parts, fetchRenderBuild]);
599
429
  // Cleanup effect for component unmount
600
- React.useEffect(() => {
430
+ react.useEffect(() => {
601
431
  return () => {
602
432
  if (videoSrc && videoSrc.startsWith("blob:")) {
603
433
  URL.revokeObjectURL(videoSrc);
@@ -612,12 +442,12 @@ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
612
442
  };
613
443
 
614
444
  const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
615
- const [spriteSrc, setSpriteSrc] = React.useState(null);
616
- const [isRenderingSprite, setIsRenderingSprite] = React.useState(false);
617
- const [renderError, setRenderError] = React.useState(null);
618
- const [spriteMetadata, setSpriteMetadata] = React.useState(null);
619
- const previousPartsRef = React.useRef(null);
620
- const fetchRenderSprite = React.useCallback(async (currentParts) => {
445
+ const [spriteSrc, setSpriteSrc] = react.useState(null);
446
+ const [isRenderingSprite, setIsRenderingSprite] = react.useState(false);
447
+ const [renderError, setRenderError] = react.useState(null);
448
+ const [spriteMetadata, setSpriteMetadata] = react.useState(null);
449
+ const previousPartsRef = react.useRef(null);
450
+ const fetchRenderSprite = react.useCallback(async (currentParts) => {
621
451
  try {
622
452
  setIsRenderingSprite(true);
623
453
  setRenderError(null);
@@ -661,7 +491,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
661
491
  }
662
492
  }, [apiConfig, onLoadStart, options?.mode]);
663
493
  // Effect to call API when parts content changes (using custom equality check)
664
- React.useEffect(() => {
494
+ react.useEffect(() => {
665
495
  const shouldFetch = previousPartsRef.current === null ||
666
496
  !arePartsEqual(previousPartsRef.current, parts);
667
497
  if (shouldFetch) {
@@ -670,7 +500,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
670
500
  }
671
501
  }, [parts, fetchRenderSprite]);
672
502
  // Cleanup effect for component unmount
673
- React.useEffect(() => {
503
+ react.useEffect(() => {
674
504
  return () => {
675
505
  if (spriteSrc && spriteSrc.startsWith("blob:")) {
676
506
  URL.revokeObjectURL(spriteSrc);
@@ -741,10 +571,10 @@ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
741
571
  };
742
572
 
743
573
  const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, }) => {
744
- const canvasRef = React.useRef(null);
745
- const [img, setImg] = React.useState(null);
746
- const [isLoading, setIsLoading] = React.useState(true);
747
- const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
574
+ const canvasRef = react.useRef(null);
575
+ const [img, setImg] = react.useState(null);
576
+ const [isLoading, setIsLoading] = react.useState(true);
577
+ const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
748
578
  const displayW = width ?? size ?? 300;
749
579
  const displayH = height ?? size ?? 300;
750
580
  // Use custom hook for sprite rendering
@@ -753,12 +583,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
753
583
  const total = spriteMetadata ? spriteMetadata.totalFrames : 72;
754
584
  const cols = spriteMetadata ? spriteMetadata.cols : 12;
755
585
  const rows = spriteMetadata ? spriteMetadata.rows : 6;
756
- const frameRef = React.useRef(0);
586
+ const frameRef = react.useRef(0);
757
587
  // Image/frame sizes
758
588
  const frameW = img ? img.width / cols : 0;
759
589
  const frameH = img ? img.height / rows : 0;
760
590
  // ---- Load sprite image ----
761
- React.useEffect(() => {
591
+ react.useEffect(() => {
762
592
  if (!spriteSrc) {
763
593
  setImg(null);
764
594
  setIsLoading(true);
@@ -783,7 +613,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
783
613
  };
784
614
  }, [spriteSrc]);
785
615
  // ---- Drawing function ----
786
- const draw = React.useCallback((frameIndex) => {
616
+ const draw = react.useCallback((frameIndex) => {
787
617
  const cnv = canvasRef.current;
788
618
  if (!cnv || !img || !frameW || !frameH)
789
619
  return;
@@ -822,12 +652,12 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
822
652
  draw(newFrame);
823
653
  },
824
654
  });
825
- React.useCallback(() => {
655
+ react.useCallback(() => {
826
656
  setIsLoading(true);
827
657
  setBouncingAllowed(false);
828
658
  }, []);
829
659
  // Auto-rotate when bouncing is allowed and not dragged
830
- React.useEffect(() => {
660
+ react.useEffect(() => {
831
661
  if (hasDragged.current || !img)
832
662
  return;
833
663
  // Calculate frame based on progress value (similar to video time calculation)
@@ -836,7 +666,7 @@ const BuildRender = ({ parts, width, height, size, apiConfig, useSpriteRenderOpt
836
666
  draw(frame);
837
667
  }, [progressValue, hasDragged, img, total, draw]);
838
668
  // Initial draw once image is ready
839
- React.useEffect(() => {
669
+ react.useEffect(() => {
840
670
  if (img && !isLoading) {
841
671
  draw(frameRef.current);
842
672
  }
@@ -879,12 +709,12 @@ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
879
709
  };
880
710
  const useVideoScrubbing = (videoRef, options = {}) => {
881
711
  const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
882
- const [isDragging, setIsDragging] = React.useState(false);
883
- const [dragStartX, setDragStartX] = React.useState(0);
884
- const [dragStartTime, setDragStartTime] = React.useState(0);
885
- const hasDragged = React.useRef(false);
712
+ const [isDragging, setIsDragging] = react.useState(false);
713
+ const [dragStartX, setDragStartX] = react.useState(0);
714
+ const [dragStartTime, setDragStartTime] = react.useState(0);
715
+ const hasDragged = react.useRef(false);
886
716
  // Helper to start dragging (common logic for mouse and touch)
887
- const startDrag = React.useCallback((clientX, event) => {
717
+ const startDrag = react.useCallback((clientX, event) => {
888
718
  if (!videoRef.current)
889
719
  return;
890
720
  setIsDragging(true);
@@ -894,7 +724,7 @@ const useVideoScrubbing = (videoRef, options = {}) => {
894
724
  event.preventDefault();
895
725
  }, [videoRef]);
896
726
  // Helper to handle drag movement (common logic for mouse and touch)
897
- const handleDragMove = React.useCallback((clientX, sensitivity) => {
727
+ const handleDragMove = react.useCallback((clientX, sensitivity) => {
898
728
  if (!isDragging || !videoRef.current)
899
729
  return;
900
730
  const deltaX = clientX - dragStartX;
@@ -905,29 +735,29 @@ const useVideoScrubbing = (videoRef, options = {}) => {
905
735
  }
906
736
  }, [isDragging, dragStartX, dragStartTime, videoRef]);
907
737
  // Helper to end dragging (common logic for mouse and touch)
908
- const endDrag = React.useCallback(() => {
738
+ const endDrag = react.useCallback(() => {
909
739
  setIsDragging(false);
910
740
  }, []);
911
- const handleMouseDown = React.useCallback((e) => {
741
+ const handleMouseDown = react.useCallback((e) => {
912
742
  startDrag(e.clientX, e.nativeEvent);
913
743
  }, [startDrag]);
914
- const handleTouchStart = React.useCallback((e) => {
744
+ const handleTouchStart = react.useCallback((e) => {
915
745
  startDrag(e.touches[0].clientX, e.nativeEvent);
916
746
  }, [startDrag]);
917
- const handleDocumentMouseMove = React.useCallback((e) => {
747
+ const handleDocumentMouseMove = react.useCallback((e) => {
918
748
  handleDragMove(getClientX(e), mouseSensitivity);
919
749
  }, [handleDragMove, mouseSensitivity]);
920
- const handleDocumentTouchMove = React.useCallback((e) => {
750
+ const handleDocumentTouchMove = react.useCallback((e) => {
921
751
  handleDragMove(getClientX(e), touchSensitivity);
922
752
  }, [handleDragMove, touchSensitivity]);
923
- const handleDocumentMouseUp = React.useCallback(() => {
753
+ const handleDocumentMouseUp = react.useCallback(() => {
924
754
  endDrag();
925
755
  }, [endDrag]);
926
- const handleDocumentTouchEnd = React.useCallback(() => {
756
+ const handleDocumentTouchEnd = react.useCallback(() => {
927
757
  endDrag();
928
758
  }, [endDrag]);
929
759
  // Add document-level event listeners when dragging starts
930
- React.useEffect(() => {
760
+ react.useEffect(() => {
931
761
  if (isDragging) {
932
762
  document.addEventListener("mousemove", handleDocumentMouseMove);
933
763
  document.addEventListener("mouseup", handleDocumentMouseUp);
@@ -956,9 +786,9 @@ const useVideoScrubbing = (videoRef, options = {}) => {
956
786
  };
957
787
 
958
788
  const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRenderOptions, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
959
- const videoRef = React.useRef(null);
960
- const [isLoading, setIsLoading] = React.useState(true);
961
- const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
789
+ const videoRef = react.useRef(null);
790
+ const [isLoading, setIsLoading] = react.useState(true);
791
+ const [bouncingAllowed, setBouncingAllowed] = react.useState(false);
962
792
  const displayW = width ?? size ?? 300;
963
793
  const displayH = height ?? size ?? 300;
964
794
  // Use custom hook for build rendering
@@ -968,18 +798,18 @@ const BuildRenderVideo = ({ parts, width, height, size, apiConfig, useBuildRende
968
798
  mouseSensitivity,
969
799
  touchSensitivity,
970
800
  });
971
- const handleLoadStartInternal = React.useCallback(() => {
801
+ const handleLoadStartInternal = react.useCallback(() => {
972
802
  setIsLoading(true);
973
803
  setBouncingAllowed(false);
974
804
  }, []);
975
- const handleCanPlayInternal = React.useCallback(() => {
805
+ const handleCanPlayInternal = react.useCallback(() => {
976
806
  setIsLoading(false);
977
807
  // Start bouncing animation after delay
978
808
  setTimeout(() => {
979
809
  setBouncingAllowed(true);
980
810
  }, 2000);
981
811
  }, []);
982
- React.useEffect(() => {
812
+ react.useEffect(() => {
983
813
  if (hasDragged.current || !videoRef.current)
984
814
  return;
985
815
  const duration = videoRef.current.duration;