@xhub-reel/feed 0.1.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,1855 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@xhub-reel/core');
5
+ var gestures = require('@xhub-reel/gestures');
6
+ var player = require('@xhub-reel/player');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+ var api = require('@xhub-reel/core/api');
9
+ var reactQuery = require('@tanstack/react-query');
10
+
11
+ // src/components/VideoFeed.tsx
12
+ var videoFeedItemStyles = {
13
+ container: {
14
+ position: "relative",
15
+ width: "100%",
16
+ height: "100%",
17
+ backgroundColor: core.colors.background
18
+ },
19
+ video: {
20
+ position: "absolute",
21
+ inset: 0,
22
+ width: "100%",
23
+ height: "100%",
24
+ objectFit: "contain"
25
+ },
26
+ placeholder: {
27
+ position: "absolute",
28
+ inset: 0,
29
+ backgroundSize: "cover",
30
+ backgroundPosition: "center"
31
+ },
32
+ // Tap area for play/pause
33
+ tapArea: {
34
+ zIndex: core.zIndices.base},
35
+ // Pause overlay icon
36
+ pauseOverlay: {
37
+ zIndex: core.zIndices.overlay},
38
+ pauseIconWrapper: {
39
+ borderRadius: core.radii.full,
40
+ transition: `opacity ${core.durations.normal}ms ${core.easings.xhubReel}, transform ${core.durations.normal}ms ${core.easings.xhubReel}`
41
+ }
42
+ };
43
+ var VideoFeedItemContext = react.createContext(null);
44
+ function useVideoFeedItemContext() {
45
+ const context = react.useContext(VideoFeedItemContext);
46
+ if (!context) {
47
+ throw new Error(
48
+ "VideoFeedItem compound components must be used within a VideoFeedItem"
49
+ );
50
+ }
51
+ return context;
52
+ }
53
+ function useVideoVisibility({
54
+ elementRef,
55
+ activateThreshold = core.VIDEO_ACTIVATION.ACTIVATION_THRESHOLD,
56
+ deactivateThreshold = core.VIDEO_ACTIVATION.DEACTIVATION_THRESHOLD,
57
+ rootMargin = "0px",
58
+ onVisibilityChange
59
+ }) {
60
+ const [isVisible, setIsVisible] = react.useState(false);
61
+ const [isActive, setIsActive] = react.useState(false);
62
+ const [visibilityRatio, setVisibilityRatio] = react.useState(0);
63
+ const observerRef = react.useRef(null);
64
+ react.useEffect(() => {
65
+ const element = elementRef.current;
66
+ if (!element) return;
67
+ if (observerRef.current) {
68
+ observerRef.current.disconnect();
69
+ }
70
+ const thresholds = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
71
+ observerRef.current = new IntersectionObserver(
72
+ (entries) => {
73
+ entries.forEach((entry) => {
74
+ const ratio = entry.intersectionRatio;
75
+ setVisibilityRatio(ratio);
76
+ setIsVisible(ratio > 0);
77
+ if (ratio >= activateThreshold) {
78
+ setIsActive(true);
79
+ } else if (ratio < deactivateThreshold) {
80
+ setIsActive(false);
81
+ }
82
+ onVisibilityChange?.(ratio > 0, ratio);
83
+ });
84
+ },
85
+ {
86
+ threshold: thresholds,
87
+ rootMargin
88
+ }
89
+ );
90
+ observerRef.current.observe(element);
91
+ return () => {
92
+ observerRef.current?.disconnect();
93
+ };
94
+ }, [elementRef, activateThreshold, deactivateThreshold, rootMargin, onVisibilityChange]);
95
+ return {
96
+ isVisible,
97
+ isActive,
98
+ visibilityRatio
99
+ };
100
+ }
101
+
102
+ // src/hooks/useVideoActivation.ts
103
+ function useVideoActivation({
104
+ containerRef,
105
+ videoRef,
106
+ isCurrentVideo = false,
107
+ onActivate,
108
+ onDeactivate,
109
+ autoActivate = true,
110
+ trackVisibility = false,
111
+ onVisibilityChange
112
+ }) {
113
+ const wasActiveRef = react.useRef(false);
114
+ const {
115
+ isVisible: observerIsVisible,
116
+ isActive: observerIsActive,
117
+ visibilityRatio: observerRatio
118
+ } = useVideoVisibility({
119
+ elementRef: containerRef ?? { current: null },
120
+ onVisibilityChange: trackVisibility ? onVisibilityChange : void 0
121
+ });
122
+ const effectiveIsActive = trackVisibility ? observerIsActive : isCurrentVideo;
123
+ react.useEffect(() => {
124
+ if (!autoActivate) return;
125
+ if (effectiveIsActive && !wasActiveRef.current) {
126
+ wasActiveRef.current = true;
127
+ onActivate?.();
128
+ } else if (!effectiveIsActive && wasActiveRef.current) {
129
+ wasActiveRef.current = false;
130
+ onDeactivate?.();
131
+ }
132
+ }, [effectiveIsActive, onActivate, onDeactivate, autoActivate]);
133
+ const activate = react.useCallback(() => {
134
+ const video = videoRef.current;
135
+ if (video) {
136
+ video.play().catch(() => {
137
+ video.muted = true;
138
+ video.play().catch(() => {
139
+ });
140
+ });
141
+ }
142
+ onActivate?.();
143
+ }, [videoRef, onActivate]);
144
+ const deactivate = react.useCallback(() => {
145
+ const video = videoRef.current;
146
+ if (video) {
147
+ video.pause();
148
+ video.currentTime = 0;
149
+ }
150
+ onDeactivate?.();
151
+ }, [videoRef, onDeactivate]);
152
+ return {
153
+ isActive: effectiveIsActive,
154
+ // Only meaningful when trackVisibility is enabled
155
+ isVisible: trackVisibility ? observerIsVisible : isCurrentVideo,
156
+ visibilityRatio: trackVisibility ? observerRatio : isCurrentVideo ? 1 : 0,
157
+ activate,
158
+ deactivate
159
+ };
160
+ }
161
+ var MemoryManager = class {
162
+ entries = /* @__PURE__ */ new Map();
163
+ listeners = /* @__PURE__ */ new Set();
164
+ memoryWarningThreshold;
165
+ constructor() {
166
+ this.memoryWarningThreshold = core.MEMORY_CONFIG?.MAX_TOTAL_MEMORY ? core.MEMORY_CONFIG.MAX_TOTAL_MEMORY / (1024 * 1024) : 150;
167
+ this.setupMemoryPressureListener();
168
+ }
169
+ /**
170
+ * Register a video
171
+ */
172
+ register(videoId, estimatedSizeMB = 10) {
173
+ this.entries.set(videoId, {
174
+ videoId,
175
+ inDom: false,
176
+ hasDecodedFrames: false,
177
+ estimatedSizeMB,
178
+ lastAccessed: Date.now()
179
+ });
180
+ this.notifyListeners();
181
+ }
182
+ /**
183
+ * Unregister a video
184
+ */
185
+ unregister(videoId) {
186
+ this.entries.delete(videoId);
187
+ this.notifyListeners();
188
+ }
189
+ /**
190
+ * Mark video as in DOM
191
+ */
192
+ setInDom(videoId, inDom) {
193
+ const entry = this.entries.get(videoId);
194
+ if (entry) {
195
+ entry.inDom = inDom;
196
+ entry.lastAccessed = Date.now();
197
+ this.notifyListeners();
198
+ }
199
+ }
200
+ /**
201
+ * Mark video as having decoded frames
202
+ */
203
+ setHasDecodedFrames(videoId, hasFrames) {
204
+ const entry = this.entries.get(videoId);
205
+ if (entry) {
206
+ entry.hasDecodedFrames = hasFrames;
207
+ entry.lastAccessed = Date.now();
208
+ this.notifyListeners();
209
+ }
210
+ }
211
+ /**
212
+ * Get current memory state
213
+ */
214
+ getState() {
215
+ let videosInDom = 0;
216
+ let decodedVideos = 0;
217
+ let estimatedMemoryMB = 0;
218
+ this.entries.forEach((entry) => {
219
+ if (entry.inDom) videosInDom++;
220
+ if (entry.hasDecodedFrames) decodedVideos++;
221
+ estimatedMemoryMB += entry.estimatedSizeMB;
222
+ });
223
+ return {
224
+ videosInDom,
225
+ decodedVideos,
226
+ estimatedMemoryMB,
227
+ isLowMemory: estimatedMemoryMB > this.memoryWarningThreshold
228
+ };
229
+ }
230
+ /**
231
+ * Get videos that should be disposed (LRU)
232
+ */
233
+ getVideosToDispose() {
234
+ const state = this.getState();
235
+ const toDispose = [];
236
+ const maxInDom = core.MEMORY_CONFIG?.MAX_VIDEOS_IN_DOM ?? 5;
237
+ const maxDecoded = core.MEMORY_CONFIG?.MAX_DECODED_FRAMES ?? 3;
238
+ const sortedEntries = Array.from(this.entries.values()).sort(
239
+ (a, b) => a.lastAccessed - b.lastAccessed
240
+ );
241
+ if (state.videosInDom > maxInDom) {
242
+ const toRemove = state.videosInDom - maxInDom;
243
+ let removed = 0;
244
+ for (const entry of sortedEntries) {
245
+ if (removed >= toRemove) break;
246
+ if (entry.inDom) {
247
+ toDispose.push(entry.videoId);
248
+ removed++;
249
+ }
250
+ }
251
+ }
252
+ if (state.decodedVideos > maxDecoded) {
253
+ const toRemove = state.decodedVideos - maxDecoded;
254
+ let removed = 0;
255
+ for (const entry of sortedEntries) {
256
+ if (removed >= toRemove) break;
257
+ if (entry.hasDecodedFrames && !toDispose.includes(entry.videoId)) {
258
+ toDispose.push(entry.videoId);
259
+ removed++;
260
+ }
261
+ }
262
+ }
263
+ return toDispose;
264
+ }
265
+ /**
266
+ * Subscribe to memory state changes
267
+ */
268
+ subscribe(listener) {
269
+ this.listeners.add(listener);
270
+ return () => this.listeners.delete(listener);
271
+ }
272
+ /**
273
+ * Force cleanup
274
+ */
275
+ forceCleanup() {
276
+ const toDispose = this.getVideosToDispose();
277
+ toDispose.forEach((id) => this.unregister(id));
278
+ return toDispose;
279
+ }
280
+ notifyListeners() {
281
+ const state = this.getState();
282
+ this.listeners.forEach((listener) => listener(state));
283
+ }
284
+ setupMemoryPressureListener() {
285
+ if (typeof window === "undefined") return;
286
+ if ("memory" in performance) {
287
+ setInterval(() => {
288
+ const state = this.getState();
289
+ if (state.isLowMemory) {
290
+ console.warn("[MemoryManager] Low memory warning, forcing cleanup");
291
+ this.forceCleanup();
292
+ }
293
+ }, 5e3);
294
+ }
295
+ }
296
+ };
297
+ var memoryManager = new MemoryManager();
298
+
299
+ // src/hooks/useMemoryManager.ts
300
+ function useMemoryManager({
301
+ videoId,
302
+ estimatedSizeMB = 10,
303
+ onShouldDispose
304
+ }) {
305
+ const [memoryState, setMemoryState] = react.useState(
306
+ () => memoryManager.getState()
307
+ );
308
+ const [shouldDispose, setShouldDispose] = react.useState(false);
309
+ react.useEffect(() => {
310
+ memoryManager.register(videoId, estimatedSizeMB);
311
+ return () => {
312
+ memoryManager.unregister(videoId);
313
+ };
314
+ }, [videoId, estimatedSizeMB]);
315
+ react.useEffect(() => {
316
+ const unsubscribe = memoryManager.subscribe((state) => {
317
+ setMemoryState(state);
318
+ const toDispose = memoryManager.getVideosToDispose();
319
+ const shouldDisposeThis = toDispose.includes(videoId);
320
+ if (shouldDisposeThis && !shouldDispose) {
321
+ setShouldDispose(true);
322
+ onShouldDispose?.();
323
+ } else if (!shouldDisposeThis && shouldDispose) {
324
+ setShouldDispose(false);
325
+ }
326
+ });
327
+ return unsubscribe;
328
+ }, [videoId, shouldDispose, onShouldDispose]);
329
+ const setInDom = react.useCallback(
330
+ (inDom) => {
331
+ memoryManager.setInDom(videoId, inDom);
332
+ },
333
+ [videoId]
334
+ );
335
+ const setHasDecodedFrames = react.useCallback(
336
+ (hasFrames) => {
337
+ memoryManager.setHasDecodedFrames(videoId, hasFrames);
338
+ },
339
+ [videoId]
340
+ );
341
+ return {
342
+ memoryState,
343
+ setInDom,
344
+ setHasDecodedFrames,
345
+ shouldDispose
346
+ };
347
+ }
348
+ function useGlobalMemoryState() {
349
+ const [state, setState] = react.useState(() => memoryManager.getState());
350
+ react.useEffect(() => {
351
+ const unsubscribe = memoryManager.subscribe(setState);
352
+ return unsubscribe;
353
+ }, []);
354
+ return state;
355
+ }
356
+
357
+ // src/components/VideoFeedItem/useVideoFeedItemState.ts
358
+ function useVideoFeedItemState({
359
+ video,
360
+ isActive,
361
+ priority,
362
+ onLike,
363
+ onComment,
364
+ onShare,
365
+ onAuthorClick
366
+ }) {
367
+ const containerRef = react.useRef(null);
368
+ const videoRef = react.useRef(null);
369
+ const wasPlayingBeforeSeekRef = react.useRef(false);
370
+ const [showPauseOverlay, setShowPauseOverlay] = react.useState(false);
371
+ const [timelineExpanded, setTimelineExpanded] = react.useState(false);
372
+ const [isPreloaded, setIsPreloaded] = react.useState(false);
373
+ const {
374
+ isShowing: showHeart,
375
+ position: heartPosition,
376
+ showHeart: triggerHeart
377
+ } = player.useDoubleTapHeart();
378
+ const preloadConfig = react.useMemo(
379
+ () => ({
380
+ maxConcurrent: 2,
381
+ maxBufferSize: 10 * 1024 * 1024,
382
+ priorityLevels: 10
383
+ }),
384
+ []
385
+ );
386
+ const handleNetworkChange = react.useCallback((_info) => {
387
+ }, []);
388
+ const handlePowerChange = react.useCallback((_info) => {
389
+ }, []);
390
+ const handleAnalyticsUpdate = react.useCallback(
391
+ (metrics) => {
392
+ if (metrics.startupTime && metrics.startupTime > 1e3) {
393
+ console.warn("[VideoFeedItem] Slow startup:", metrics.startupTime, "ms", video.id);
394
+ }
395
+ },
396
+ [video.id]
397
+ );
398
+ const { state } = player.usePlayer(videoRef, containerRef, {
399
+ preferNative: true,
400
+ enableSmoothTimeUpdates: true,
401
+ networkBehavior: "feed",
402
+ powerBehavior: "moderate",
403
+ preloadConfig,
404
+ enableAnalytics: true,
405
+ onPlay: () => {
406
+ setShowPauseOverlay(false);
407
+ setTimelineExpanded(false);
408
+ },
409
+ onPause: () => {
410
+ if (!videoRef.current?.seeking) {
411
+ setShowPauseOverlay(true);
412
+ setTimelineExpanded(true);
413
+ }
414
+ },
415
+ onNetworkChange: handleNetworkChange,
416
+ onPowerChange: handlePowerChange,
417
+ onAnalyticsUpdate: handleAnalyticsUpdate
418
+ });
419
+ const play = react.useCallback(async () => {
420
+ const videoEl = videoRef.current;
421
+ if (videoEl) {
422
+ videoEl.muted = true;
423
+ try {
424
+ await videoEl.play();
425
+ } catch (err) {
426
+ console.warn("[VideoFeedItem] Play failed:", err.message);
427
+ }
428
+ }
429
+ }, []);
430
+ const pause = react.useCallback(() => {
431
+ const videoEl = videoRef.current;
432
+ if (videoEl) {
433
+ videoEl.pause();
434
+ }
435
+ }, []);
436
+ const seek = react.useCallback((time) => {
437
+ const videoEl = videoRef.current;
438
+ if (videoEl) {
439
+ videoEl.currentTime = time;
440
+ }
441
+ }, []);
442
+ const [isPlaying, setIsPlaying] = react.useState(false);
443
+ react.useEffect(() => {
444
+ const videoEl = videoRef.current;
445
+ if (!videoEl) return;
446
+ const handlePlay = () => setIsPlaying(true);
447
+ const handlePause = () => setIsPlaying(false);
448
+ const handleEnded = () => setIsPlaying(false);
449
+ videoEl.addEventListener("play", handlePlay);
450
+ videoEl.addEventListener("pause", handlePause);
451
+ videoEl.addEventListener("ended", handleEnded);
452
+ setIsPlaying(!videoEl.paused);
453
+ return () => {
454
+ videoEl.removeEventListener("play", handlePlay);
455
+ videoEl.removeEventListener("pause", handlePause);
456
+ videoEl.removeEventListener("ended", handleEnded);
457
+ };
458
+ }, [video.id]);
459
+ const effectiveIsPlaying = isPlaying || state.state === "playing";
460
+ const { setInDom, setHasDecodedFrames, shouldDispose } = useMemoryManager({
461
+ videoId: video.id,
462
+ onShouldDispose: () => {
463
+ pause();
464
+ if (videoRef.current) {
465
+ videoRef.current.src = "";
466
+ videoRef.current.load();
467
+ }
468
+ }
469
+ });
470
+ const shouldRenderVideo = !shouldDispose && priority !== "none";
471
+ const pendingPlayRef = react.useRef(false);
472
+ useVideoActivation({
473
+ videoRef,
474
+ isCurrentVideo: isActive,
475
+ onActivate: () => {
476
+ console.log("[VideoFeedItem] onActivate called, videoRef:", videoRef.current?.src);
477
+ setHasDecodedFrames(true);
478
+ if (!videoRef.current) {
479
+ console.log("[VideoFeedItem] Video element not ready, marking pending play");
480
+ pendingPlayRef.current = true;
481
+ } else {
482
+ play();
483
+ }
484
+ },
485
+ onDeactivate: () => {
486
+ console.log("[VideoFeedItem] onDeactivate called");
487
+ pendingPlayRef.current = false;
488
+ setHasDecodedFrames(false);
489
+ pause();
490
+ seek(0);
491
+ },
492
+ autoActivate: true
493
+ });
494
+ react.useEffect(() => {
495
+ const videoEl = videoRef.current;
496
+ if (videoEl && pendingPlayRef.current && isActive) {
497
+ console.log("[VideoFeedItem] Video element now available, executing pending play");
498
+ pendingPlayRef.current = false;
499
+ play();
500
+ }
501
+ });
502
+ react.useEffect(() => {
503
+ setInDom(true);
504
+ return () => setInDom(false);
505
+ }, [setInDom]);
506
+ react.useEffect(() => {
507
+ const videoEl = videoRef.current;
508
+ if (!videoEl) return;
509
+ setIsPreloaded(false);
510
+ const handleLoadedData = () => {
511
+ console.log("[VideoFeedItem] Video loadeddata:", video.id, { isActive, priority });
512
+ if (priority === "high" && !isActive) {
513
+ requestAnimationFrame(() => {
514
+ if (videoEl.readyState >= 2) {
515
+ videoEl.currentTime = 0.01;
516
+ setIsPreloaded(true);
517
+ console.log("[VideoFeedItem] First frame decoded (preloaded):", video.id);
518
+ }
519
+ });
520
+ } else if (isActive) {
521
+ setIsPreloaded(true);
522
+ }
523
+ };
524
+ const handleCanPlay = () => {
525
+ if (priority === "high" && !isPreloaded) {
526
+ setIsPreloaded(true);
527
+ }
528
+ };
529
+ videoEl.addEventListener("loadeddata", handleLoadedData);
530
+ videoEl.addEventListener("canplay", handleCanPlay);
531
+ if (videoEl.readyState >= 2) {
532
+ handleLoadedData();
533
+ }
534
+ return () => {
535
+ videoEl.removeEventListener("loadeddata", handleLoadedData);
536
+ videoEl.removeEventListener("canplay", handleCanPlay);
537
+ };
538
+ }, [video.id, isActive, priority, isPreloaded]);
539
+ react.useEffect(() => {
540
+ console.log("[VideoFeedItem] State:", {
541
+ videoId: video.id,
542
+ isActive,
543
+ priority,
544
+ shouldRenderVideo,
545
+ hasVideoElement: !!videoRef.current,
546
+ videoSrc: videoRef.current?.src
547
+ });
548
+ }, [video.id, isActive, priority, shouldRenderVideo]);
549
+ const preload = react.useMemo(() => {
550
+ switch (priority) {
551
+ case "high":
552
+ return "auto";
553
+ case "medium":
554
+ return "metadata";
555
+ case "low":
556
+ case "metadata":
557
+ return "none";
558
+ default:
559
+ return "none";
560
+ }
561
+ }, [priority]);
562
+ const handleSeekStart = react.useCallback(() => {
563
+ wasPlayingBeforeSeekRef.current = effectiveIsPlaying;
564
+ setTimelineExpanded(true);
565
+ setShowPauseOverlay(false);
566
+ pause();
567
+ }, [effectiveIsPlaying, pause]);
568
+ const handleSeekEnd = react.useCallback(
569
+ (time) => {
570
+ seek(time);
571
+ if (wasPlayingBeforeSeekRef.current) {
572
+ play();
573
+ setTimelineExpanded(false);
574
+ } else {
575
+ setShowPauseOverlay(true);
576
+ }
577
+ },
578
+ [seek, play]
579
+ );
580
+ const handleSingleTap = react.useCallback(() => {
581
+ if (effectiveIsPlaying) {
582
+ pause();
583
+ } else {
584
+ play();
585
+ }
586
+ }, [effectiveIsPlaying, play, pause]);
587
+ const handleDoubleTap = react.useCallback(
588
+ (_zone, position) => {
589
+ triggerHeart(position.x, position.y);
590
+ onLike?.();
591
+ },
592
+ [triggerHeart, onLike]
593
+ );
594
+ const handleLongPress = react.useCallback(() => {
595
+ }, []);
596
+ const gestureBindings = gestures.useVideoGestures({
597
+ onSingleTap: handleSingleTap,
598
+ onDoubleTap: handleDoubleTap,
599
+ onLongPress: handleLongPress
600
+ });
601
+ return {
602
+ video,
603
+ isActive,
604
+ shouldRenderVideo,
605
+ preload,
606
+ isPreloaded,
607
+ containerRef,
608
+ videoRef,
609
+ isPlaying: effectiveIsPlaying,
610
+ showPauseOverlay,
611
+ timelineExpanded,
612
+ play,
613
+ pause,
614
+ seek,
615
+ setShowPauseOverlay,
616
+ setTimelineExpanded,
617
+ gestureBindings,
618
+ showHeart,
619
+ heartPosition,
620
+ triggerHeart,
621
+ onLike,
622
+ onComment,
623
+ onShare,
624
+ onAuthorClick,
625
+ handleSeekStart,
626
+ handleSeekEnd
627
+ };
628
+ }
629
+ var VideoFeedItemPlayer = react.forwardRef(
630
+ ({ placeholder, ...props }, ref) => {
631
+ const { video, videoRef, shouldRenderVideo, preload, isPreloaded } = useVideoFeedItemContext();
632
+ if (!shouldRenderVideo) {
633
+ return placeholder ?? /* @__PURE__ */ jsxRuntime.jsx(
634
+ "div",
635
+ {
636
+ ...props,
637
+ style: {
638
+ ...videoFeedItemStyles.placeholder,
639
+ backgroundImage: `url(${video.thumbnail})`,
640
+ ...props.style
641
+ }
642
+ }
643
+ );
644
+ }
645
+ const showPoster = !isPreloaded;
646
+ return /* @__PURE__ */ jsxRuntime.jsx(
647
+ "video",
648
+ {
649
+ ref: (node) => {
650
+ if (typeof ref === "function") {
651
+ ref(node);
652
+ } else if (ref) {
653
+ ref.current = node;
654
+ }
655
+ videoRef.current = node;
656
+ },
657
+ src: video.url,
658
+ poster: showPoster ? video.thumbnail : void 0,
659
+ preload,
660
+ loop: true,
661
+ playsInline: true,
662
+ muted: true,
663
+ style: videoFeedItemStyles.video
664
+ }
665
+ );
666
+ }
667
+ );
668
+ VideoFeedItemPlayer.displayName = "VideoFeedItemPlayer";
669
+ var VideoFeedItemActions = react.forwardRef(
670
+ ({ onLike: onLikeProp, onComment: onCommentProp, onShare: onShareProp, ...props }, ref) => {
671
+ const {
672
+ video,
673
+ onLike: contextOnLike,
674
+ onComment: contextOnComment,
675
+ onShare: contextOnShare
676
+ } = useVideoFeedItemContext();
677
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
678
+ player.ActionBar,
679
+ {
680
+ avatarUrl: video.author.avatar,
681
+ likeCount: video.stats.likes,
682
+ commentCount: video.stats.comments,
683
+ shareCount: video.stats.shares,
684
+ isLiked: video.isLiked,
685
+ onLike: onLikeProp ?? contextOnLike,
686
+ onComment: onCommentProp ?? contextOnComment,
687
+ onShare: onShareProp ?? contextOnShare
688
+ }
689
+ ) });
690
+ }
691
+ );
692
+ VideoFeedItemActions.displayName = "VideoFeedItemActions";
693
+ var VideoFeedItemTimeline = react.forwardRef(
694
+ ({ expanded: expandedProp, ...props }, ref) => {
695
+ const {
696
+ videoRef,
697
+ shouldRenderVideo,
698
+ timelineExpanded,
699
+ setTimelineExpanded,
700
+ handleSeekStart,
701
+ handleSeekEnd
702
+ } = useVideoFeedItemContext();
703
+ if (!shouldRenderVideo) {
704
+ return null;
705
+ }
706
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
707
+ player.Timeline,
708
+ {
709
+ videoRef,
710
+ expanded: expandedProp ?? timelineExpanded,
711
+ onSeekStart: handleSeekStart,
712
+ onSeekEnd: handleSeekEnd,
713
+ onExpandedChange: setTimelineExpanded
714
+ }
715
+ ) });
716
+ }
717
+ );
718
+ VideoFeedItemTimeline.displayName = "VideoFeedItemTimeline";
719
+ var overlayStyles = {
720
+ container: {
721
+ position: "absolute",
722
+ bottom: 0,
723
+ left: 0,
724
+ right: 64,
725
+ // Space for ActionBar (48px + 16px spacing)
726
+ padding: core.spacing[4],
727
+ paddingBottom: "env(safe-area-inset-bottom, 16px)",
728
+ zIndex: core.zIndices.base
729
+ },
730
+ containerWithTimeline: {
731
+ paddingBottom: "calc(env(safe-area-inset-bottom, 16px) + 48px)"
732
+ },
733
+ authorButton: {
734
+ gap: core.spacing[2],
735
+ marginBottom: core.spacing[3]},
736
+ avatar: {
737
+ borderRadius: core.radii.full,
738
+ border: `2px solid ${core.colors.text}`},
739
+ username: {
740
+ color: core.colors.text,
741
+ fontWeight: core.fontWeights.semibold,
742
+ fontSize: core.fontSizes.sm,
743
+ textShadow: core.shadows.text
744
+ },
745
+ caption: {
746
+ color: core.colors.text,
747
+ fontSize: core.fontSizes.sm,
748
+ lineHeight: 1.4,
749
+ textShadow: core.shadows.text,
750
+ display: "-webkit-box",
751
+ WebkitLineClamp: 2,
752
+ WebkitBoxOrient: "vertical",
753
+ overflow: "hidden",
754
+ paddingBottom: core.spacing[1]
755
+ },
756
+ author: {
757
+ color: core.colors.text,
758
+ fontSize: core.fontSizes.sm,
759
+ fontWeight: core.fontWeights.semibold,
760
+ textShadow: core.shadows.text,
761
+ display: "-webkit-box",
762
+ WebkitLineClamp: 1,
763
+ WebkitBoxOrient: "vertical",
764
+ overflow: "hidden",
765
+ paddingBottom: core.spacing[1]
766
+ },
767
+ hashtags: {
768
+ display: "flex",
769
+ flexWrap: "wrap",
770
+ gap: core.spacing[1],
771
+ marginTop: core.spacing[2],
772
+ paddingBottom: core.spacing[1]
773
+ },
774
+ hashtag: {
775
+ color: core.colors.accent,
776
+ fontSize: core.fontSizes.sm,
777
+ fontWeight: core.fontWeights.medium,
778
+ textShadow: core.shadows.text
779
+ }
780
+ };
781
+ function VideoOverlay({
782
+ video,
783
+ timelineExpanded = false,
784
+ style,
785
+ className = ""
786
+ }) {
787
+ const containerStyles = core.mergeStyles(
788
+ overlayStyles.container,
789
+ timelineExpanded && overlayStyles.containerWithTimeline,
790
+ style
791
+ );
792
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyles, className, children: [
793
+ video.author && /* @__PURE__ */ jsxRuntime.jsx("p", { style: overlayStyles.author, children: video.author.displayName }),
794
+ video.caption && /* @__PURE__ */ jsxRuntime.jsx("p", { style: overlayStyles.caption, children: video.caption }),
795
+ video.hashtags && video.hashtags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlayStyles.hashtags, children: video.hashtags.slice(0, 3).map((tag, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { style: overlayStyles.hashtag, children: [
796
+ "#",
797
+ tag
798
+ ] }, i)) })
799
+ ] });
800
+ }
801
+ var VideoFeedItemOverlay = react.forwardRef(
802
+ ({
803
+ showPlayPause = true,
804
+ showDoubleTapHeart = true,
805
+ showVideoInfo = true,
806
+ ...props
807
+ }, ref) => {
808
+ const {
809
+ video,
810
+ isPlaying,
811
+ showPauseOverlay,
812
+ timelineExpanded,
813
+ showHeart,
814
+ heartPosition,
815
+ onAuthorClick
816
+ } = useVideoFeedItemContext();
817
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, ...props, children: [
818
+ showPlayPause && /* @__PURE__ */ jsxRuntime.jsx(
819
+ player.PlayPauseOverlay,
820
+ {
821
+ isPlaying,
822
+ show: showPauseOverlay,
823
+ size: 64,
824
+ autoHideDelay: 800,
825
+ showOnStateChange: false
826
+ }
827
+ ),
828
+ showDoubleTapHeart && /* @__PURE__ */ jsxRuntime.jsx(
829
+ player.DoubleTapHeart,
830
+ {
831
+ show: showHeart,
832
+ position: heartPosition,
833
+ size: 100,
834
+ showParticles: true,
835
+ particleCount: 8
836
+ }
837
+ ),
838
+ showVideoInfo && /* @__PURE__ */ jsxRuntime.jsx(
839
+ VideoOverlay,
840
+ {
841
+ video,
842
+ onAuthorClick,
843
+ timelineExpanded
844
+ }
845
+ )
846
+ ] });
847
+ }
848
+ );
849
+ VideoFeedItemOverlay.displayName = "VideoFeedItemOverlay";
850
+ var VideoFeedItem = react.forwardRef(
851
+ ({
852
+ video,
853
+ isActive = false,
854
+ priority = "none",
855
+ showTimeline = true,
856
+ onLike,
857
+ onComment,
858
+ onShare,
859
+ onAuthorClick,
860
+ style,
861
+ className = "",
862
+ children
863
+ }, ref) => {
864
+ const state = useVideoFeedItemState({
865
+ video,
866
+ isActive,
867
+ priority,
868
+ onLike,
869
+ onComment,
870
+ onShare,
871
+ onAuthorClick
872
+ });
873
+ const content = children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
874
+ /* @__PURE__ */ jsxRuntime.jsx(VideoFeedItemPlayer, {}),
875
+ /* @__PURE__ */ jsxRuntime.jsx(VideoFeedItemOverlay, {}),
876
+ /* @__PURE__ */ jsxRuntime.jsx(VideoFeedItemActions, {}),
877
+ showTimeline && /* @__PURE__ */ jsxRuntime.jsx(VideoFeedItemTimeline, {})
878
+ ] });
879
+ return /* @__PURE__ */ jsxRuntime.jsx(VideoFeedItemContext.Provider, { value: state, children: /* @__PURE__ */ jsxRuntime.jsx(
880
+ "div",
881
+ {
882
+ ref: (node) => {
883
+ if (typeof ref === "function") {
884
+ ref(node);
885
+ } else if (ref) {
886
+ ref.current = node;
887
+ }
888
+ state.containerRef.current = node;
889
+ },
890
+ style: core.mergeStyles(videoFeedItemStyles.container, style),
891
+ className,
892
+ ...state.gestureBindings(),
893
+ children: content
894
+ }
895
+ ) });
896
+ }
897
+ );
898
+ VideoFeedItem.displayName = "VideoFeedItem";
899
+ function getPreloadPriorityForFeed(index, currentIndex) {
900
+ const distance = Math.abs(index - currentIndex);
901
+ if (distance === 0) return 1;
902
+ if (distance === 1) return 3;
903
+ if (distance === 2) return 5;
904
+ if (distance === 3) return 7;
905
+ return 10;
906
+ }
907
+ function mapPriorityToNumeric(priority) {
908
+ switch (priority) {
909
+ case "high":
910
+ return 1;
911
+ case "medium":
912
+ return 3;
913
+ case "low":
914
+ return 5;
915
+ case "metadata":
916
+ return 7;
917
+ default:
918
+ return 10;
919
+ }
920
+ }
921
+ function getPreloadPriority(index, currentIndex) {
922
+ const distance = index - currentIndex;
923
+ if (distance === 0) return "high";
924
+ if (distance === -1) return "high";
925
+ if (distance === 1) return "high";
926
+ if (distance === 2) return "medium";
927
+ if (distance === 3) return "low";
928
+ if (Math.abs(distance) <= 5) return "metadata";
929
+ return "none";
930
+ }
931
+ var SPRING_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
932
+ var DEFAULT_DURATION = 300;
933
+ var DEFAULT_VIEWPORT_HEIGHT = 800;
934
+ var TRANSITION_END_BUFFER = 50;
935
+ function useSwipeAnimation({
936
+ trackRef,
937
+ transitionDuration = DEFAULT_DURATION,
938
+ easing = SPRING_EASING,
939
+ onTransitionEnd
940
+ }) {
941
+ const viewportHeightRef = react.useRef(
942
+ typeof window !== "undefined" ? window.innerHeight : DEFAULT_VIEWPORT_HEIGHT
943
+ );
944
+ const currentYRef = react.useRef(0);
945
+ const rafIdRef = react.useRef(null);
946
+ const isAnimatingRef = react.useRef(false);
947
+ const isMountedRef = react.useRef(true);
948
+ react.useEffect(() => {
949
+ const handleResize = () => {
950
+ viewportHeightRef.current = window.innerHeight;
951
+ };
952
+ const handleOrientationChange = () => {
953
+ setTimeout(() => {
954
+ viewportHeightRef.current = window.innerHeight;
955
+ }, 100);
956
+ };
957
+ window.addEventListener("resize", handleResize, { passive: true });
958
+ window.addEventListener("orientationchange", handleOrientationChange, { passive: true });
959
+ return () => {
960
+ window.removeEventListener("resize", handleResize);
961
+ window.removeEventListener("orientationchange", handleOrientationChange);
962
+ };
963
+ }, []);
964
+ const setTranslateY = react.useCallback((y) => {
965
+ currentYRef.current = y;
966
+ if (rafIdRef.current !== null) {
967
+ cancelAnimationFrame(rafIdRef.current);
968
+ }
969
+ rafIdRef.current = requestAnimationFrame(() => {
970
+ const track = trackRef.current;
971
+ if (track) {
972
+ track.style.transition = "none";
973
+ track.style.transform = `translateY(${y}px)`;
974
+ }
975
+ rafIdRef.current = null;
976
+ });
977
+ }, [trackRef]);
978
+ const animateTo = react.useCallback((targetY) => {
979
+ return new Promise((resolve) => {
980
+ const track = trackRef.current;
981
+ if (!track || !isMountedRef.current) {
982
+ resolve();
983
+ return;
984
+ }
985
+ if (isAnimatingRef.current) {
986
+ resolve();
987
+ return;
988
+ }
989
+ isAnimatingRef.current = true;
990
+ currentYRef.current = targetY;
991
+ let cleanup = null;
992
+ let fallbackTimeout = null;
993
+ const handleTransitionEnd = (e) => {
994
+ if (e.propertyName !== "transform") return;
995
+ cleanup?.();
996
+ if (isMountedRef.current) {
997
+ isAnimatingRef.current = false;
998
+ onTransitionEnd?.();
999
+ }
1000
+ resolve();
1001
+ };
1002
+ cleanup = () => {
1003
+ track.removeEventListener("transitionend", handleTransitionEnd);
1004
+ if (fallbackTimeout) {
1005
+ clearTimeout(fallbackTimeout);
1006
+ fallbackTimeout = null;
1007
+ }
1008
+ };
1009
+ track.addEventListener("transitionend", handleTransitionEnd);
1010
+ fallbackTimeout = setTimeout(() => {
1011
+ cleanup?.();
1012
+ if (isMountedRef.current && isAnimatingRef.current) {
1013
+ isAnimatingRef.current = false;
1014
+ onTransitionEnd?.();
1015
+ }
1016
+ resolve();
1017
+ }, transitionDuration + TRANSITION_END_BUFFER);
1018
+ track.offsetHeight;
1019
+ track.style.transition = `transform ${transitionDuration}ms ${easing}`;
1020
+ track.style.transform = `translateY(${targetY}px)`;
1021
+ });
1022
+ }, [trackRef, transitionDuration, easing, onTransitionEnd]);
1023
+ const snapBack = react.useCallback(() => {
1024
+ return animateTo(0);
1025
+ }, [animateTo]);
1026
+ const getCurrentY = react.useCallback(() => {
1027
+ return currentYRef.current;
1028
+ }, []);
1029
+ react.useEffect(() => {
1030
+ isMountedRef.current = true;
1031
+ return () => {
1032
+ isMountedRef.current = false;
1033
+ if (rafIdRef.current !== null) {
1034
+ cancelAnimationFrame(rafIdRef.current);
1035
+ rafIdRef.current = null;
1036
+ }
1037
+ };
1038
+ }, []);
1039
+ return {
1040
+ setTranslateY,
1041
+ animateTo,
1042
+ snapBack,
1043
+ getCurrentY,
1044
+ viewportHeight: viewportHeightRef.current,
1045
+ isAnimating: isAnimatingRef.current
1046
+ };
1047
+ }
1048
+ var DEFAULT_TRANSITION_DURATION = 300;
1049
+ var DEFAULT_SWIPE_THRESHOLD = 50;
1050
+ var DEFAULT_VELOCITY_THRESHOLD = 0.3;
1051
+ var SPRING_EASING2 = "cubic-bezier(0.32, 0.72, 0, 1)";
1052
+ var feedStyles = {
1053
+ container: {
1054
+ position: "fixed",
1055
+ inset: 0,
1056
+ overflow: "hidden",
1057
+ backgroundColor: core.colors.background,
1058
+ touchAction: "none",
1059
+ // Prevent browser gestures, @use-gesture handles all
1060
+ userSelect: "none",
1061
+ WebkitUserSelect: "none"
1062
+ },
1063
+ track: {
1064
+ position: "relative",
1065
+ width: "100%",
1066
+ height: "100%",
1067
+ willChange: "transform"
1068
+ },
1069
+ slide: {
1070
+ position: "absolute",
1071
+ left: 0,
1072
+ width: "100%",
1073
+ height: "100%",
1074
+ backfaceVisibility: "hidden",
1075
+ WebkitBackfaceVisibility: "hidden"
1076
+ },
1077
+ loadingIndicator: {
1078
+ position: "absolute",
1079
+ bottom: 80,
1080
+ left: 0,
1081
+ right: 0,
1082
+ display: "flex",
1083
+ justifyContent: "center",
1084
+ zIndex: core.zIndices.base,
1085
+ pointerEvents: "none"
1086
+ },
1087
+ spinner: {
1088
+ width: 24,
1089
+ height: 24,
1090
+ borderWidth: 2,
1091
+ borderStyle: "solid",
1092
+ borderColor: "rgba(255, 255, 255, 0.3)",
1093
+ borderTopColor: core.colors.text,
1094
+ borderRadius: core.radii.full,
1095
+ animation: "xhub-reel-spin 1s linear infinite"
1096
+ }
1097
+ };
1098
+ var VideoFeed = react.forwardRef(
1099
+ ({
1100
+ videos,
1101
+ initialIndex = 0,
1102
+ onLoadMore,
1103
+ onVideoChange,
1104
+ onLike,
1105
+ onComment,
1106
+ onShare,
1107
+ onAuthorClick,
1108
+ isLoading = false,
1109
+ hasMore = false,
1110
+ loadMoreThreshold = 3,
1111
+ transitionDuration = DEFAULT_TRANSITION_DURATION,
1112
+ swipeThreshold = DEFAULT_SWIPE_THRESHOLD,
1113
+ velocityThreshold = DEFAULT_VELOCITY_THRESHOLD,
1114
+ gesturesDisabled = false,
1115
+ hapticEnabled = true,
1116
+ style,
1117
+ className = ""
1118
+ }, ref) => {
1119
+ const [currentIndex, setCurrentIndex] = react.useState(
1120
+ () => Math.min(Math.max(0, initialIndex), Math.max(0, videos.length - 1))
1121
+ );
1122
+ const [isLocked, setIsLocked] = react.useState(false);
1123
+ const [isAnimating, setIsAnimating] = react.useState(false);
1124
+ const containerRef = react.useRef(null);
1125
+ const trackRef = react.useRef(null);
1126
+ const videosRef = react.useRef(videos);
1127
+ const currentIndexRef = react.useRef(currentIndex);
1128
+ const isLoadingRef = react.useRef(isLoading);
1129
+ const hasMoreRef = react.useRef(hasMore);
1130
+ const isLockedRef = react.useRef(isLocked);
1131
+ const { setCurrentIndex: storeSetCurrentIndex } = core.useFeedStore();
1132
+ const {
1133
+ setTranslateY,
1134
+ // Direct DOM manipulation, RAF batched
1135
+ animateTo,
1136
+ // CSS transition with transitionend
1137
+ snapBack,
1138
+ // Animate back to 0
1139
+ viewportHeight
1140
+ // Cached, no layout thrashing
1141
+ } = useSwipeAnimation({ trackRef, transitionDuration, easing: SPRING_EASING2 });
1142
+ react.useEffect(() => {
1143
+ videosRef.current = videos;
1144
+ }, [videos]);
1145
+ react.useEffect(() => {
1146
+ currentIndexRef.current = currentIndex;
1147
+ }, [currentIndex]);
1148
+ react.useEffect(() => {
1149
+ isLoadingRef.current = isLoading;
1150
+ }, [isLoading]);
1151
+ react.useEffect(() => {
1152
+ hasMoreRef.current = hasMore;
1153
+ }, [hasMore]);
1154
+ react.useEffect(() => {
1155
+ isLockedRef.current = isLocked;
1156
+ }, [isLocked]);
1157
+ react.useEffect(() => {
1158
+ if (videos.length === 0) {
1159
+ setCurrentIndex(0);
1160
+ return;
1161
+ }
1162
+ if (currentIndex >= videos.length) {
1163
+ const newIndex = videos.length - 1;
1164
+ const video = videos[newIndex];
1165
+ setCurrentIndex(newIndex);
1166
+ storeSetCurrentIndex(newIndex);
1167
+ if (video) {
1168
+ onVideoChange?.(video, newIndex);
1169
+ }
1170
+ }
1171
+ }, [videos.length, currentIndex, storeSetCurrentIndex, onVideoChange, videos]);
1172
+ const getVideoPriority = react.useCallback((index) => {
1173
+ return getPreloadPriority(index, currentIndex);
1174
+ }, [currentIndex]);
1175
+ const checkLoadMore = react.useCallback((index) => {
1176
+ const vids = videosRef.current;
1177
+ if (hasMoreRef.current && !isLoadingRef.current && vids.length - index <= loadMoreThreshold) {
1178
+ onLoadMore?.();
1179
+ }
1180
+ }, [loadMoreThreshold, onLoadMore]);
1181
+ const slideTo = react.useCallback(async (index, animated = true) => {
1182
+ if (isLockedRef.current) return;
1183
+ const vids = videosRef.current;
1184
+ const clampedIndex = Math.max(0, Math.min(index, vids.length - 1));
1185
+ const currentIdx = currentIndexRef.current;
1186
+ if (clampedIndex === currentIdx) {
1187
+ setTranslateY(0);
1188
+ return;
1189
+ }
1190
+ const direction = clampedIndex > currentIdx ? -1 : 1;
1191
+ if (animated) {
1192
+ setIsLocked(true);
1193
+ setIsAnimating(true);
1194
+ await animateTo(direction * viewportHeight);
1195
+ setCurrentIndex(clampedIndex);
1196
+ storeSetCurrentIndex(clampedIndex);
1197
+ setTranslateY(0);
1198
+ const video = vids[clampedIndex];
1199
+ if (video) {
1200
+ onVideoChange?.(video, clampedIndex);
1201
+ }
1202
+ checkLoadMore(clampedIndex);
1203
+ setIsAnimating(false);
1204
+ setIsLocked(false);
1205
+ } else {
1206
+ setCurrentIndex(clampedIndex);
1207
+ storeSetCurrentIndex(clampedIndex);
1208
+ setTranslateY(0);
1209
+ const video = vids[clampedIndex];
1210
+ if (video) {
1211
+ onVideoChange?.(video, clampedIndex);
1212
+ checkLoadMore(clampedIndex);
1213
+ }
1214
+ }
1215
+ }, [viewportHeight, animateTo, setTranslateY, storeSetCurrentIndex, onVideoChange, checkLoadMore]);
1216
+ const slideNext = react.useCallback((animated = true) => {
1217
+ const vids = videosRef.current;
1218
+ const idx = currentIndexRef.current;
1219
+ if (idx < vids.length - 1) {
1220
+ slideTo(idx + 1, animated);
1221
+ }
1222
+ }, [slideTo]);
1223
+ const slidePrev = react.useCallback((animated = true) => {
1224
+ const idx = currentIndexRef.current;
1225
+ if (idx > 0) {
1226
+ slideTo(idx - 1, animated);
1227
+ }
1228
+ }, [slideTo]);
1229
+ const handleSwipeProgress = react.useCallback(
1230
+ (_progress, direction, movement) => {
1231
+ const vids = videosRef.current;
1232
+ const idx = currentIndexRef.current;
1233
+ const canGoUp = idx > 0;
1234
+ const canGoDown = idx < vids.length - 1;
1235
+ let delta = movement;
1236
+ const atBoundary = direction === "down" && !canGoUp || direction === "up" && !canGoDown;
1237
+ if (atBoundary) {
1238
+ delta *= 0.3;
1239
+ }
1240
+ setTranslateY(delta);
1241
+ },
1242
+ [setTranslateY]
1243
+ );
1244
+ const handleSwipeUp = react.useCallback(async () => {
1245
+ const vids = videosRef.current;
1246
+ const idx = currentIndexRef.current;
1247
+ const canGoDown = idx < vids.length - 1;
1248
+ if (!canGoDown) {
1249
+ await snapBack();
1250
+ return;
1251
+ }
1252
+ setIsLocked(true);
1253
+ setIsAnimating(true);
1254
+ await animateTo(-viewportHeight);
1255
+ const newIndex = currentIndexRef.current + 1;
1256
+ const newVids = videosRef.current;
1257
+ if (newIndex < newVids.length) {
1258
+ setCurrentIndex(newIndex);
1259
+ storeSetCurrentIndex(newIndex);
1260
+ const video = newVids[newIndex];
1261
+ if (video) {
1262
+ onVideoChange?.(video, newIndex);
1263
+ }
1264
+ checkLoadMore(newIndex);
1265
+ }
1266
+ setTranslateY(0);
1267
+ setIsAnimating(false);
1268
+ setIsLocked(false);
1269
+ }, [viewportHeight, animateTo, snapBack, setTranslateY, storeSetCurrentIndex, onVideoChange, checkLoadMore]);
1270
+ const handleSwipeDown = react.useCallback(async () => {
1271
+ const idx = currentIndexRef.current;
1272
+ const canGoUp = idx > 0;
1273
+ if (!canGoUp) {
1274
+ await snapBack();
1275
+ return;
1276
+ }
1277
+ setIsLocked(true);
1278
+ setIsAnimating(true);
1279
+ await animateTo(viewportHeight);
1280
+ const newIndex = currentIndexRef.current - 1;
1281
+ if (newIndex >= 0) {
1282
+ setCurrentIndex(newIndex);
1283
+ storeSetCurrentIndex(newIndex);
1284
+ const video = videosRef.current[newIndex];
1285
+ if (video) {
1286
+ onVideoChange?.(video, newIndex);
1287
+ }
1288
+ }
1289
+ setTranslateY(0);
1290
+ setIsAnimating(false);
1291
+ setIsLocked(false);
1292
+ }, [viewportHeight, animateTo, snapBack, setTranslateY, storeSetCurrentIndex, onVideoChange]);
1293
+ const handleSwipeCancel = react.useCallback(async () => {
1294
+ await snapBack();
1295
+ }, [snapBack]);
1296
+ const { bind: swipeBind } = gestures.useVerticalSwipe({
1297
+ onSwipeUp: handleSwipeUp,
1298
+ onSwipeDown: handleSwipeDown,
1299
+ onSwipeProgress: handleSwipeProgress,
1300
+ onSwipeCancel: handleSwipeCancel,
1301
+ threshold: swipeThreshold / viewportHeight,
1302
+ // Convert px to percentage
1303
+ velocityThreshold,
1304
+ hapticEnabled,
1305
+ disabled: gesturesDisabled || isLocked,
1306
+ enableProgressState: false
1307
+ // Disable for maximum performance
1308
+ });
1309
+ react.useImperativeHandle(ref, () => ({
1310
+ slideTo,
1311
+ slideNext,
1312
+ slidePrev,
1313
+ get activeIndex() {
1314
+ return currentIndexRef.current;
1315
+ },
1316
+ get totalSlides() {
1317
+ return videosRef.current.length;
1318
+ },
1319
+ get isBeginning() {
1320
+ return currentIndexRef.current === 0;
1321
+ },
1322
+ get isEnd() {
1323
+ return currentIndexRef.current === videosRef.current.length - 1;
1324
+ }
1325
+ }));
1326
+ react.useEffect(() => {
1327
+ const video = videos[currentIndex];
1328
+ if (video) {
1329
+ onVideoChange?.(video, currentIndex);
1330
+ }
1331
+ }, []);
1332
+ const slides = [];
1333
+ if (currentIndex > 0) {
1334
+ slides.push({ index: currentIndex - 1, position: -1 });
1335
+ }
1336
+ slides.push({ index: currentIndex, position: 0 });
1337
+ if (currentIndex < videos.length - 1) {
1338
+ slides.push({ index: currentIndex + 1, position: 1 });
1339
+ }
1340
+ if (videos.length === 0) {
1341
+ return /* @__PURE__ */ jsxRuntime.jsx(
1342
+ "div",
1343
+ {
1344
+ ref: containerRef,
1345
+ style: core.mergeStyles(feedStyles.container, style),
1346
+ className,
1347
+ "data-xhub-reel-feed": true,
1348
+ children: isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...feedStyles.loadingIndicator, top: "50%", bottom: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: feedStyles.spinner }) })
1349
+ }
1350
+ );
1351
+ }
1352
+ const trackStyle = {
1353
+ ...feedStyles.track
1354
+ // transform and transition are now managed via trackRef.current.style
1355
+ };
1356
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1357
+ "div",
1358
+ {
1359
+ ref: containerRef,
1360
+ ...swipeBind(),
1361
+ style: core.mergeStyles(feedStyles.container, style),
1362
+ className,
1363
+ "data-xhub-reel-feed": true,
1364
+ children: [
1365
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1366
+ @keyframes xhub-reel-spin {
1367
+ to { transform: rotate(360deg); }
1368
+ }
1369
+ ` }),
1370
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: trackRef, style: trackStyle, children: slides.map(({ index, position }) => {
1371
+ const video = videos[index];
1372
+ if (!video) return null;
1373
+ const priority = getVideoPriority(index);
1374
+ const isActive = index === currentIndex && !isAnimating;
1375
+ return /* @__PURE__ */ jsxRuntime.jsx(
1376
+ "div",
1377
+ {
1378
+ "data-index": index,
1379
+ style: {
1380
+ ...feedStyles.slide,
1381
+ top: position * viewportHeight
1382
+ },
1383
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1384
+ VideoFeedItem,
1385
+ {
1386
+ video,
1387
+ isActive,
1388
+ priority,
1389
+ onLike: () => onLike?.(video),
1390
+ onComment: () => onComment?.(video),
1391
+ onShare: () => onShare?.(video),
1392
+ onAuthorClick: () => onAuthorClick?.(video)
1393
+ }
1394
+ )
1395
+ },
1396
+ video.id
1397
+ );
1398
+ }) }),
1399
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: feedStyles.loadingIndicator, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: feedStyles.spinner }) })
1400
+ ]
1401
+ }
1402
+ );
1403
+ }
1404
+ );
1405
+ VideoFeed.displayName = "VideoFeed";
1406
+ function useVideoFeed(options = {}) {
1407
+ const {
1408
+ config,
1409
+ enabled = true,
1410
+ initialVideos = [],
1411
+ staleTime = 1e3 * 60 * 5,
1412
+ // 5 minutes
1413
+ refetchOnWindowFocus = false,
1414
+ onSuccess,
1415
+ onError,
1416
+ userId,
1417
+ tag,
1418
+ searchQuery,
1419
+ limit = 10,
1420
+ cursor
1421
+ } = options;
1422
+ void reactQuery.useQueryClient();
1423
+ const apiClientRef = react.useRef(null);
1424
+ const configKey = react.useMemo(() => {
1425
+ if (!config) return null;
1426
+ return JSON.stringify({
1427
+ baseUrl: config.baseUrl,
1428
+ apiKey: config.apiKey,
1429
+ auth: config.auth?.accessToken,
1430
+ endpoints: config.endpoints
1431
+ });
1432
+ }, [config]);
1433
+ react.useEffect(() => {
1434
+ if (config && configKey) {
1435
+ apiClientRef.current = api.createXHubReelApiClient(config);
1436
+ } else {
1437
+ apiClientRef.current = null;
1438
+ }
1439
+ }, [configKey]);
1440
+ const fetchParams = react.useMemo(
1441
+ () => ({
1442
+ userId,
1443
+ tag,
1444
+ searchQuery,
1445
+ limit,
1446
+ cursor
1447
+ }),
1448
+ [userId, tag, searchQuery, limit, cursor]
1449
+ );
1450
+ const queryKey = react.useMemo(
1451
+ () => api.queryKeys.videos.list(fetchParams),
1452
+ [fetchParams]
1453
+ );
1454
+ const fetchVideos = react.useCallback(
1455
+ async ({ pageParam }) => {
1456
+ if (!apiClientRef.current) {
1457
+ throw new Error("[XHubReel] API client not initialized. Provide config to useVideoFeed.");
1458
+ }
1459
+ return apiClientRef.current.fetchVideos({
1460
+ ...fetchParams,
1461
+ cursor: pageParam
1462
+ });
1463
+ },
1464
+ [fetchParams]
1465
+ );
1466
+ const {
1467
+ data,
1468
+ isLoading,
1469
+ isFetchingNextPage,
1470
+ hasNextPage,
1471
+ fetchNextPage: tanstackFetchNextPage,
1472
+ refetch: tanstackRefetch,
1473
+ error
1474
+ } = reactQuery.useInfiniteQuery({
1475
+ queryKey,
1476
+ queryFn: fetchVideos,
1477
+ getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextCursor : void 0,
1478
+ initialPageParam: void 0,
1479
+ enabled: enabled && !!config,
1480
+ staleTime,
1481
+ refetchOnWindowFocus
1482
+ });
1483
+ react.useEffect(() => {
1484
+ if (data && onSuccess) {
1485
+ const allVideos = data.pages.flatMap((page) => page.videos);
1486
+ onSuccess(allVideos);
1487
+ }
1488
+ }, [data, onSuccess]);
1489
+ react.useEffect(() => {
1490
+ if (error && onError) {
1491
+ onError(error);
1492
+ }
1493
+ }, [error, onError]);
1494
+ const videos = react.useMemo(() => {
1495
+ if (!data) {
1496
+ return initialVideos;
1497
+ }
1498
+ return data.pages.flatMap((page) => page.videos);
1499
+ }, [data, initialVideos]);
1500
+ const totalCount = react.useMemo(() => {
1501
+ if (!data || data.pages.length === 0) return void 0;
1502
+ const firstPage = data.pages[0];
1503
+ return firstPage?.total;
1504
+ }, [data]);
1505
+ const fetchNextPage = react.useCallback(async () => {
1506
+ if (!hasNextPage || isFetchingNextPage) return;
1507
+ try {
1508
+ await tanstackFetchNextPage();
1509
+ } catch (err) {
1510
+ console.error("[XHubReel] Error fetching next page:", err);
1511
+ onError?.(err);
1512
+ }
1513
+ }, [hasNextPage, isFetchingNextPage, tanstackFetchNextPage, onError]);
1514
+ const refetch = react.useCallback(async () => {
1515
+ try {
1516
+ await tanstackRefetch();
1517
+ } catch (err) {
1518
+ console.error("[XHubReel] Error refetching:", err);
1519
+ onError?.(err);
1520
+ }
1521
+ }, [tanstackRefetch, onError]);
1522
+ return {
1523
+ videos,
1524
+ isLoading: isLoading && !!config,
1525
+ isFetchingMore: isFetchingNextPage,
1526
+ hasMore: hasNextPage ?? false,
1527
+ fetchNextPage,
1528
+ refetch,
1529
+ error: error ?? null,
1530
+ isApiMode: !!config,
1531
+ totalCount
1532
+ };
1533
+ }
1534
+ async function prefetchVideoFeed(queryClient, config, params = {}) {
1535
+ const client = api.createXHubReelApiClient(config);
1536
+ await queryClient.prefetchInfiniteQuery({
1537
+ queryKey: api.queryKeys.videos.list(params),
1538
+ queryFn: ({ pageParam }) => client.fetchVideos({ ...params, cursor: pageParam }),
1539
+ initialPageParam: void 0
1540
+ });
1541
+ }
1542
+ var stateStyles = {
1543
+ container: {
1544
+ position: "fixed",
1545
+ inset: 0,
1546
+ display: "flex",
1547
+ flexDirection: "column",
1548
+ alignItems: "center",
1549
+ justifyContent: "center",
1550
+ backgroundColor: core.colors.background,
1551
+ color: core.colors.text,
1552
+ gap: core.spacing[4]
1553
+ },
1554
+ spinner: {
1555
+ width: 40,
1556
+ height: 40,
1557
+ borderWidth: 3,
1558
+ borderStyle: "solid",
1559
+ borderColor: "rgba(255, 255, 255, 0.2)",
1560
+ borderTopColor: core.colors.accent,
1561
+ borderRadius: core.radii.full,
1562
+ animation: "xhub-reel-spin 1s linear infinite"
1563
+ },
1564
+ text: {
1565
+ fontSize: core.fontSizes.md,
1566
+ color: core.colors.textSecondary,
1567
+ textAlign: "center",
1568
+ maxWidth: 280,
1569
+ lineHeight: 1.5
1570
+ },
1571
+ button: {
1572
+ padding: `${core.spacing[3]}px ${core.spacing[6]}px`,
1573
+ backgroundColor: core.colors.accent,
1574
+ color: core.colors.text,
1575
+ border: "none",
1576
+ borderRadius: core.radii.md,
1577
+ fontSize: core.fontSizes.sm,
1578
+ fontWeight: core.fontWeights.semibold,
1579
+ cursor: "pointer"
1580
+ },
1581
+ errorIcon: {
1582
+ fontSize: 48,
1583
+ marginBottom: core.spacing[2]
1584
+ }
1585
+ };
1586
+ var DefaultLoading = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: stateStyles.container, children: [
1587
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1588
+ @keyframes xhub-reel-spin {
1589
+ to { transform: rotate(360deg); }
1590
+ }
1591
+ ` }),
1592
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: stateStyles.spinner }),
1593
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: stateStyles.text, children: "\u0110ang t\u1EA3i video..." })
1594
+ ] });
1595
+ var DefaultError = ({ error, retry }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: stateStyles.container, children: [
1596
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: stateStyles.errorIcon, children: "\u{1F615}" }),
1597
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: stateStyles.text, children: error.message || "C\xF3 l\u1ED7i x\u1EA3y ra khi t\u1EA3i video" }),
1598
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: stateStyles.button, onClick: retry, children: "Th\u1EED l\u1EA1i" })
1599
+ ] });
1600
+ var DefaultEmpty = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: stateStyles.container, children: [
1601
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: stateStyles.errorIcon, children: "\u{1F4ED}" }),
1602
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: stateStyles.text, children: "Kh\xF4ng c\xF3 video n\xE0o \u0111\u1EC3 hi\u1EC3n th\u1ECB" })
1603
+ ] });
1604
+ var ConnectedVideoFeed = react.forwardRef(
1605
+ ({
1606
+ config: configProp,
1607
+ userId,
1608
+ tag,
1609
+ searchQuery,
1610
+ pageSize = 10,
1611
+ initialVideos,
1612
+ onFetchSuccess,
1613
+ onFetchError,
1614
+ renderLoading = () => /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {}),
1615
+ renderError = (error, retry) => /* @__PURE__ */ jsxRuntime.jsx(DefaultError, { error, retry }),
1616
+ renderEmpty = () => /* @__PURE__ */ jsxRuntime.jsx(DefaultEmpty, {}),
1617
+ // Pass through VideoFeed props
1618
+ onVideoChange,
1619
+ onLike,
1620
+ onComment,
1621
+ onShare,
1622
+ onAuthorClick,
1623
+ ...videoFeedProps
1624
+ }, ref) => {
1625
+ const { config: contextConfig } = api.useXHubReelConfig();
1626
+ const config = configProp || contextConfig;
1627
+ const {
1628
+ videos,
1629
+ isLoading,
1630
+ isFetchingMore,
1631
+ hasMore,
1632
+ fetchNextPage,
1633
+ refetch,
1634
+ error
1635
+ } = useVideoFeed({
1636
+ config: config || void 0,
1637
+ userId,
1638
+ tag,
1639
+ searchQuery,
1640
+ limit: pageSize,
1641
+ initialVideos,
1642
+ onSuccess: onFetchSuccess,
1643
+ onError: onFetchError
1644
+ });
1645
+ const handleLoadMore = react.useCallback(async () => {
1646
+ await fetchNextPage();
1647
+ }, [fetchNextPage]);
1648
+ const handleRetry = react.useCallback(() => {
1649
+ refetch();
1650
+ }, [refetch]);
1651
+ if (!config) {
1652
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: stateStyles.container, children: [
1653
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: stateStyles.errorIcon, children: "\u26A0\uFE0F" }),
1654
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: stateStyles.text, children: "Ch\u01B0a c\u1EA5u h\xECnh API. Vui l\xF2ng wrap component trong XHubReelProvider v\u1EDBi config ho\u1EB7c truy\u1EC1n config prop." })
1655
+ ] });
1656
+ }
1657
+ if (isLoading && videos.length === 0) {
1658
+ return renderLoading();
1659
+ }
1660
+ if (error && videos.length === 0) {
1661
+ return renderError(error, handleRetry);
1662
+ }
1663
+ if (!isLoading && videos.length === 0) {
1664
+ return renderEmpty();
1665
+ }
1666
+ return /* @__PURE__ */ jsxRuntime.jsx(
1667
+ VideoFeed,
1668
+ {
1669
+ ref,
1670
+ videos,
1671
+ isLoading: isFetchingMore,
1672
+ hasMore,
1673
+ onLoadMore: handleLoadMore,
1674
+ onVideoChange,
1675
+ onLike,
1676
+ onComment,
1677
+ onShare,
1678
+ onAuthorClick,
1679
+ ...videoFeedProps
1680
+ }
1681
+ );
1682
+ }
1683
+ );
1684
+ ConnectedVideoFeed.displayName = "ConnectedVideoFeed";
1685
+ function useFeedScroll({
1686
+ scrollRef,
1687
+ itemCount,
1688
+ itemHeight,
1689
+ onScrollChange,
1690
+ onIndexChange
1691
+ }) {
1692
+ const [currentIndex, setCurrentIndex] = react.useState(0);
1693
+ const [scrollVelocity, setScrollVelocity] = react.useState(0);
1694
+ const [isScrolling, setIsScrolling] = react.useState(false);
1695
+ const lastScrollTopRef = react.useRef(0);
1696
+ const lastScrollTimeRef = react.useRef(Date.now());
1697
+ const scrollTimeoutRef = react.useRef(null);
1698
+ const getItemHeight = react.useCallback(() => {
1699
+ if (itemHeight) return itemHeight;
1700
+ if (typeof window !== "undefined") return window.innerHeight;
1701
+ return 800;
1702
+ }, [itemHeight]);
1703
+ const scrollToIndex = react.useCallback(
1704
+ (index, smooth = true) => {
1705
+ const element = scrollRef.current;
1706
+ if (!element) return;
1707
+ const clampedIndex = Math.max(0, Math.min(index, itemCount - 1));
1708
+ const top = clampedIndex * getItemHeight();
1709
+ element.scrollTo({
1710
+ top,
1711
+ behavior: smooth ? "smooth" : "auto"
1712
+ });
1713
+ },
1714
+ [scrollRef, itemCount, getItemHeight]
1715
+ );
1716
+ const scrollToNext = react.useCallback(() => {
1717
+ scrollToIndex(currentIndex + 1);
1718
+ }, [currentIndex, scrollToIndex]);
1719
+ const scrollToPrev = react.useCallback(() => {
1720
+ scrollToIndex(currentIndex - 1);
1721
+ }, [currentIndex, scrollToIndex]);
1722
+ react.useEffect(() => {
1723
+ const element = scrollRef.current;
1724
+ if (!element) return;
1725
+ const handleScroll = () => {
1726
+ const now = Date.now();
1727
+ const scrollTop = element.scrollTop;
1728
+ const timeDelta = now - lastScrollTimeRef.current;
1729
+ const scrollDelta = Math.abs(scrollTop - lastScrollTopRef.current);
1730
+ if (timeDelta > 0) {
1731
+ const velocity = scrollDelta / timeDelta * 1e3;
1732
+ setScrollVelocity(velocity);
1733
+ onScrollChange?.(scrollTop, velocity);
1734
+ }
1735
+ const height = getItemHeight();
1736
+ const newIndex = Math.round(scrollTop / height);
1737
+ if (newIndex !== currentIndex && newIndex >= 0 && newIndex < itemCount) {
1738
+ setCurrentIndex(newIndex);
1739
+ onIndexChange?.(newIndex);
1740
+ }
1741
+ setIsScrolling(true);
1742
+ if (scrollTimeoutRef.current) {
1743
+ clearTimeout(scrollTimeoutRef.current);
1744
+ }
1745
+ scrollTimeoutRef.current = setTimeout(() => {
1746
+ setIsScrolling(false);
1747
+ setScrollVelocity(0);
1748
+ }, 150);
1749
+ lastScrollTopRef.current = scrollTop;
1750
+ lastScrollTimeRef.current = now;
1751
+ };
1752
+ element.addEventListener("scroll", handleScroll, { passive: true });
1753
+ return () => {
1754
+ element.removeEventListener("scroll", handleScroll);
1755
+ if (scrollTimeoutRef.current) {
1756
+ clearTimeout(scrollTimeoutRef.current);
1757
+ }
1758
+ };
1759
+ }, [scrollRef, itemCount, currentIndex, getItemHeight, onScrollChange, onIndexChange]);
1760
+ return {
1761
+ currentIndex,
1762
+ scrollVelocity,
1763
+ isScrolling,
1764
+ scrollToIndex,
1765
+ scrollToNext,
1766
+ scrollToPrev
1767
+ };
1768
+ }
1769
+ function useInfiniteScroll({
1770
+ onLoadMore,
1771
+ isLoading = false,
1772
+ hasMore = true,
1773
+ threshold: _threshold = 3,
1774
+ rootMargin = "0px 0px 200px 0px"
1775
+ }) {
1776
+ const [isLoadingMore, setIsLoadingMore] = react.useState(false);
1777
+ const observerRef = react.useRef(null);
1778
+ const sentinelRef = react.useRef(null);
1779
+ const loadingRef = react.useRef(false);
1780
+ const handleLoadMore = react.useCallback(async () => {
1781
+ if (loadingRef.current || isLoading || !hasMore || !onLoadMore) return;
1782
+ loadingRef.current = true;
1783
+ setIsLoadingMore(true);
1784
+ try {
1785
+ await onLoadMore();
1786
+ } finally {
1787
+ loadingRef.current = false;
1788
+ setIsLoadingMore(false);
1789
+ }
1790
+ }, [onLoadMore, isLoading, hasMore]);
1791
+ const setSentinelRef = react.useCallback(
1792
+ (element) => {
1793
+ if (observerRef.current) {
1794
+ observerRef.current.disconnect();
1795
+ }
1796
+ sentinelRef.current = element;
1797
+ if (!element || !hasMore) return;
1798
+ observerRef.current = new IntersectionObserver(
1799
+ (entries) => {
1800
+ const [entry] = entries;
1801
+ if (entry?.isIntersecting) {
1802
+ handleLoadMore();
1803
+ }
1804
+ },
1805
+ {
1806
+ rootMargin,
1807
+ threshold: 0
1808
+ }
1809
+ );
1810
+ observerRef.current.observe(element);
1811
+ },
1812
+ [hasMore, rootMargin, handleLoadMore]
1813
+ );
1814
+ react.useEffect(() => {
1815
+ return () => {
1816
+ if (observerRef.current) {
1817
+ observerRef.current.disconnect();
1818
+ }
1819
+ };
1820
+ }, []);
1821
+ react.useEffect(() => {
1822
+ loadingRef.current = isLoading;
1823
+ }, [isLoading]);
1824
+ return {
1825
+ sentinelRef: setSentinelRef,
1826
+ isLoadingMore
1827
+ };
1828
+ }
1829
+
1830
+ Object.defineProperty(exports, "usePreload", {
1831
+ enumerable: true,
1832
+ get: function () { return player.usePreload; }
1833
+ });
1834
+ exports.ConnectedVideoFeed = ConnectedVideoFeed;
1835
+ exports.VideoFeed = VideoFeed;
1836
+ exports.VideoFeedItem = VideoFeedItem;
1837
+ exports.VideoFeedItemActions = VideoFeedItemActions;
1838
+ exports.VideoFeedItemOverlay = VideoFeedItemOverlay;
1839
+ exports.VideoFeedItemPlayer = VideoFeedItemPlayer;
1840
+ exports.VideoFeedItemTimeline = VideoFeedItemTimeline;
1841
+ exports.VideoOverlay = VideoOverlay;
1842
+ exports.getPreloadPriority = getPreloadPriority;
1843
+ exports.getPreloadPriorityForFeed = getPreloadPriorityForFeed;
1844
+ exports.mapPriorityToNumeric = mapPriorityToNumeric;
1845
+ exports.memoryManager = memoryManager;
1846
+ exports.prefetchVideoFeed = prefetchVideoFeed;
1847
+ exports.useFeedScroll = useFeedScroll;
1848
+ exports.useGlobalMemoryState = useGlobalMemoryState;
1849
+ exports.useInfiniteScroll = useInfiniteScroll;
1850
+ exports.useMemoryManager = useMemoryManager;
1851
+ exports.useSwipeAnimation = useSwipeAnimation;
1852
+ exports.useVideoActivation = useVideoActivation;
1853
+ exports.useVideoFeed = useVideoFeed;
1854
+ exports.useVideoFeedItemContext = useVideoFeedItemContext;
1855
+ exports.useVideoVisibility = useVideoVisibility;