@buildcores/render-client 1.0.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 ADDED
@@ -0,0 +1,597 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+
6
+ // Helper to extract clientX from mouse or touch events
7
+ const getClientX = (e) => {
8
+ return "touches" in e ? e.touches[0].clientX : e.clientX;
9
+ };
10
+ // Helper to calculate new video time with circular wrapping
11
+ const calculateCircularTime = (startTime, deltaX, sensitivity, duration) => {
12
+ const timeDelta = deltaX * sensitivity;
13
+ let newTime = startTime + timeDelta;
14
+ // Make it circular - wrap around when going past boundaries
15
+ newTime = newTime % duration;
16
+ if (newTime < 0) {
17
+ newTime += duration;
18
+ }
19
+ return newTime;
20
+ };
21
+ const useVideoScrubbing = (videoRef, options = {}) => {
22
+ const { mouseSensitivity = 0.01, touchSensitivity = 0.01 } = options;
23
+ const [isDragging, setIsDragging] = React.useState(false);
24
+ const [dragStartX, setDragStartX] = React.useState(0);
25
+ const [dragStartTime, setDragStartTime] = React.useState(0);
26
+ const hasDragged = React.useRef(false);
27
+ // Helper to start dragging (common logic for mouse and touch)
28
+ const startDrag = React.useCallback((clientX, event) => {
29
+ if (!videoRef.current)
30
+ return;
31
+ setIsDragging(true);
32
+ setDragStartX(clientX);
33
+ setDragStartTime(videoRef.current.currentTime);
34
+ hasDragged.current = true;
35
+ event.preventDefault();
36
+ }, [videoRef]);
37
+ // Helper to handle drag movement (common logic for mouse and touch)
38
+ const handleDragMove = React.useCallback((clientX, sensitivity) => {
39
+ if (!isDragging || !videoRef.current)
40
+ return;
41
+ const deltaX = clientX - dragStartX;
42
+ const duration = videoRef.current.duration || 0;
43
+ if (duration > 0) {
44
+ const newTime = calculateCircularTime(dragStartTime, deltaX, sensitivity, duration);
45
+ videoRef.current.currentTime = newTime;
46
+ }
47
+ }, [isDragging, dragStartX, dragStartTime, videoRef]);
48
+ // Helper to end dragging (common logic for mouse and touch)
49
+ const endDrag = React.useCallback(() => {
50
+ setIsDragging(false);
51
+ }, []);
52
+ const handleMouseDown = React.useCallback((e) => {
53
+ startDrag(e.clientX, e.nativeEvent);
54
+ }, [startDrag]);
55
+ const handleTouchStart = React.useCallback((e) => {
56
+ startDrag(e.touches[0].clientX, e.nativeEvent);
57
+ }, [startDrag]);
58
+ const handleDocumentMouseMove = React.useCallback((e) => {
59
+ handleDragMove(getClientX(e), mouseSensitivity);
60
+ }, [handleDragMove, mouseSensitivity]);
61
+ const handleDocumentTouchMove = React.useCallback((e) => {
62
+ handleDragMove(getClientX(e), touchSensitivity);
63
+ }, [handleDragMove, touchSensitivity]);
64
+ const handleDocumentMouseUp = React.useCallback(() => {
65
+ endDrag();
66
+ }, [endDrag]);
67
+ const handleDocumentTouchEnd = React.useCallback(() => {
68
+ endDrag();
69
+ }, [endDrag]);
70
+ // Add document-level event listeners when dragging starts
71
+ React.useEffect(() => {
72
+ if (isDragging) {
73
+ document.addEventListener("mousemove", handleDocumentMouseMove);
74
+ document.addEventListener("mouseup", handleDocumentMouseUp);
75
+ document.addEventListener("touchmove", handleDocumentTouchMove);
76
+ document.addEventListener("touchend", handleDocumentTouchEnd);
77
+ return () => {
78
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
79
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
80
+ document.removeEventListener("touchmove", handleDocumentTouchMove);
81
+ document.removeEventListener("touchend", handleDocumentTouchEnd);
82
+ };
83
+ }
84
+ }, [
85
+ isDragging,
86
+ handleDocumentMouseMove,
87
+ handleDocumentMouseUp,
88
+ handleDocumentTouchMove,
89
+ handleDocumentTouchEnd,
90
+ ]);
91
+ return {
92
+ isDragging,
93
+ handleMouseDown,
94
+ handleTouchStart,
95
+ hasDragged,
96
+ };
97
+ };
98
+
99
+ /**
100
+ * @public
101
+ */
102
+ const MotionConfigContext = React.createContext({
103
+ transformPagePoint: (p) => p,
104
+ isStatic: false,
105
+ reducedMotion: "never",
106
+ });
107
+
108
+ /*#__NO_SIDE_EFFECTS__*/
109
+ const noop = (any) => any;
110
+
111
+ if (process.env.NODE_ENV !== "production") ;
112
+
113
+ function createRenderStep(runNextFrame) {
114
+ /**
115
+ * We create and reuse two queues, one to queue jobs for the current frame
116
+ * and one for the next. We reuse to avoid triggering GC after x frames.
117
+ */
118
+ let thisFrame = new Set();
119
+ let nextFrame = new Set();
120
+ /**
121
+ * Track whether we're currently processing jobs in this step. This way
122
+ * we can decide whether to schedule new jobs for this frame or next.
123
+ */
124
+ let isProcessing = false;
125
+ let flushNextFrame = false;
126
+ /**
127
+ * A set of processes which were marked keepAlive when scheduled.
128
+ */
129
+ const toKeepAlive = new WeakSet();
130
+ let latestFrameData = {
131
+ delta: 0.0,
132
+ timestamp: 0.0,
133
+ isProcessing: false,
134
+ };
135
+ function triggerCallback(callback) {
136
+ if (toKeepAlive.has(callback)) {
137
+ step.schedule(callback);
138
+ runNextFrame();
139
+ }
140
+ callback(latestFrameData);
141
+ }
142
+ const step = {
143
+ /**
144
+ * Schedule a process to run on the next frame.
145
+ */
146
+ schedule: (callback, keepAlive = false, immediate = false) => {
147
+ const addToCurrentFrame = immediate && isProcessing;
148
+ const queue = addToCurrentFrame ? thisFrame : nextFrame;
149
+ if (keepAlive)
150
+ toKeepAlive.add(callback);
151
+ if (!queue.has(callback))
152
+ queue.add(callback);
153
+ return callback;
154
+ },
155
+ /**
156
+ * Cancel the provided callback from running on the next frame.
157
+ */
158
+ cancel: (callback) => {
159
+ nextFrame.delete(callback);
160
+ toKeepAlive.delete(callback);
161
+ },
162
+ /**
163
+ * Execute all schedule callbacks.
164
+ */
165
+ process: (frameData) => {
166
+ latestFrameData = frameData;
167
+ /**
168
+ * If we're already processing we've probably been triggered by a flushSync
169
+ * inside an existing process. Instead of executing, mark flushNextFrame
170
+ * as true and ensure we flush the following frame at the end of this one.
171
+ */
172
+ if (isProcessing) {
173
+ flushNextFrame = true;
174
+ return;
175
+ }
176
+ isProcessing = true;
177
+ [thisFrame, nextFrame] = [nextFrame, thisFrame];
178
+ // Execute this frame
179
+ thisFrame.forEach(triggerCallback);
180
+ // Clear the frame so no callbacks remain. This is to avoid
181
+ // memory leaks should this render step not run for a while.
182
+ thisFrame.clear();
183
+ isProcessing = false;
184
+ if (flushNextFrame) {
185
+ flushNextFrame = false;
186
+ step.process(frameData);
187
+ }
188
+ },
189
+ };
190
+ return step;
191
+ }
192
+
193
+ const stepsOrder = [
194
+ "read", // Read
195
+ "resolveKeyframes", // Write/Read/Write/Read
196
+ "update", // Compute
197
+ "preRender", // Compute
198
+ "render", // Write
199
+ "postRender", // Compute
200
+ ];
201
+ const maxElapsed = 40;
202
+ function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
203
+ let runNextFrame = false;
204
+ let useDefaultElapsed = true;
205
+ const state = {
206
+ delta: 0.0,
207
+ timestamp: 0.0,
208
+ isProcessing: false,
209
+ };
210
+ const flagRunNextFrame = () => (runNextFrame = true);
211
+ const steps = stepsOrder.reduce((acc, key) => {
212
+ acc[key] = createRenderStep(flagRunNextFrame);
213
+ return acc;
214
+ }, {});
215
+ const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
216
+ const processBatch = () => {
217
+ const timestamp = performance.now();
218
+ runNextFrame = false;
219
+ state.delta = useDefaultElapsed
220
+ ? 1000 / 60
221
+ : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1);
222
+ state.timestamp = timestamp;
223
+ state.isProcessing = true;
224
+ // Unrolled render loop for better per-frame performance
225
+ read.process(state);
226
+ resolveKeyframes.process(state);
227
+ update.process(state);
228
+ preRender.process(state);
229
+ render.process(state);
230
+ postRender.process(state);
231
+ state.isProcessing = false;
232
+ if (runNextFrame && allowKeepAlive) {
233
+ useDefaultElapsed = false;
234
+ scheduleNextBatch(processBatch);
235
+ }
236
+ };
237
+ const wake = () => {
238
+ runNextFrame = true;
239
+ useDefaultElapsed = true;
240
+ if (!state.isProcessing) {
241
+ scheduleNextBatch(processBatch);
242
+ }
243
+ };
244
+ const schedule = stepsOrder.reduce((acc, key) => {
245
+ const step = steps[key];
246
+ acc[key] = (process, keepAlive = false, immediate = false) => {
247
+ if (!runNextFrame)
248
+ wake();
249
+ return step.schedule(process, keepAlive, immediate);
250
+ };
251
+ return acc;
252
+ }, {});
253
+ const cancel = (process) => {
254
+ for (let i = 0; i < stepsOrder.length; i++) {
255
+ steps[stepsOrder[i]].cancel(process);
256
+ }
257
+ };
258
+ return { schedule, cancel, state, steps };
259
+ }
260
+
261
+ const { schedule: frame, cancel: cancelFrame} = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
262
+
263
+ function useAnimationFrame(callback) {
264
+ const initialTimestamp = React.useRef(0);
265
+ const { isStatic } = React.useContext(MotionConfigContext);
266
+ React.useEffect(() => {
267
+ if (isStatic)
268
+ return;
269
+ const provideTimeSinceStart = ({ timestamp, delta }) => {
270
+ if (!initialTimestamp.current)
271
+ initialTimestamp.current = timestamp;
272
+ callback(timestamp - initialTimestamp.current, delta);
273
+ };
274
+ frame.update(provideTimeSinceStart, true);
275
+ return () => cancelFrame(provideTimeSinceStart);
276
+ }, [callback]);
277
+ }
278
+
279
+ function useBouncePatternProgress(enabled = true) {
280
+ const [value, setValue] = React.useState(0);
281
+ const [isBouncing, setIsBouncing] = React.useState(false);
282
+ const start = React.useRef(null);
283
+ useAnimationFrame((t) => {
284
+ if (!enabled) {
285
+ // Reset animation when disabled
286
+ if (start.current !== null) {
287
+ start.current = null;
288
+ setValue(0);
289
+ setIsBouncing(false);
290
+ }
291
+ return;
292
+ }
293
+ if (start.current === null)
294
+ start.current = t;
295
+ const elapsed = (t - start.current) % 3000; // 3s full cycle
296
+ let progress = 0;
297
+ const bouncing = elapsed < 1000; // Bouncing during first 1 second
298
+ if (elapsed < 500) {
299
+ // 0 → 1
300
+ progress = elapsed / 500;
301
+ }
302
+ else if (elapsed < 1000) {
303
+ // 1 → 0
304
+ progress = 1 - (elapsed - 500) / 500;
305
+ }
306
+ else {
307
+ // Pause at 0 for 2 seconds
308
+ progress = 0;
309
+ }
310
+ setValue(progress);
311
+ setIsBouncing(bouncing);
312
+ });
313
+ return { value, isBouncing };
314
+ }
315
+
316
+ // API Types
317
+ /**
318
+ * Enum defining all available PC part categories that can be rendered.
319
+ *
320
+ * Each category represents a different type of computer component that can be
321
+ * included in the 3D build visualization.
322
+ *
323
+ * @example
324
+ * ```tsx
325
+ * // All available categories
326
+ * const categories = [
327
+ * PartCategory.CPU, // "CPU"
328
+ * PartCategory.GPU, // "GPU"
329
+ * PartCategory.RAM, // "RAM"
330
+ * PartCategory.Motherboard,// "Motherboard"
331
+ * PartCategory.PSU, // "PSU"
332
+ * PartCategory.Storage, // "Storage"
333
+ * PartCategory.PCCase, // "PCCase"
334
+ * PartCategory.CPUCooler, // "CPUCooler"
335
+ * ];
336
+ * ```
337
+ */
338
+ exports.PartCategory = void 0;
339
+ (function (PartCategory) {
340
+ /** Central Processing Unit - The main processor */
341
+ PartCategory["CPU"] = "CPU";
342
+ /** Graphics Processing Unit - Video card for rendering */
343
+ PartCategory["GPU"] = "GPU";
344
+ /** Random Access Memory - System memory modules */
345
+ PartCategory["RAM"] = "RAM";
346
+ /** Main circuit board that connects all components */
347
+ PartCategory["Motherboard"] = "Motherboard";
348
+ /** Power Supply Unit - Provides power to all components */
349
+ PartCategory["PSU"] = "PSU";
350
+ /** Storage devices like SSDs, HDDs, NVMe drives */
351
+ PartCategory["Storage"] = "Storage";
352
+ /** PC Case - The enclosure that houses all components */
353
+ PartCategory["PCCase"] = "PCCase";
354
+ /** CPU Cooler - Air or liquid cooling for the processor */
355
+ PartCategory["CPUCooler"] = "CPUCooler";
356
+ })(exports.PartCategory || (exports.PartCategory = {}));
357
+
358
+ // API Configuration
359
+ const API_BASE_URL = "https://squid-app-7aeyk.ondigitalocean.app";
360
+ // API Endpoints
361
+ const API_ENDPOINTS = {
362
+ RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental",
363
+ AVAILABLE_PARTS: "/available-parts",
364
+ };
365
+ // API URL helpers
366
+ const buildApiUrl = (endpoint) => {
367
+ return `${API_BASE_URL}${endpoint}`;
368
+ };
369
+ // API Implementation
370
+ const renderBuildExperimental = async (request) => {
371
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL), {
372
+ method: "POST",
373
+ headers: {
374
+ "Content-Type": "application/json",
375
+ },
376
+ body: JSON.stringify(request),
377
+ });
378
+ if (!response.ok) {
379
+ throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
380
+ }
381
+ const video = await response.blob();
382
+ return {
383
+ video,
384
+ metadata: {
385
+ size: video.size,
386
+ format: "video/mp4",
387
+ },
388
+ };
389
+ };
390
+ const getAvailableParts = async () => {
391
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.AVAILABLE_PARTS), {
392
+ method: "GET",
393
+ headers: {
394
+ "Content-Type": "application/json",
395
+ },
396
+ });
397
+ if (!response.ok) {
398
+ throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
399
+ }
400
+ return response.json();
401
+ };
402
+
403
+ /**
404
+ * Compares two RenderBuildRequest objects for equality by checking if the same IDs
405
+ * are present in each category array, regardless of order.
406
+ */
407
+ const arePartsEqual = (parts1, parts2) => {
408
+ const categories = Object.values(exports.PartCategory);
409
+ for (const category of categories) {
410
+ const arr1 = parts1.parts[category] || [];
411
+ const arr2 = parts2.parts[category] || [];
412
+ // Check if arrays have the same length
413
+ if (arr1.length !== arr2.length) {
414
+ return false;
415
+ }
416
+ // Check if arrays contain the same elements (order doesn't matter)
417
+ const set1 = new Set(arr1);
418
+ const set2 = new Set(arr2);
419
+ if (set1.size !== set2.size) {
420
+ return false;
421
+ }
422
+ for (const id of set1) {
423
+ if (!set2.has(id)) {
424
+ return false;
425
+ }
426
+ }
427
+ }
428
+ return true;
429
+ };
430
+ const useBuildRender = (parts, onLoadStart) => {
431
+ const [videoSrc, setVideoSrc] = React.useState(null);
432
+ const [isRenderingBuild, setIsRenderingBuild] = React.useState(false);
433
+ const [renderError, setRenderError] = React.useState(null);
434
+ const previousPartsRef = React.useRef(null);
435
+ const fetchRenderBuild = React.useCallback(async (currentParts) => {
436
+ try {
437
+ setIsRenderingBuild(true);
438
+ setRenderError(null);
439
+ onLoadStart?.();
440
+ const response = await renderBuildExperimental(currentParts);
441
+ const objectUrl = URL.createObjectURL(response.video);
442
+ // Clean up previous video URL before setting new one
443
+ setVideoSrc((prevSrc) => {
444
+ if (prevSrc) {
445
+ URL.revokeObjectURL(prevSrc);
446
+ }
447
+ return objectUrl;
448
+ });
449
+ }
450
+ catch (error) {
451
+ setRenderError(error instanceof Error ? error.message : "Failed to render build");
452
+ }
453
+ finally {
454
+ setIsRenderingBuild(false);
455
+ }
456
+ }, [onLoadStart]);
457
+ // Effect to call API when parts content changes (using custom equality check)
458
+ React.useEffect(() => {
459
+ const shouldFetch = previousPartsRef.current === null ||
460
+ !arePartsEqual(previousPartsRef.current, parts);
461
+ if (shouldFetch) {
462
+ previousPartsRef.current = parts;
463
+ fetchRenderBuild(parts);
464
+ }
465
+ }, [parts, fetchRenderBuild]);
466
+ // Cleanup effect for component unmount
467
+ React.useEffect(() => {
468
+ return () => {
469
+ if (videoSrc) {
470
+ URL.revokeObjectURL(videoSrc);
471
+ }
472
+ };
473
+ }, [videoSrc]);
474
+ return {
475
+ videoSrc,
476
+ isRenderingBuild,
477
+ renderError,
478
+ };
479
+ };
480
+
481
+ const LoadingErrorOverlay = ({ isVisible, renderError, size, }) => {
482
+ if (!isVisible)
483
+ return null;
484
+ return (jsxRuntime.jsx("div", { style: {
485
+ position: "absolute",
486
+ top: 0,
487
+ left: 0,
488
+ right: 0,
489
+ bottom: 0,
490
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
491
+ display: "flex",
492
+ flexDirection: "column",
493
+ alignItems: "center",
494
+ justifyContent: "center",
495
+ color: "white",
496
+ zIndex: 10,
497
+ }, children: renderError ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: { marginBottom: "20px", fontSize: "18px" }, children: "Render Failed" }), jsxRuntime.jsx("div", { style: {
498
+ fontSize: "14px",
499
+ textAlign: "center",
500
+ maxWidth: size * 0.8,
501
+ }, children: renderError })] })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx("div", { style: { marginBottom: "20px", fontSize: "18px" }, children: "Loading Build..." }) })) }));
502
+ };
503
+
504
+ const DragIcon = ({ width = 24, height = 24, className, style, ...props }) => {
505
+ return (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", id: "Layer_1", width: width, height: height, "data-name": "Layer 1", viewBox: "0 0 24 24", className: className, style: style, ...props, children: [jsxRuntime.jsx("defs", { children: jsxRuntime.jsx("style", { children: ".cls-1{fill:none;stroke:currentColor;stroke-miterlimit:10;stroke-width:1.91px}" }) }), jsxRuntime.jsx("path", { d: "m11.05 22.5-5.14-5.14a2 2 0 0 1-.59-1.43 2 2 0 0 1 2-2 2 2 0 0 1 1.43.59l1.32 1.32V6.38a2 2 0 0 1 1.74-2 1.89 1.89 0 0 1 1.52.56 1.87 1.87 0 0 1 .56 1.34V12l5 .72a1.91 1.91 0 0 1 1.64 1.89 17.18 17.18 0 0 1-1.82 7.71l-.09.18M19.64 7.23l2.86-2.87-2.86-2.86M15.82 4.36h6.68M4.36 7.23 1.5 4.36 4.36 1.5M8.18 4.36H1.5", className: "cls-1" })] }));
506
+ };
507
+
508
+ const InstructionTooltip = ({ isVisible, progressValue, instructionIcon, }) => {
509
+ if (!isVisible) {
510
+ return null;
511
+ }
512
+ return (jsxRuntime.jsx("div", { style: {
513
+ position: "absolute",
514
+ top: "50%",
515
+ left: "50%",
516
+ transform: `translate(-50%, -50%) translateX(${progressValue * 100}px)`,
517
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
518
+ color: "white",
519
+ padding: "12px",
520
+ borderRadius: "8px",
521
+ pointerEvents: "none",
522
+ zIndex: 5,
523
+ display: "flex",
524
+ alignItems: "center",
525
+ justifyContent: "center",
526
+ }, children: instructionIcon ? (jsxRuntime.jsx("img", { src: instructionIcon, alt: "drag to view 360", style: {
527
+ width: "24px",
528
+ height: "24px",
529
+ filter: "invert(1)", // Makes the icon white
530
+ } })) : (jsxRuntime.jsx(DragIcon, { width: 24, height: 24, style: {
531
+ color: "white",
532
+ } })) }));
533
+ };
534
+
535
+ const BuildRender = ({ parts, size, mouseSensitivity = 0.01, touchSensitivity = 0.01, }) => {
536
+ const videoRef = React.useRef(null);
537
+ const [isLoading, setIsLoading] = React.useState(true);
538
+ const [bouncingAllowed, setBouncingAllowed] = React.useState(false);
539
+ // Use custom hook for build rendering
540
+ const { videoSrc, isRenderingBuild, renderError } = useBuildRender(parts);
541
+ const { value: progressValue, isBouncing } = useBouncePatternProgress(bouncingAllowed);
542
+ const { isDragging, handleMouseDown, handleTouchStart, hasDragged } = useVideoScrubbing(videoRef, {
543
+ mouseSensitivity,
544
+ touchSensitivity,
545
+ });
546
+ const handleLoadStartInternal = React.useCallback(() => {
547
+ setIsLoading(true);
548
+ setBouncingAllowed(false);
549
+ }, []);
550
+ const handleCanPlayInternal = React.useCallback(() => {
551
+ setIsLoading(false);
552
+ // Start bouncing animation after delay
553
+ setTimeout(() => {
554
+ setBouncingAllowed(true);
555
+ }, 2000);
556
+ }, []);
557
+ React.useEffect(() => {
558
+ if (hasDragged.current || !videoRef.current)
559
+ return;
560
+ const duration = videoRef.current.duration;
561
+ if (!isFinite(duration))
562
+ return;
563
+ const time = calculateCircularTime(0, progressValue, 0.5, duration);
564
+ if (isFinite(time)) {
565
+ videoRef.current.currentTime = time;
566
+ }
567
+ }, [progressValue, hasDragged]);
568
+ return (jsxRuntime.jsxs("div", { style: { position: "relative", width: size, height: size }, children: [videoSrc && (jsxRuntime.jsx("video", { ref: videoRef, src: videoSrc, width: size, height: size, autoPlay: true, preload: "metadata", muted: true, playsInline: true, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, onLoadStart: handleLoadStartInternal, onCanPlay: handleCanPlayInternal, onLoadedData: () => {
569
+ if (videoRef.current) {
570
+ videoRef.current.pause();
571
+ }
572
+ }, style: {
573
+ cursor: isDragging ? "grabbing" : "grab",
574
+ touchAction: "none", // Prevents default touch behaviors like scrolling
575
+ display: "block",
576
+ }, children: "Your browser does not support the video tag." }, videoSrc)), jsxRuntime.jsx(LoadingErrorOverlay, { isVisible: isLoading || isRenderingBuild || !!renderError, renderError: renderError || undefined, size: size }), jsxRuntime.jsx(InstructionTooltip, { isVisible: !isLoading &&
577
+ !isRenderingBuild &&
578
+ !renderError &&
579
+ isBouncing &&
580
+ !hasDragged.current, progressValue: progressValue })] }));
581
+ };
582
+
583
+ exports.API_BASE_URL = API_BASE_URL;
584
+ exports.API_ENDPOINTS = API_ENDPOINTS;
585
+ exports.BuildRender = BuildRender;
586
+ exports.DragIcon = DragIcon;
587
+ exports.InstructionTooltip = InstructionTooltip;
588
+ exports.LoadingErrorOverlay = LoadingErrorOverlay;
589
+ exports.arePartsEqual = arePartsEqual;
590
+ exports.buildApiUrl = buildApiUrl;
591
+ exports.calculateCircularTime = calculateCircularTime;
592
+ exports.getAvailableParts = getAvailableParts;
593
+ exports.renderBuildExperimental = renderBuildExperimental;
594
+ exports.useBouncePatternProgress = useBouncePatternProgress;
595
+ exports.useBuildRender = useBuildRender;
596
+ exports.useVideoScrubbing = useVideoScrubbing;
597
+ //# sourceMappingURL=index.js.map