@xhub-reels/sdk 0.2.11 → 0.2.13

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.cjs CHANGED
@@ -4,6 +4,7 @@ var vanilla = require('zustand/vanilla');
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var Hls = require('hls.js');
7
+ var reactDom = require('react-dom');
7
8
 
8
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
10
 
@@ -106,6 +107,7 @@ var PlayerEngine = class {
106
107
  this.circuitResetTimer = null;
107
108
  this.watchTimeInterval = null;
108
109
  this.lastStatus = "idle" /* IDLE */;
110
+ this._destroyed = false;
109
111
  this.config = { ...DEFAULT_PLAYER_CONFIG, ...config };
110
112
  this.analytics = analytics;
111
113
  this.logger = logger;
@@ -315,8 +317,13 @@ var PlayerEngine = class {
315
317
  this.lastStatus = "idle" /* IDLE */;
316
318
  }
317
319
  destroy() {
320
+ if (this._destroyed) return;
321
+ this._destroyed = true;
318
322
  this.stopWatchTime();
319
- if (this.circuitResetTimer) clearTimeout(this.circuitResetTimer);
323
+ if (this.circuitResetTimer) {
324
+ clearTimeout(this.circuitResetTimer);
325
+ this.circuitResetTimer = null;
326
+ }
320
327
  this.listeners.clear();
321
328
  }
322
329
  // ═══════════════════════════════════════════
@@ -419,6 +426,8 @@ var FeedManager = class {
419
426
  this.inFlightRequests = /* @__PURE__ */ new Map();
420
427
  /** LRU tracking: itemId → lastAccessTime */
421
428
  this.accessOrder = /* @__PURE__ */ new Map();
429
+ /** Idempotency guard for destroy() */
430
+ this._destroyed = false;
422
431
  /** Prefetch cache — instance-scoped (not static) */
423
432
  this.prefetchCache = null;
424
433
  this.config = { ...DEFAULT_FEED_CONFIG, ...config };
@@ -444,6 +453,13 @@ var FeedManager = class {
444
453
  // ═══════════════════════════════════════════
445
454
  // PUBLIC API — Prefetch
446
455
  // ═══════════════════════════════════════════
456
+ setInitialItems(items) {
457
+ this.prefetchCache = {
458
+ items,
459
+ nextCursor: null,
460
+ timestamp: Date.now()
461
+ };
462
+ }
447
463
  async prefetch(ttlMs) {
448
464
  if (this.prefetchCache) {
449
465
  const ttl = ttlMs ?? this.config.staleTTL;
@@ -524,7 +540,10 @@ var FeedManager = class {
524
540
  // PUBLIC API — Lifecycle
525
541
  // ═══════════════════════════════════════════
526
542
  destroy() {
543
+ if (this._destroyed) return;
544
+ this._destroyed = true;
527
545
  this.abortController?.abort();
546
+ this.abortController = null;
528
547
  this.inFlightRequests.clear();
529
548
  this.accessOrder.clear();
530
549
  this.prefetchCache = null;
@@ -593,7 +612,13 @@ var FeedManager = class {
593
612
  applyItems(incoming, _nextCursor, append) {
594
613
  const { itemsById, displayOrder } = this.store.getState();
595
614
  const nextById = new Map(itemsById);
615
+ const seenInBatch = /* @__PURE__ */ new Set();
616
+ const deduped = [];
596
617
  for (const item of incoming) {
618
+ if (!seenInBatch.has(item.id)) {
619
+ seenInBatch.add(item.id);
620
+ deduped.push(item);
621
+ }
597
622
  nextById.set(item.id, item);
598
623
  this.accessOrder.set(item.id, Date.now());
599
624
  }
@@ -601,14 +626,14 @@ var FeedManager = class {
601
626
  if (append) {
602
627
  const existingIds = new Set(displayOrder);
603
628
  const newIds = [];
604
- for (const item of incoming) {
629
+ for (const item of deduped) {
605
630
  if (!existingIds.has(item.id)) {
606
631
  newIds.push(item.id);
607
632
  }
608
633
  }
609
634
  nextOrder = [...displayOrder, ...newIds];
610
635
  } else {
611
- nextOrder = incoming.map((item) => item.id);
636
+ nextOrder = deduped.map((item) => item.id);
612
637
  }
613
638
  if (nextById.size > this.config.maxCacheSize) {
614
639
  this.evictLRU(nextById, nextOrder);
@@ -650,6 +675,8 @@ var OptimisticManager = class {
650
675
  this.likeDebounceTimers = /* @__PURE__ */ new Map();
651
676
  /** Pending like direction: contentId → final intended state */
652
677
  this.pendingLikeState = /* @__PURE__ */ new Map();
678
+ /** Idempotency guard for destroy() */
679
+ this._destroyed = false;
653
680
  this.interaction = interaction;
654
681
  this.logger = logger;
655
682
  this.store = vanilla.createStore(createInitialState3);
@@ -740,6 +767,8 @@ var OptimisticManager = class {
740
767
  // PUBLIC API — Lifecycle
741
768
  // ═══════════════════════════════════════════
742
769
  destroy() {
770
+ if (this._destroyed) return;
771
+ this._destroyed = true;
743
772
  for (const timer of this.likeDebounceTimers.values()) {
744
773
  clearTimeout(timer);
745
774
  }
@@ -761,6 +790,7 @@ var DEFAULT_RESOURCE_CONFIG = {
761
790
  var ResourceGovernor = class {
762
791
  constructor(config = {}, videoLoader, network, logger) {
763
792
  this.focusDebounceTimer = null;
793
+ this._destroyed = false;
764
794
  this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
765
795
  this.videoLoader = videoLoader;
766
796
  this.network = network;
@@ -791,11 +821,19 @@ var ResourceGovernor = class {
791
821
  this.logger?.debug("[ResourceGovernor] Activated");
792
822
  }
793
823
  deactivate() {
824
+ if (this._destroyed) return;
825
+ if (!this.store.getState().isActive) return;
794
826
  this.networkUnsubscribe?.();
795
- if (this.focusDebounceTimer) clearTimeout(this.focusDebounceTimer);
827
+ this.networkUnsubscribe = void 0;
828
+ if (this.focusDebounceTimer) {
829
+ clearTimeout(this.focusDebounceTimer);
830
+ this.focusDebounceTimer = null;
831
+ }
796
832
  this.store.setState({ isActive: false });
797
833
  }
798
834
  destroy() {
835
+ if (this._destroyed) return;
836
+ this._destroyed = true;
799
837
  this.deactivate();
800
838
  this.videoLoader?.clearAll();
801
839
  }
@@ -919,6 +957,153 @@ var ResourceGovernor = class {
919
957
  );
920
958
  }
921
959
  };
960
+ var DEFAULT_NAVIGATION_CONFIG = {
961
+ phaseTimeoutMs: 1200
962
+ };
963
+ var VALID_NAV_TRANSITIONS = {
964
+ closed: ["opening"],
965
+ // Allow opening → closing so a fast user can dismiss mid-open.
966
+ opening: ["open", "closing", "closed"],
967
+ open: ["closing"],
968
+ // Allow closing → opening so re-opening during the exit animation works.
969
+ closing: ["closed", "opening"]
970
+ };
971
+ function isValidNavTransition(from, to) {
972
+ if (from === to) return false;
973
+ return VALID_NAV_TRANSITIONS[from].includes(to);
974
+ }
975
+ var NavigationManager = class {
976
+ constructor(config = {}, logger) {
977
+ /** Fallback timer guarding against a missing animationend report. */
978
+ this.phaseTimer = null;
979
+ this._destroyed = false;
980
+ this.config = { ...DEFAULT_NAVIGATION_CONFIG, ...config };
981
+ this.logger = logger;
982
+ this.store = vanilla.createStore(() => ({
983
+ phase: "closed",
984
+ openIndex: null
985
+ }));
986
+ }
987
+ // ═══════════════════════════════════════════
988
+ // PUBLIC API — Intent
989
+ // ═══════════════════════════════════════════
990
+ /**
991
+ * Open the reels viewer at `index`. Safe to call from a thumbnail click
992
+ * handler — it transitions into `opening`, at which point ReelsModal mounts
993
+ * <ReelsFeed> hidden and begins prewarming.
994
+ *
995
+ * Calling open() while already open/opening just retargets the index
996
+ * (e.g. user taps a different thumbnail before the animation settles).
997
+ */
998
+ open(index) {
999
+ if (this._destroyed) return;
1000
+ const { phase, openIndex } = this.store.getState();
1001
+ if ((phase === "open" || phase === "opening") && index === openIndex) {
1002
+ return;
1003
+ }
1004
+ if (phase === "open" || phase === "opening") {
1005
+ this.store.setState({ openIndex: index });
1006
+ this.logger?.debug(`[NavigationManager] retarget openIndex=${index}`);
1007
+ return;
1008
+ }
1009
+ if (!this.transition("opening")) return;
1010
+ this.store.setState({ openIndex: index });
1011
+ this.logger?.debug(`[NavigationManager] open(${index}) \u2192 opening`);
1012
+ }
1013
+ /**
1014
+ * Begin closing the viewer. ReelsModal animates out then calls
1015
+ * `setPhase('closed')`. Idempotent if already closing/closed.
1016
+ */
1017
+ close() {
1018
+ if (this._destroyed) return;
1019
+ const { phase } = this.store.getState();
1020
+ if (phase === "closed" || phase === "closing") return;
1021
+ this.transition("closing");
1022
+ this.logger?.debug("[NavigationManager] close() \u2192 closing");
1023
+ }
1024
+ // ═══════════════════════════════════════════
1025
+ // PUBLIC API — Phase reporting (called by ReelsModal)
1026
+ // ═══════════════════════════════════════════
1027
+ /**
1028
+ * Report an animation-driven phase change from the component layer.
1029
+ * Invalid transitions are dropped. When reaching `closed`, openIndex is
1030
+ * cleared so <ReelsFeed> unmounts.
1031
+ */
1032
+ setPhase(next) {
1033
+ if (this._destroyed) return;
1034
+ this.transition(next);
1035
+ }
1036
+ // ═══════════════════════════════════════════
1037
+ // PUBLIC API — Queries
1038
+ // ═══════════════════════════════════════════
1039
+ getPhase() {
1040
+ return this.store.getState().phase;
1041
+ }
1042
+ getOpenIndex() {
1043
+ return this.store.getState().openIndex;
1044
+ }
1045
+ isOpen() {
1046
+ const { phase } = this.store.getState();
1047
+ return phase === "open" || phase === "opening";
1048
+ }
1049
+ /** Whether <ReelsFeed> should be mounted (any non-closed phase). */
1050
+ shouldMount() {
1051
+ return this.store.getState().phase !== "closed";
1052
+ }
1053
+ // ═══════════════════════════════════════════
1054
+ // PUBLIC API — Lifecycle
1055
+ // ═══════════════════════════════════════════
1056
+ destroy() {
1057
+ if (this._destroyed) return;
1058
+ this._destroyed = true;
1059
+ if (this.phaseTimer) {
1060
+ clearTimeout(this.phaseTimer);
1061
+ this.phaseTimer = null;
1062
+ }
1063
+ }
1064
+ // ═══════════════════════════════════════════
1065
+ // PRIVATE
1066
+ // ═══════════════════════════════════════════
1067
+ /**
1068
+ * Apply a guarded phase transition. Returns true if it was applied.
1069
+ * Manages the fallback timer for transient phases (opening/closing).
1070
+ */
1071
+ transition(next) {
1072
+ const { phase } = this.store.getState();
1073
+ if (!isValidNavTransition(phase, next)) {
1074
+ this.logger?.debug(
1075
+ `[NavigationManager] dropped invalid transition ${phase} \u2192 ${next}`
1076
+ );
1077
+ return false;
1078
+ }
1079
+ this.clearPhaseTimer();
1080
+ if (next === "closed") {
1081
+ this.store.setState({ phase: "closed", openIndex: null });
1082
+ return true;
1083
+ }
1084
+ this.store.setState({ phase: next });
1085
+ if (next === "opening" || next === "closing") {
1086
+ const fallbackTarget = next === "opening" ? "open" : "closed";
1087
+ this.phaseTimer = setTimeout(() => {
1088
+ this.phaseTimer = null;
1089
+ const current = this.store.getState().phase;
1090
+ if (current === next) {
1091
+ this.logger?.warn(
1092
+ `[NavigationManager] phase fallback ${next} \u2192 ${fallbackTarget}`
1093
+ );
1094
+ this.transition(fallbackTarget);
1095
+ }
1096
+ }, this.config.phaseTimeoutMs);
1097
+ }
1098
+ return true;
1099
+ }
1100
+ clearPhaseTimer() {
1101
+ if (this.phaseTimer) {
1102
+ clearTimeout(this.phaseTimer);
1103
+ this.phaseTimer = null;
1104
+ }
1105
+ }
1106
+ };
922
1107
  function usePointerGesture(config = {}) {
923
1108
  const {
924
1109
  axis = "y",
@@ -1108,7 +1293,9 @@ function useSnapAnimation(config = {}) {
1108
1293
  }
1109
1294
  );
1110
1295
  anim.addEventListener("finish", () => {
1111
- element.style.transform = `translateY(${toY}px)`;
1296
+ if (element.isConnected) {
1297
+ element.style.transform = `translateY(${toY}px)`;
1298
+ }
1112
1299
  anim.cancel();
1113
1300
  });
1114
1301
  animations.push(anim);
@@ -1136,31 +1323,100 @@ function useSnapAnimation(config = {}) {
1136
1323
  }, [cancelAnimation]);
1137
1324
  return { animateSnap, animateBounceBack, cancelAnimation };
1138
1325
  }
1326
+ var noop = () => {
1327
+ };
1328
+ var noopAsync = () => Promise.resolve();
1329
+ var DEFAULT_LOGGER = {
1330
+ debug: noop,
1331
+ info: noop,
1332
+ warn: noop,
1333
+ error: noop
1334
+ };
1335
+ var DEFAULT_ANALYTICS = {
1336
+ trackView: noop,
1337
+ trackLike: noop,
1338
+ trackShare: noop,
1339
+ trackComment: noop,
1340
+ trackError: noop,
1341
+ trackPlaybackEvent: noop
1342
+ };
1343
+ var DEFAULT_INTERACTION = {
1344
+ like: noopAsync,
1345
+ unlike: noopAsync,
1346
+ follow: noopAsync,
1347
+ unfollow: noopAsync,
1348
+ bookmark: noopAsync,
1349
+ unbookmark: noopAsync,
1350
+ share: noopAsync
1351
+ };
1352
+ var DEFAULT_STORAGE = {
1353
+ get: () => null,
1354
+ set: noop,
1355
+ remove: noop,
1356
+ clear: noop
1357
+ };
1358
+ var DEFAULT_NETWORK = {
1359
+ getNetworkType: () => "unknown",
1360
+ isOnline: () => true,
1361
+ onNetworkChange: () => noop
1362
+ };
1363
+ var DEFAULT_VIDEO_LOADER = {
1364
+ preload: (videoId) => Promise.resolve({ videoId, status: "idle" }),
1365
+ cancel: noop,
1366
+ isPreloaded: () => false,
1367
+ getPreloadStatus: () => "idle",
1368
+ clearAll: noop
1369
+ };
1370
+ var DEFAULT_COMMENT = {
1371
+ fetchComments: () => Promise.resolve({ items: [], nextCursor: null, hasMore: false, total: 0 }),
1372
+ postComment: () => Promise.reject(new Error("Comment adapter not configured")),
1373
+ deleteComment: noopAsync,
1374
+ likeComment: noopAsync,
1375
+ unlikeComment: noopAsync
1376
+ };
1139
1377
  var SDKContext = react.createContext(null);
1140
- function ReelsProvider({ children, adapters, debug = false }) {
1141
- const logger = adapters.logger;
1142
- const sdkRef = react.useRef(null);
1378
+ function ReelsProvider({
1379
+ children,
1380
+ adapters,
1381
+ initialItems,
1382
+ debug = false,
1383
+ navigationConfig
1384
+ }) {
1385
+ const resolvedAdapters = react.useMemo(() => ({
1386
+ dataSource: adapters.dataSource,
1387
+ interaction: adapters.interaction ?? DEFAULT_INTERACTION,
1388
+ storage: adapters.storage ?? DEFAULT_STORAGE,
1389
+ analytics: adapters.analytics ?? DEFAULT_ANALYTICS,
1390
+ logger: adapters.logger ?? DEFAULT_LOGGER,
1391
+ network: adapters.network ?? DEFAULT_NETWORK,
1392
+ videoLoader: adapters.videoLoader ?? DEFAULT_VIDEO_LOADER,
1393
+ comment: adapters.comment ?? DEFAULT_COMMENT
1394
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1395
+ }), [adapters.dataSource]);
1396
+ const logger = resolvedAdapters.logger;
1397
+ const prevInstanceRef = react.useRef(null);
1143
1398
  const value = react.useMemo(() => {
1144
- if (sdkRef.current) {
1145
- sdkRef.current.feedManager.destroy();
1146
- sdkRef.current.playerEngine.destroy();
1147
- sdkRef.current.resourceGovernor.destroy();
1148
- sdkRef.current.optimisticManager.destroy();
1149
- }
1150
1399
  const feedManager = new FeedManager(adapters.dataSource, {}, logger);
1400
+ if (initialItems && initialItems.length > 0) {
1401
+ feedManager.setInitialItems(initialItems);
1402
+ }
1151
1403
  const playerEngine = new PlayerEngine(
1152
1404
  {},
1153
- adapters.analytics,
1405
+ resolvedAdapters.analytics,
1154
1406
  logger
1155
1407
  );
1156
1408
  const resourceGovernor = new ResourceGovernor(
1157
1409
  {},
1158
- adapters.videoLoader,
1159
- adapters.network,
1410
+ resolvedAdapters.videoLoader,
1411
+ resolvedAdapters.network,
1160
1412
  logger
1161
1413
  );
1162
1414
  const optimisticManager = new OptimisticManager(
1163
- adapters.interaction ?? {},
1415
+ resolvedAdapters.interaction,
1416
+ logger
1417
+ );
1418
+ const navigationManager = new NavigationManager(
1419
+ navigationConfig ?? {},
1164
1420
  logger
1165
1421
  );
1166
1422
  const instance = {
@@ -1168,15 +1424,27 @@ function ReelsProvider({ children, adapters, debug = false }) {
1168
1424
  playerEngine,
1169
1425
  resourceGovernor,
1170
1426
  optimisticManager,
1171
- adapters
1427
+ navigationManager,
1428
+ adapters: resolvedAdapters
1172
1429
  };
1173
- sdkRef.current = instance;
1174
1430
  return instance;
1175
1431
  }, [adapters.dataSource]);
1176
1432
  react.useEffect(() => {
1177
- value.resourceGovernor.activate();
1433
+ const prev = prevInstanceRef.current;
1434
+ if (prev && prev !== value) {
1435
+ prev.feedManager.destroy();
1436
+ prev.playerEngine.destroy();
1437
+ prev.resourceGovernor.destroy();
1438
+ prev.optimisticManager.destroy();
1439
+ prev.navigationManager.destroy();
1440
+ }
1441
+ prevInstanceRef.current = value;
1442
+ }, [value]);
1443
+ react.useEffect(() => {
1444
+ const governor = value.resourceGovernor;
1445
+ governor.activate();
1178
1446
  return () => {
1179
- value.resourceGovernor.deactivate();
1447
+ governor.deactivate();
1180
1448
  };
1181
1449
  }, [value.resourceGovernor]);
1182
1450
  react.useEffect(() => {
@@ -1186,10 +1454,11 @@ function ReelsProvider({ children, adapters, debug = false }) {
1186
1454
  }, [debug, logger]);
1187
1455
  react.useEffect(() => {
1188
1456
  return () => {
1189
- sdkRef.current?.feedManager.destroy();
1190
- sdkRef.current?.playerEngine.destroy();
1191
- sdkRef.current?.resourceGovernor.destroy();
1192
- sdkRef.current?.optimisticManager.destroy();
1457
+ prevInstanceRef.current?.feedManager.destroy();
1458
+ prevInstanceRef.current?.playerEngine.destroy();
1459
+ prevInstanceRef.current?.resourceGovernor.destroy();
1460
+ prevInstanceRef.current?.optimisticManager.destroy();
1461
+ prevInstanceRef.current?.navigationManager.destroy();
1193
1462
  };
1194
1463
  }, []);
1195
1464
  return /* @__PURE__ */ jsxRuntime.jsx(SDKContext.Provider, { value, children });
@@ -1336,10 +1605,11 @@ var ACTIVE_HLS_DEFAULTS = {
1336
1605
  maxMaxBufferLength: 15,
1337
1606
  capLevelToPlayerSize: true,
1338
1607
  startLevel: 0,
1339
- abrEwmaDefaultEstimate: 5e5,
1608
+ abrEwmaDefaultEstimate: 2e6,
1340
1609
  lowLatencyMode: false,
1341
1610
  backBufferLength: 5,
1342
- enableWorker: true
1611
+ enableWorker: true,
1612
+ startFragPrefetch: true
1343
1613
  };
1344
1614
  var HOT_HLS_DEFAULTS = {
1345
1615
  maxBufferLength: 2,
@@ -1692,25 +1962,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
1692
1962
  var PLAY_AHEAD_STAGGER_MS = 80;
1693
1963
  var _playAheadActive = 0;
1694
1964
  var _playAheadQueue = [];
1965
+ var _tokenCounter = 0;
1966
+ var _releasedTokens = /* @__PURE__ */ new Set();
1695
1967
  function acquirePlayAhead() {
1968
+ const token = ++_tokenCounter;
1969
+ const makeRelease = () => {
1970
+ let released = false;
1971
+ return () => {
1972
+ if (released) return;
1973
+ released = true;
1974
+ _releasedTokens.add(token);
1975
+ _playAheadActive = Math.max(0, _playAheadActive - 1);
1976
+ const next = _playAheadQueue.shift();
1977
+ if (next) {
1978
+ next();
1979
+ }
1980
+ };
1981
+ };
1696
1982
  if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
1697
1983
  _playAheadActive++;
1698
- return Promise.resolve();
1984
+ return Promise.resolve(makeRelease());
1699
1985
  }
1700
1986
  return new Promise((resolve) => {
1701
1987
  _playAheadQueue.push(() => {
1702
- setTimeout(resolve, PLAY_AHEAD_STAGGER_MS);
1988
+ _playAheadActive++;
1989
+ setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
1703
1990
  });
1704
1991
  });
1705
1992
  }
1706
- function releasePlayAhead() {
1707
- _playAheadActive = Math.max(0, _playAheadActive - 1);
1708
- const next = _playAheadQueue.shift();
1709
- if (next) {
1710
- _playAheadActive++;
1711
- next();
1712
- }
1713
- }
1714
1993
  function VideoSlot({
1715
1994
  item,
1716
1995
  index,
@@ -1833,6 +2112,56 @@ function VideoSlotInner({
1833
2112
  }
1834
2113
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1835
2114
  const isReady = isHlsSource ? hlsReady : mp4Ready;
2115
+ const [capturedPoster, setCapturedPoster] = react.useState(null);
2116
+ react.useEffect(() => {
2117
+ const video = videoRef.current;
2118
+ if (!video || !shouldLoadSrc) return;
2119
+ let cancelled = false;
2120
+ const captureFrame = () => {
2121
+ if (cancelled) return;
2122
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
2123
+ if (video.videoWidth === 0 || video.videoHeight === 0) return;
2124
+ try {
2125
+ const canvas = document.createElement("canvas");
2126
+ canvas.width = video.videoWidth;
2127
+ canvas.height = video.videoHeight;
2128
+ const ctx = canvas.getContext("2d");
2129
+ if (!ctx) return;
2130
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2131
+ const dataUrl = canvas.toDataURL("image/webp", 0.85);
2132
+ if (!cancelled) {
2133
+ setCapturedPoster(dataUrl);
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ };
2138
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
2139
+ captureFrame();
2140
+ } else {
2141
+ video.addEventListener("loadeddata", captureFrame, { once: true });
2142
+ }
2143
+ return () => {
2144
+ cancelled = true;
2145
+ video.removeEventListener("loadeddata", captureFrame);
2146
+ };
2147
+ }, [src, shouldLoadSrc]);
2148
+ react.useEffect(() => {
2149
+ setCapturedPoster(null);
2150
+ }, [src]);
2151
+ const [isVideoPlaying, setIsVideoPlaying] = react.useState(false);
2152
+ react.useEffect(() => {
2153
+ const video = videoRef.current;
2154
+ if (!video || !isActive) {
2155
+ setIsVideoPlaying(false);
2156
+ return;
2157
+ }
2158
+ const onPlaying = () => setIsVideoPlaying(true);
2159
+ video.addEventListener("playing", onPlaying);
2160
+ return () => {
2161
+ video.removeEventListener("playing", onPlaying);
2162
+ setIsVideoPlaying(false);
2163
+ };
2164
+ }, [isActive]);
1836
2165
  const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1837
2166
  react.useEffect(() => {
1838
2167
  const video = videoRef.current;
@@ -1842,36 +2171,43 @@ function VideoSlotInner({
1842
2171
  const prevMuted = video.muted;
1843
2172
  video.muted = true;
1844
2173
  let cancelled = false;
2174
+ let release = null;
1845
2175
  const doPlayAhead = async () => {
1846
- await acquirePlayAhead();
2176
+ release = await acquirePlayAhead();
2177
+ if (cancelled) {
2178
+ release();
2179
+ return;
2180
+ }
1847
2181
  try {
1848
2182
  await video.play();
1849
2183
  if (cancelled) {
1850
2184
  video.pause();
1851
- releasePlayAhead();
2185
+ release();
1852
2186
  return;
1853
2187
  }
1854
2188
  const pauseAfterDecode = () => {
1855
2189
  video.pause();
1856
2190
  video.currentTime = 0;
1857
2191
  video.muted = prevMuted;
1858
- releasePlayAhead();
2192
+ release?.();
1859
2193
  if (!cancelled) {
1860
2194
  setHasPlayedAhead(true);
1861
2195
  }
1862
2196
  };
1863
2197
  setTimeout(pauseAfterDecode, 50);
1864
2198
  } catch {
1865
- releasePlayAhead();
2199
+ release?.();
1866
2200
  }
1867
2201
  };
1868
2202
  doPlayAhead();
1869
2203
  return () => {
1870
2204
  cancelled = true;
2205
+ release?.();
1871
2206
  };
1872
2207
  }, [isActive, isReady, hasPlayedAhead]);
1873
2208
  react.useEffect(() => {
1874
2209
  setHasPlayedAhead(false);
2210
+ setIsVideoPlaying(false);
1875
2211
  }, [src]);
1876
2212
  const wasActiveRef = react.useRef(false);
1877
2213
  const [isManuallyPaused, setIsManuallyPaused] = react.useState(false);
@@ -1948,7 +2284,7 @@ function VideoSlotInner({
1948
2284
  if (!video) return;
1949
2285
  video.muted = isActive ? isMuted : true;
1950
2286
  }, [isMuted, isActive]);
1951
- const showPosterOverlay = !isReady && !hasPlayedAhead;
2287
+ const showPosterOverlay = isActive ? !isVideoPlaying : !isReady && !hasPlayedAhead;
1952
2288
  const isPreDecoded = hasPlayedAhead;
1953
2289
  const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
1954
2290
  const muteIndicatorTimer = react.useRef(null);
@@ -2066,23 +2402,23 @@ function VideoSlotInner({
2066
2402
  height: "100%",
2067
2403
  objectFit: "cover",
2068
2404
  // Hide video until ready to avoid black frame flash.
2069
- // When pre-decoded, skip transition — first frame is already on canvas.
2405
+ // When pre-decoded or active, skip transition — first frame is already on canvas or playing.
2070
2406
  opacity: showPosterOverlay ? 0 : 1,
2071
- transition: isPreDecoded ? "none" : "opacity 0.15s ease"
2407
+ transition: isActive ? "none" : isPreDecoded ? "none" : "opacity 0.15s ease"
2072
2408
  }
2073
2409
  }
2074
2410
  ),
2075
- item.poster && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
2411
+ (capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
2076
2412
  "div",
2077
2413
  {
2078
2414
  style: {
2079
2415
  position: "absolute",
2080
2416
  inset: 0,
2081
- backgroundImage: `url(${item.poster})`,
2417
+ backgroundImage: `url(${capturedPoster || item.poster})`,
2082
2418
  backgroundSize: "cover",
2083
2419
  backgroundPosition: "center",
2084
2420
  opacity: showPosterOverlay ? 1 : 0,
2085
- transition: "opacity 0.15s ease",
2421
+ transition: isActive ? "none" : "opacity 0.15s ease",
2086
2422
  pointerEvents: "none"
2087
2423
  }
2088
2424
  }
@@ -2290,7 +2626,7 @@ function ReelsFeed({
2290
2626
  }, 16);
2291
2627
  };
2292
2628
  const observer = new MutationObserver(debouncedRebuild);
2293
- observer.observe(container, { childList: true, subtree: true });
2629
+ observer.observe(container, { childList: true, subtree: false });
2294
2630
  return () => {
2295
2631
  observer.disconnect();
2296
2632
  if (rebuildTimer !== null) clearTimeout(rebuildTimer);
@@ -2527,6 +2863,56 @@ function parsePxTranslateY(el) {
2527
2863
  if (!match || !match[1]) return 0;
2528
2864
  return Number.parseFloat(match[1]);
2529
2865
  }
2866
+ function useNavigationSelector(selector) {
2867
+ const { navigationManager } = useSDK();
2868
+ const selectorRef = react.useRef(selector);
2869
+ selectorRef.current = selector;
2870
+ const lastSnapshot = react.useRef(void 0);
2871
+ const lastState = react.useRef(void 0);
2872
+ const getSnapshot = react.useCallback(() => {
2873
+ const state = navigationManager.store.getState();
2874
+ if (state !== lastState.current) {
2875
+ lastState.current = state;
2876
+ lastSnapshot.current = selectorRef.current(state);
2877
+ }
2878
+ return lastSnapshot.current;
2879
+ }, [navigationManager]);
2880
+ return react.useSyncExternalStore(
2881
+ navigationManager.store.subscribe,
2882
+ getSnapshot,
2883
+ getSnapshot
2884
+ );
2885
+ }
2886
+ function useNavigation() {
2887
+ const { navigationManager } = useSDK();
2888
+ const selectPhase = react.useCallback((s) => s.phase, []);
2889
+ const selectOpenIndex = react.useCallback((s) => s.openIndex, []);
2890
+ const phase = useNavigationSelector(selectPhase);
2891
+ const openIndex = useNavigationSelector(selectOpenIndex);
2892
+ const isOpen = phase === "open" || phase === "opening";
2893
+ const shouldMount = phase !== "closed";
2894
+ const open = react.useCallback(
2895
+ (index) => navigationManager.open(index),
2896
+ [navigationManager]
2897
+ );
2898
+ const close = react.useCallback(
2899
+ () => navigationManager.close(),
2900
+ [navigationManager]
2901
+ );
2902
+ const setPhase = react.useCallback(
2903
+ (next) => navigationManager.setPhase(next),
2904
+ [navigationManager]
2905
+ );
2906
+ return {
2907
+ phase,
2908
+ openIndex,
2909
+ isOpen,
2910
+ shouldMount,
2911
+ open,
2912
+ close,
2913
+ setPhase
2914
+ };
2915
+ }
2530
2916
  function ReelsFeedThumbnail({
2531
2917
  renderThumbnail,
2532
2918
  onThumbnailClick,
@@ -2536,33 +2922,48 @@ function ReelsFeedThumbnail({
2536
2922
  className = "grid grid-cols-2 gap-3",
2537
2923
  wrap = true,
2538
2924
  setFocusOnClick = true,
2925
+ openOnClick = false,
2539
2926
  prefetchOnHover = false,
2927
+ prewarmOnClick = true,
2540
2928
  getKey
2541
2929
  }) {
2542
2930
  const { items, loading, error, refresh } = useFeed();
2543
2931
  const { setFocusedIndexImmediate } = useResource();
2932
+ const { open } = useNavigation();
2544
2933
  const { adapters } = useSDK();
2545
2934
  const prefetchedRef = react.useRef(/* @__PURE__ */ new Set());
2935
+ const prewarm = react.useCallback(
2936
+ (item) => {
2937
+ if (!isVideoItem(item)) return;
2938
+ if (item.source.type !== "hls") return;
2939
+ const url = item.source.url;
2940
+ if (prefetchedRef.current.has(url)) return;
2941
+ prefetchedRef.current.add(url);
2942
+ adapters.videoLoader?.preloadMetadata?.(url);
2943
+ },
2944
+ [adapters.videoLoader]
2945
+ );
2546
2946
  const handleClick = react.useCallback(
2547
2947
  (id, item, index) => {
2948
+ if (prewarmOnClick) {
2949
+ prewarm(item);
2950
+ }
2548
2951
  if (setFocusOnClick) {
2549
2952
  setFocusedIndexImmediate(index);
2550
2953
  }
2954
+ if (openOnClick) {
2955
+ open(index);
2956
+ }
2551
2957
  onThumbnailClick?.(id, item, index);
2552
2958
  },
2553
- [setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
2959
+ [prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
2554
2960
  );
2555
2961
  const handlePointerEnter = react.useCallback(
2556
2962
  (item) => {
2557
2963
  if (!prefetchOnHover) return;
2558
- if (!isVideoItem(item)) return;
2559
- if (item.source.type !== "hls") return;
2560
- const url = item.source.url;
2561
- if (prefetchedRef.current.has(url)) return;
2562
- prefetchedRef.current.add(url);
2563
- adapters.videoLoader?.preloadMetadata?.(url);
2964
+ prewarm(item);
2564
2965
  },
2565
- [prefetchOnHover, adapters.videoLoader]
2966
+ [prefetchOnHover, prewarm]
2566
2967
  );
2567
2968
  if (loading && items.length === 0) {
2568
2969
  if (renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
@@ -2602,6 +3003,309 @@ function ReelsFeedThumbnail({
2602
3003
  }
2603
3004
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
2604
3005
  }
3006
+ var DEFAULT_DURATION = 300;
3007
+ var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
3008
+ var DEFAULT_DIRECTION = "up";
3009
+ var DEFAULT_Z_INDEX = 1e3;
3010
+ var DEFAULT_PREWARM_FORWARD = 2;
3011
+ function offscreenTransform(direction) {
3012
+ switch (direction) {
3013
+ case "down":
3014
+ return "translateY(-100%)";
3015
+ case "fade":
3016
+ return "translateY(0)";
3017
+ case "up":
3018
+ default:
3019
+ return "translateY(100%)";
3020
+ }
3021
+ }
3022
+ function prefersReducedMotion() {
3023
+ if (typeof window === "undefined" || !window.matchMedia) return false;
3024
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3025
+ }
3026
+ var FOCUSABLE = 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
3027
+ function ReelsModal({
3028
+ feedProps,
3029
+ animationConfig,
3030
+ renderBackdrop,
3031
+ renderCloseButton,
3032
+ onOpen,
3033
+ onClose,
3034
+ closeOnBackdropClick = true,
3035
+ closeOnEscape = true,
3036
+ lockBodyScroll = true,
3037
+ prewarmForward = DEFAULT_PREWARM_FORWARD,
3038
+ className,
3039
+ zIndex = DEFAULT_Z_INDEX,
3040
+ portalTarget
3041
+ }) {
3042
+ const { phase, openIndex, shouldMount, close, setPhase } = useNavigation();
3043
+ const { setFocusedIndexImmediate } = useResource();
3044
+ const { items } = useFeed();
3045
+ const { adapters } = useSDK();
3046
+ const duration = animationConfig?.duration ?? DEFAULT_DURATION;
3047
+ const easing = animationConfig?.easing ?? DEFAULT_EASING;
3048
+ const direction = animationConfig?.direction ?? DEFAULT_DIRECTION;
3049
+ const backdropRef = react.useRef(null);
3050
+ const panelRef = react.useRef(null);
3051
+ const animationsRef = react.useRef([]);
3052
+ const previouslyFocusedRef = react.useRef(null);
3053
+ const renderState = react.useMemo(
3054
+ () => ({ phase, openIndex, close }),
3055
+ [phase, openIndex, close]
3056
+ );
3057
+ const prewarmedRef = react.useRef(null);
3058
+ react.useEffect(() => {
3059
+ if (phase !== "opening") {
3060
+ if (phase === "closed") prewarmedRef.current = null;
3061
+ return;
3062
+ }
3063
+ if (openIndex == null) return;
3064
+ if (prewarmedRef.current === openIndex) return;
3065
+ prewarmedRef.current = openIndex;
3066
+ setFocusedIndexImmediate(openIndex);
3067
+ const preload = adapters.videoLoader?.preloadMetadata;
3068
+ if (!preload) return;
3069
+ const targets = /* @__PURE__ */ new Set();
3070
+ targets.add(openIndex);
3071
+ for (let d = 1; d <= prewarmForward; d++) targets.add(openIndex + d);
3072
+ targets.add(openIndex - 1);
3073
+ for (const idx of targets) {
3074
+ if (idx < 0 || idx >= items.length) continue;
3075
+ const item = items[idx];
3076
+ if (item && isVideoItem(item) && item.source.type === "hls") {
3077
+ preload(item.source.url);
3078
+ }
3079
+ }
3080
+ }, [phase, openIndex, items, adapters.videoLoader, prewarmForward, setFocusedIndexImmediate]);
3081
+ react.useEffect(() => {
3082
+ if (!lockBodyScroll || !shouldMount) return;
3083
+ if (typeof document === "undefined") return;
3084
+ const prev = document.body.style.overflow;
3085
+ document.body.style.overflow = "hidden";
3086
+ return () => {
3087
+ document.body.style.overflow = prev;
3088
+ };
3089
+ }, [lockBodyScroll, shouldMount]);
3090
+ const cancelAnimations = react.useCallback(() => {
3091
+ for (const a of animationsRef.current) a.cancel();
3092
+ animationsRef.current = [];
3093
+ }, []);
3094
+ react.useLayoutEffect(() => {
3095
+ const panel = panelRef.current;
3096
+ const backdrop = backdropRef.current;
3097
+ if (!panel) return;
3098
+ if (phase === "open" || phase === "closed") return;
3099
+ const reduce = prefersReducedMotion();
3100
+ const enter = phase === "opening";
3101
+ const offscreen2 = offscreenTransform(direction);
3102
+ const panelFrames = direction === "fade" ? enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }] : enter ? [{ transform: offscreen2 }, { transform: "translateY(0)" }] : [{ transform: "translateY(0)" }, { transform: offscreen2 }];
3103
+ const backdropFrames = enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
3104
+ const animDuration = reduce ? 0 : duration;
3105
+ let cancelled = false;
3106
+ cancelAnimations();
3107
+ const anims = [];
3108
+ const panelAnim = panel.animate(panelFrames, {
3109
+ duration: animDuration,
3110
+ easing,
3111
+ fill: "forwards"
3112
+ });
3113
+ anims.push(panelAnim);
3114
+ if (backdrop) {
3115
+ anims.push(
3116
+ backdrop.animate(backdropFrames, {
3117
+ duration: animDuration,
3118
+ easing,
3119
+ fill: "forwards"
3120
+ })
3121
+ );
3122
+ }
3123
+ animationsRef.current = anims;
3124
+ const finalize = () => {
3125
+ if (cancelled) return;
3126
+ if (enter) {
3127
+ if (direction === "fade") panel.style.opacity = "1";
3128
+ else panel.style.transform = "translateY(0)";
3129
+ if (backdrop) backdrop.style.opacity = "1";
3130
+ } else {
3131
+ if (direction === "fade") panel.style.opacity = "0";
3132
+ else panel.style.transform = offscreen2;
3133
+ if (backdrop) backdrop.style.opacity = "0";
3134
+ }
3135
+ for (const a of anims) a.cancel();
3136
+ animationsRef.current = [];
3137
+ setPhase(enter ? "open" : "closed");
3138
+ };
3139
+ panelAnim.addEventListener("finish", finalize);
3140
+ return () => {
3141
+ cancelled = true;
3142
+ panelAnim.removeEventListener("finish", finalize);
3143
+ cancelAnimations();
3144
+ };
3145
+ }, [phase, direction, duration, easing, setPhase, cancelAnimations]);
3146
+ const prevPhaseRef = react.useRef("closed");
3147
+ react.useEffect(() => {
3148
+ const prev = prevPhaseRef.current;
3149
+ if (prev !== "open" && phase === "open") onOpen?.(openIndex);
3150
+ if (prev !== "closed" && phase === "closed") onClose?.();
3151
+ prevPhaseRef.current = phase;
3152
+ }, [phase, openIndex, onOpen, onClose]);
3153
+ react.useEffect(() => {
3154
+ if (phase === "opening" && typeof document !== "undefined") {
3155
+ previouslyFocusedRef.current = document.activeElement;
3156
+ const id = requestAnimationFrame(() => {
3157
+ const panel = panelRef.current;
3158
+ if (!panel) return;
3159
+ const first = panel.querySelector(FOCUSABLE);
3160
+ (first ?? panel).focus();
3161
+ });
3162
+ return () => cancelAnimationFrame(id);
3163
+ }
3164
+ if (phase === "closed") {
3165
+ previouslyFocusedRef.current?.focus?.();
3166
+ previouslyFocusedRef.current = null;
3167
+ }
3168
+ return void 0;
3169
+ }, [phase]);
3170
+ const handleKeyDown = react.useCallback(
3171
+ (e) => {
3172
+ if (closeOnEscape && e.key === "Escape") {
3173
+ e.stopPropagation();
3174
+ close();
3175
+ return;
3176
+ }
3177
+ if (e.key !== "Tab") return;
3178
+ const panel = panelRef.current;
3179
+ if (!panel) return;
3180
+ const focusables = Array.from(
3181
+ panel.querySelectorAll(FOCUSABLE)
3182
+ ).filter((el) => el.offsetParent !== null);
3183
+ if (focusables.length === 0) {
3184
+ e.preventDefault();
3185
+ panel.focus();
3186
+ return;
3187
+ }
3188
+ const first = focusables[0];
3189
+ const last = focusables[focusables.length - 1];
3190
+ const active = document.activeElement;
3191
+ if (e.shiftKey && active === first) {
3192
+ e.preventDefault();
3193
+ last.focus();
3194
+ } else if (!e.shiftKey && active === last) {
3195
+ e.preventDefault();
3196
+ first.focus();
3197
+ }
3198
+ },
3199
+ [closeOnEscape, close]
3200
+ );
3201
+ const handleBackdropClick = react.useCallback(() => {
3202
+ if (closeOnBackdropClick) close();
3203
+ }, [closeOnBackdropClick, close]);
3204
+ if (!shouldMount) return null;
3205
+ if (typeof document === "undefined") return null;
3206
+ const target = portalTarget ?? document.body;
3207
+ if (!target) return null;
3208
+ const enterStart = phase === "opening";
3209
+ const offscreen = offscreenTransform(direction);
3210
+ const panelInitialTransform = direction === "fade" ? "translateY(0)" : enterStart ? offscreen : "translateY(0)";
3211
+ const panelInitialOpacity = direction === "fade" ? enterStart ? 0 : 1 : 1;
3212
+ const backdropInitialOpacity = enterStart ? 0 : 1;
3213
+ const rootStyle = {
3214
+ position: "fixed",
3215
+ inset: 0,
3216
+ zIndex,
3217
+ // While opening, the panel is parked off-screen but MUST stay painted so
3218
+ // the <video> decodes its first frame. overflow:hidden keeps the
3219
+ // off-screen panel from creating scrollbars / capturing stray taps.
3220
+ overflow: "hidden"
3221
+ };
3222
+ const backdropStyle = {
3223
+ position: "absolute",
3224
+ inset: 0,
3225
+ opacity: backdropInitialOpacity,
3226
+ background: renderBackdrop ? "transparent" : "rgba(0,0,0,0.85)",
3227
+ // willChange hints the compositor for the opacity fade.
3228
+ willChange: "opacity"
3229
+ };
3230
+ const panelStyle = {
3231
+ position: "absolute",
3232
+ inset: 0,
3233
+ transform: panelInitialTransform,
3234
+ opacity: panelInitialOpacity,
3235
+ willChange: direction === "fade" ? "opacity" : "transform",
3236
+ outline: "none"
3237
+ };
3238
+ return reactDom.createPortal(
3239
+ /* @__PURE__ */ jsxRuntime.jsxs(
3240
+ "div",
3241
+ {
3242
+ style: rootStyle,
3243
+ onKeyDown: handleKeyDown,
3244
+ "data-reels-modal-phase": phase,
3245
+ children: [
3246
+ /* @__PURE__ */ jsxRuntime.jsx(
3247
+ "div",
3248
+ {
3249
+ ref: backdropRef,
3250
+ style: backdropStyle,
3251
+ onClick: handleBackdropClick,
3252
+ "aria-hidden": "true",
3253
+ "data-reels-modal-backdrop": true,
3254
+ children: renderBackdrop ? renderBackdrop(renderState) : null
3255
+ }
3256
+ ),
3257
+ /* @__PURE__ */ jsxRuntime.jsxs(
3258
+ "div",
3259
+ {
3260
+ ref: panelRef,
3261
+ role: "dialog",
3262
+ "aria-modal": "true",
3263
+ "aria-label": "Reels viewer",
3264
+ tabIndex: -1,
3265
+ className,
3266
+ style: panelStyle,
3267
+ "data-reels-modal-panel": true,
3268
+ children: [
3269
+ /* @__PURE__ */ jsxRuntime.jsx(ReelsFeed, { ...feedProps }),
3270
+ renderCloseButton ? renderCloseButton(renderState) : /* @__PURE__ */ jsxRuntime.jsx(DefaultCloseButton, { onClose: close })
3271
+ ]
3272
+ }
3273
+ )
3274
+ ]
3275
+ }
3276
+ ),
3277
+ target
3278
+ );
3279
+ }
3280
+ function DefaultCloseButton({ onClose }) {
3281
+ return /* @__PURE__ */ jsxRuntime.jsx(
3282
+ "button",
3283
+ {
3284
+ type: "button",
3285
+ onClick: onClose,
3286
+ "aria-label": "Close reels viewer",
3287
+ style: {
3288
+ position: "absolute",
3289
+ top: "max(12px, env(safe-area-inset-top))",
3290
+ right: 12,
3291
+ width: 40,
3292
+ height: 40,
3293
+ display: "flex",
3294
+ alignItems: "center",
3295
+ justifyContent: "center",
3296
+ borderRadius: "50%",
3297
+ border: "none",
3298
+ background: "rgba(0,0,0,0.45)",
3299
+ color: "#fff",
3300
+ fontSize: 22,
3301
+ lineHeight: 1,
3302
+ cursor: "pointer",
3303
+ zIndex: 2
3304
+ },
3305
+ children: "\u2715"
3306
+ }
3307
+ );
3308
+ }
2605
3309
  function usePlayerSelector(selector) {
2606
3310
  const { playerEngine } = useSDK();
2607
3311
  const selectorRef = react.useRef(selector);
@@ -3217,6 +3921,7 @@ var HttpError = class extends Error {
3217
3921
  };
3218
3922
 
3219
3923
  exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
3924
+ exports.DEFAULT_NAVIGATION_CONFIG = DEFAULT_NAVIGATION_CONFIG;
3220
3925
  exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
3221
3926
  exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
3222
3927
  exports.DefaultActions = DefaultActions;
@@ -3234,11 +3939,13 @@ exports.MockLogger = MockLogger;
3234
3939
  exports.MockNetworkAdapter = MockNetworkAdapter;
3235
3940
  exports.MockSessionStorage = MockSessionStorage;
3236
3941
  exports.MockVideoLoader = MockVideoLoader;
3942
+ exports.NavigationManager = NavigationManager;
3237
3943
  exports.OptimisticManager = OptimisticManager;
3238
3944
  exports.PlayerEngine = PlayerEngine;
3239
3945
  exports.PlayerStatus = PlayerStatus;
3240
3946
  exports.ReelsFeed = ReelsFeed;
3241
3947
  exports.ReelsFeedThumbnail = ReelsFeedThumbnail;
3948
+ exports.ReelsModal = ReelsModal;
3242
3949
  exports.ReelsProvider = ReelsProvider;
3243
3950
  exports.ResourceGovernor = ResourceGovernor;
3244
3951
  exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
@@ -3247,11 +3954,14 @@ exports.canPause = canPause;
3247
3954
  exports.canPlay = canPlay;
3248
3955
  exports.canSeek = canSeek;
3249
3956
  exports.isArticle = isArticle;
3957
+ exports.isValidNavTransition = isValidNavTransition;
3250
3958
  exports.isValidTransition = isValidTransition;
3251
3959
  exports.isVideoItem = isVideoItem;
3252
3960
  exports.useFeed = useFeed;
3253
3961
  exports.useFeedSelector = useFeedSelector;
3254
3962
  exports.useHls = useHls;
3963
+ exports.useNavigation = useNavigation;
3964
+ exports.useNavigationSelector = useNavigationSelector;
3255
3965
  exports.usePlayer = usePlayer;
3256
3966
  exports.usePlayerSelector = usePlayerSelector;
3257
3967
  exports.usePointerGesture = usePointerGesture;