@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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createStore } from 'zustand/vanilla';
2
- import { createContext, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore, useState } from 'react';
2
+ import { createContext, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore, useState, useLayoutEffect } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import Hls from 'hls.js';
5
+ import { createPortal } from 'react-dom';
5
6
 
6
7
  // src/types/content.ts
7
8
  function isVideoItem(item) {
@@ -100,6 +101,7 @@ var PlayerEngine = class {
100
101
  this.circuitResetTimer = null;
101
102
  this.watchTimeInterval = null;
102
103
  this.lastStatus = "idle" /* IDLE */;
104
+ this._destroyed = false;
103
105
  this.config = { ...DEFAULT_PLAYER_CONFIG, ...config };
104
106
  this.analytics = analytics;
105
107
  this.logger = logger;
@@ -309,8 +311,13 @@ var PlayerEngine = class {
309
311
  this.lastStatus = "idle" /* IDLE */;
310
312
  }
311
313
  destroy() {
314
+ if (this._destroyed) return;
315
+ this._destroyed = true;
312
316
  this.stopWatchTime();
313
- if (this.circuitResetTimer) clearTimeout(this.circuitResetTimer);
317
+ if (this.circuitResetTimer) {
318
+ clearTimeout(this.circuitResetTimer);
319
+ this.circuitResetTimer = null;
320
+ }
314
321
  this.listeners.clear();
315
322
  }
316
323
  // ═══════════════════════════════════════════
@@ -413,6 +420,8 @@ var FeedManager = class {
413
420
  this.inFlightRequests = /* @__PURE__ */ new Map();
414
421
  /** LRU tracking: itemId → lastAccessTime */
415
422
  this.accessOrder = /* @__PURE__ */ new Map();
423
+ /** Idempotency guard for destroy() */
424
+ this._destroyed = false;
416
425
  /** Prefetch cache — instance-scoped (not static) */
417
426
  this.prefetchCache = null;
418
427
  this.config = { ...DEFAULT_FEED_CONFIG, ...config };
@@ -438,6 +447,13 @@ var FeedManager = class {
438
447
  // ═══════════════════════════════════════════
439
448
  // PUBLIC API — Prefetch
440
449
  // ═══════════════════════════════════════════
450
+ setInitialItems(items) {
451
+ this.prefetchCache = {
452
+ items,
453
+ nextCursor: null,
454
+ timestamp: Date.now()
455
+ };
456
+ }
441
457
  async prefetch(ttlMs) {
442
458
  if (this.prefetchCache) {
443
459
  const ttl = ttlMs ?? this.config.staleTTL;
@@ -518,7 +534,10 @@ var FeedManager = class {
518
534
  // PUBLIC API — Lifecycle
519
535
  // ═══════════════════════════════════════════
520
536
  destroy() {
537
+ if (this._destroyed) return;
538
+ this._destroyed = true;
521
539
  this.abortController?.abort();
540
+ this.abortController = null;
522
541
  this.inFlightRequests.clear();
523
542
  this.accessOrder.clear();
524
543
  this.prefetchCache = null;
@@ -587,7 +606,13 @@ var FeedManager = class {
587
606
  applyItems(incoming, _nextCursor, append) {
588
607
  const { itemsById, displayOrder } = this.store.getState();
589
608
  const nextById = new Map(itemsById);
609
+ const seenInBatch = /* @__PURE__ */ new Set();
610
+ const deduped = [];
590
611
  for (const item of incoming) {
612
+ if (!seenInBatch.has(item.id)) {
613
+ seenInBatch.add(item.id);
614
+ deduped.push(item);
615
+ }
591
616
  nextById.set(item.id, item);
592
617
  this.accessOrder.set(item.id, Date.now());
593
618
  }
@@ -595,14 +620,14 @@ var FeedManager = class {
595
620
  if (append) {
596
621
  const existingIds = new Set(displayOrder);
597
622
  const newIds = [];
598
- for (const item of incoming) {
623
+ for (const item of deduped) {
599
624
  if (!existingIds.has(item.id)) {
600
625
  newIds.push(item.id);
601
626
  }
602
627
  }
603
628
  nextOrder = [...displayOrder, ...newIds];
604
629
  } else {
605
- nextOrder = incoming.map((item) => item.id);
630
+ nextOrder = deduped.map((item) => item.id);
606
631
  }
607
632
  if (nextById.size > this.config.maxCacheSize) {
608
633
  this.evictLRU(nextById, nextOrder);
@@ -644,6 +669,8 @@ var OptimisticManager = class {
644
669
  this.likeDebounceTimers = /* @__PURE__ */ new Map();
645
670
  /** Pending like direction: contentId → final intended state */
646
671
  this.pendingLikeState = /* @__PURE__ */ new Map();
672
+ /** Idempotency guard for destroy() */
673
+ this._destroyed = false;
647
674
  this.interaction = interaction;
648
675
  this.logger = logger;
649
676
  this.store = createStore(createInitialState3);
@@ -734,6 +761,8 @@ var OptimisticManager = class {
734
761
  // PUBLIC API — Lifecycle
735
762
  // ═══════════════════════════════════════════
736
763
  destroy() {
764
+ if (this._destroyed) return;
765
+ this._destroyed = true;
737
766
  for (const timer of this.likeDebounceTimers.values()) {
738
767
  clearTimeout(timer);
739
768
  }
@@ -755,6 +784,7 @@ var DEFAULT_RESOURCE_CONFIG = {
755
784
  var ResourceGovernor = class {
756
785
  constructor(config = {}, videoLoader, network, logger) {
757
786
  this.focusDebounceTimer = null;
787
+ this._destroyed = false;
758
788
  this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
759
789
  this.videoLoader = videoLoader;
760
790
  this.network = network;
@@ -785,11 +815,19 @@ var ResourceGovernor = class {
785
815
  this.logger?.debug("[ResourceGovernor] Activated");
786
816
  }
787
817
  deactivate() {
818
+ if (this._destroyed) return;
819
+ if (!this.store.getState().isActive) return;
788
820
  this.networkUnsubscribe?.();
789
- if (this.focusDebounceTimer) clearTimeout(this.focusDebounceTimer);
821
+ this.networkUnsubscribe = void 0;
822
+ if (this.focusDebounceTimer) {
823
+ clearTimeout(this.focusDebounceTimer);
824
+ this.focusDebounceTimer = null;
825
+ }
790
826
  this.store.setState({ isActive: false });
791
827
  }
792
828
  destroy() {
829
+ if (this._destroyed) return;
830
+ this._destroyed = true;
793
831
  this.deactivate();
794
832
  this.videoLoader?.clearAll();
795
833
  }
@@ -913,6 +951,153 @@ var ResourceGovernor = class {
913
951
  );
914
952
  }
915
953
  };
954
+ var DEFAULT_NAVIGATION_CONFIG = {
955
+ phaseTimeoutMs: 1200
956
+ };
957
+ var VALID_NAV_TRANSITIONS = {
958
+ closed: ["opening"],
959
+ // Allow opening → closing so a fast user can dismiss mid-open.
960
+ opening: ["open", "closing", "closed"],
961
+ open: ["closing"],
962
+ // Allow closing → opening so re-opening during the exit animation works.
963
+ closing: ["closed", "opening"]
964
+ };
965
+ function isValidNavTransition(from, to) {
966
+ if (from === to) return false;
967
+ return VALID_NAV_TRANSITIONS[from].includes(to);
968
+ }
969
+ var NavigationManager = class {
970
+ constructor(config = {}, logger) {
971
+ /** Fallback timer guarding against a missing animationend report. */
972
+ this.phaseTimer = null;
973
+ this._destroyed = false;
974
+ this.config = { ...DEFAULT_NAVIGATION_CONFIG, ...config };
975
+ this.logger = logger;
976
+ this.store = createStore(() => ({
977
+ phase: "closed",
978
+ openIndex: null
979
+ }));
980
+ }
981
+ // ═══════════════════════════════════════════
982
+ // PUBLIC API — Intent
983
+ // ═══════════════════════════════════════════
984
+ /**
985
+ * Open the reels viewer at `index`. Safe to call from a thumbnail click
986
+ * handler — it transitions into `opening`, at which point ReelsModal mounts
987
+ * <ReelsFeed> hidden and begins prewarming.
988
+ *
989
+ * Calling open() while already open/opening just retargets the index
990
+ * (e.g. user taps a different thumbnail before the animation settles).
991
+ */
992
+ open(index) {
993
+ if (this._destroyed) return;
994
+ const { phase, openIndex } = this.store.getState();
995
+ if ((phase === "open" || phase === "opening") && index === openIndex) {
996
+ return;
997
+ }
998
+ if (phase === "open" || phase === "opening") {
999
+ this.store.setState({ openIndex: index });
1000
+ this.logger?.debug(`[NavigationManager] retarget openIndex=${index}`);
1001
+ return;
1002
+ }
1003
+ if (!this.transition("opening")) return;
1004
+ this.store.setState({ openIndex: index });
1005
+ this.logger?.debug(`[NavigationManager] open(${index}) \u2192 opening`);
1006
+ }
1007
+ /**
1008
+ * Begin closing the viewer. ReelsModal animates out then calls
1009
+ * `setPhase('closed')`. Idempotent if already closing/closed.
1010
+ */
1011
+ close() {
1012
+ if (this._destroyed) return;
1013
+ const { phase } = this.store.getState();
1014
+ if (phase === "closed" || phase === "closing") return;
1015
+ this.transition("closing");
1016
+ this.logger?.debug("[NavigationManager] close() \u2192 closing");
1017
+ }
1018
+ // ═══════════════════════════════════════════
1019
+ // PUBLIC API — Phase reporting (called by ReelsModal)
1020
+ // ═══════════════════════════════════════════
1021
+ /**
1022
+ * Report an animation-driven phase change from the component layer.
1023
+ * Invalid transitions are dropped. When reaching `closed`, openIndex is
1024
+ * cleared so <ReelsFeed> unmounts.
1025
+ */
1026
+ setPhase(next) {
1027
+ if (this._destroyed) return;
1028
+ this.transition(next);
1029
+ }
1030
+ // ═══════════════════════════════════════════
1031
+ // PUBLIC API — Queries
1032
+ // ═══════════════════════════════════════════
1033
+ getPhase() {
1034
+ return this.store.getState().phase;
1035
+ }
1036
+ getOpenIndex() {
1037
+ return this.store.getState().openIndex;
1038
+ }
1039
+ isOpen() {
1040
+ const { phase } = this.store.getState();
1041
+ return phase === "open" || phase === "opening";
1042
+ }
1043
+ /** Whether <ReelsFeed> should be mounted (any non-closed phase). */
1044
+ shouldMount() {
1045
+ return this.store.getState().phase !== "closed";
1046
+ }
1047
+ // ═══════════════════════════════════════════
1048
+ // PUBLIC API — Lifecycle
1049
+ // ═══════════════════════════════════════════
1050
+ destroy() {
1051
+ if (this._destroyed) return;
1052
+ this._destroyed = true;
1053
+ if (this.phaseTimer) {
1054
+ clearTimeout(this.phaseTimer);
1055
+ this.phaseTimer = null;
1056
+ }
1057
+ }
1058
+ // ═══════════════════════════════════════════
1059
+ // PRIVATE
1060
+ // ═══════════════════════════════════════════
1061
+ /**
1062
+ * Apply a guarded phase transition. Returns true if it was applied.
1063
+ * Manages the fallback timer for transient phases (opening/closing).
1064
+ */
1065
+ transition(next) {
1066
+ const { phase } = this.store.getState();
1067
+ if (!isValidNavTransition(phase, next)) {
1068
+ this.logger?.debug(
1069
+ `[NavigationManager] dropped invalid transition ${phase} \u2192 ${next}`
1070
+ );
1071
+ return false;
1072
+ }
1073
+ this.clearPhaseTimer();
1074
+ if (next === "closed") {
1075
+ this.store.setState({ phase: "closed", openIndex: null });
1076
+ return true;
1077
+ }
1078
+ this.store.setState({ phase: next });
1079
+ if (next === "opening" || next === "closing") {
1080
+ const fallbackTarget = next === "opening" ? "open" : "closed";
1081
+ this.phaseTimer = setTimeout(() => {
1082
+ this.phaseTimer = null;
1083
+ const current = this.store.getState().phase;
1084
+ if (current === next) {
1085
+ this.logger?.warn(
1086
+ `[NavigationManager] phase fallback ${next} \u2192 ${fallbackTarget}`
1087
+ );
1088
+ this.transition(fallbackTarget);
1089
+ }
1090
+ }, this.config.phaseTimeoutMs);
1091
+ }
1092
+ return true;
1093
+ }
1094
+ clearPhaseTimer() {
1095
+ if (this.phaseTimer) {
1096
+ clearTimeout(this.phaseTimer);
1097
+ this.phaseTimer = null;
1098
+ }
1099
+ }
1100
+ };
916
1101
  function usePointerGesture(config = {}) {
917
1102
  const {
918
1103
  axis = "y",
@@ -1102,7 +1287,9 @@ function useSnapAnimation(config = {}) {
1102
1287
  }
1103
1288
  );
1104
1289
  anim.addEventListener("finish", () => {
1105
- element.style.transform = `translateY(${toY}px)`;
1290
+ if (element.isConnected) {
1291
+ element.style.transform = `translateY(${toY}px)`;
1292
+ }
1106
1293
  anim.cancel();
1107
1294
  });
1108
1295
  animations.push(anim);
@@ -1130,31 +1317,100 @@ function useSnapAnimation(config = {}) {
1130
1317
  }, [cancelAnimation]);
1131
1318
  return { animateSnap, animateBounceBack, cancelAnimation };
1132
1319
  }
1320
+ var noop = () => {
1321
+ };
1322
+ var noopAsync = () => Promise.resolve();
1323
+ var DEFAULT_LOGGER = {
1324
+ debug: noop,
1325
+ info: noop,
1326
+ warn: noop,
1327
+ error: noop
1328
+ };
1329
+ var DEFAULT_ANALYTICS = {
1330
+ trackView: noop,
1331
+ trackLike: noop,
1332
+ trackShare: noop,
1333
+ trackComment: noop,
1334
+ trackError: noop,
1335
+ trackPlaybackEvent: noop
1336
+ };
1337
+ var DEFAULT_INTERACTION = {
1338
+ like: noopAsync,
1339
+ unlike: noopAsync,
1340
+ follow: noopAsync,
1341
+ unfollow: noopAsync,
1342
+ bookmark: noopAsync,
1343
+ unbookmark: noopAsync,
1344
+ share: noopAsync
1345
+ };
1346
+ var DEFAULT_STORAGE = {
1347
+ get: () => null,
1348
+ set: noop,
1349
+ remove: noop,
1350
+ clear: noop
1351
+ };
1352
+ var DEFAULT_NETWORK = {
1353
+ getNetworkType: () => "unknown",
1354
+ isOnline: () => true,
1355
+ onNetworkChange: () => noop
1356
+ };
1357
+ var DEFAULT_VIDEO_LOADER = {
1358
+ preload: (videoId) => Promise.resolve({ videoId, status: "idle" }),
1359
+ cancel: noop,
1360
+ isPreloaded: () => false,
1361
+ getPreloadStatus: () => "idle",
1362
+ clearAll: noop
1363
+ };
1364
+ var DEFAULT_COMMENT = {
1365
+ fetchComments: () => Promise.resolve({ items: [], nextCursor: null, hasMore: false, total: 0 }),
1366
+ postComment: () => Promise.reject(new Error("Comment adapter not configured")),
1367
+ deleteComment: noopAsync,
1368
+ likeComment: noopAsync,
1369
+ unlikeComment: noopAsync
1370
+ };
1133
1371
  var SDKContext = createContext(null);
1134
- function ReelsProvider({ children, adapters, debug = false }) {
1135
- const logger = adapters.logger;
1136
- const sdkRef = useRef(null);
1372
+ function ReelsProvider({
1373
+ children,
1374
+ adapters,
1375
+ initialItems,
1376
+ debug = false,
1377
+ navigationConfig
1378
+ }) {
1379
+ const resolvedAdapters = useMemo(() => ({
1380
+ dataSource: adapters.dataSource,
1381
+ interaction: adapters.interaction ?? DEFAULT_INTERACTION,
1382
+ storage: adapters.storage ?? DEFAULT_STORAGE,
1383
+ analytics: adapters.analytics ?? DEFAULT_ANALYTICS,
1384
+ logger: adapters.logger ?? DEFAULT_LOGGER,
1385
+ network: adapters.network ?? DEFAULT_NETWORK,
1386
+ videoLoader: adapters.videoLoader ?? DEFAULT_VIDEO_LOADER,
1387
+ comment: adapters.comment ?? DEFAULT_COMMENT
1388
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1389
+ }), [adapters.dataSource]);
1390
+ const logger = resolvedAdapters.logger;
1391
+ const prevInstanceRef = useRef(null);
1137
1392
  const value = useMemo(() => {
1138
- if (sdkRef.current) {
1139
- sdkRef.current.feedManager.destroy();
1140
- sdkRef.current.playerEngine.destroy();
1141
- sdkRef.current.resourceGovernor.destroy();
1142
- sdkRef.current.optimisticManager.destroy();
1143
- }
1144
1393
  const feedManager = new FeedManager(adapters.dataSource, {}, logger);
1394
+ if (initialItems && initialItems.length > 0) {
1395
+ feedManager.setInitialItems(initialItems);
1396
+ }
1145
1397
  const playerEngine = new PlayerEngine(
1146
1398
  {},
1147
- adapters.analytics,
1399
+ resolvedAdapters.analytics,
1148
1400
  logger
1149
1401
  );
1150
1402
  const resourceGovernor = new ResourceGovernor(
1151
1403
  {},
1152
- adapters.videoLoader,
1153
- adapters.network,
1404
+ resolvedAdapters.videoLoader,
1405
+ resolvedAdapters.network,
1154
1406
  logger
1155
1407
  );
1156
1408
  const optimisticManager = new OptimisticManager(
1157
- adapters.interaction ?? {},
1409
+ resolvedAdapters.interaction,
1410
+ logger
1411
+ );
1412
+ const navigationManager = new NavigationManager(
1413
+ navigationConfig ?? {},
1158
1414
  logger
1159
1415
  );
1160
1416
  const instance = {
@@ -1162,15 +1418,27 @@ function ReelsProvider({ children, adapters, debug = false }) {
1162
1418
  playerEngine,
1163
1419
  resourceGovernor,
1164
1420
  optimisticManager,
1165
- adapters
1421
+ navigationManager,
1422
+ adapters: resolvedAdapters
1166
1423
  };
1167
- sdkRef.current = instance;
1168
1424
  return instance;
1169
1425
  }, [adapters.dataSource]);
1170
1426
  useEffect(() => {
1171
- value.resourceGovernor.activate();
1427
+ const prev = prevInstanceRef.current;
1428
+ if (prev && prev !== value) {
1429
+ prev.feedManager.destroy();
1430
+ prev.playerEngine.destroy();
1431
+ prev.resourceGovernor.destroy();
1432
+ prev.optimisticManager.destroy();
1433
+ prev.navigationManager.destroy();
1434
+ }
1435
+ prevInstanceRef.current = value;
1436
+ }, [value]);
1437
+ useEffect(() => {
1438
+ const governor = value.resourceGovernor;
1439
+ governor.activate();
1172
1440
  return () => {
1173
- value.resourceGovernor.deactivate();
1441
+ governor.deactivate();
1174
1442
  };
1175
1443
  }, [value.resourceGovernor]);
1176
1444
  useEffect(() => {
@@ -1180,10 +1448,11 @@ function ReelsProvider({ children, adapters, debug = false }) {
1180
1448
  }, [debug, logger]);
1181
1449
  useEffect(() => {
1182
1450
  return () => {
1183
- sdkRef.current?.feedManager.destroy();
1184
- sdkRef.current?.playerEngine.destroy();
1185
- sdkRef.current?.resourceGovernor.destroy();
1186
- sdkRef.current?.optimisticManager.destroy();
1451
+ prevInstanceRef.current?.feedManager.destroy();
1452
+ prevInstanceRef.current?.playerEngine.destroy();
1453
+ prevInstanceRef.current?.resourceGovernor.destroy();
1454
+ prevInstanceRef.current?.optimisticManager.destroy();
1455
+ prevInstanceRef.current?.navigationManager.destroy();
1187
1456
  };
1188
1457
  }, []);
1189
1458
  return /* @__PURE__ */ jsx(SDKContext.Provider, { value, children });
@@ -1330,10 +1599,11 @@ var ACTIVE_HLS_DEFAULTS = {
1330
1599
  maxMaxBufferLength: 15,
1331
1600
  capLevelToPlayerSize: true,
1332
1601
  startLevel: 0,
1333
- abrEwmaDefaultEstimate: 5e5,
1602
+ abrEwmaDefaultEstimate: 2e6,
1334
1603
  lowLatencyMode: false,
1335
1604
  backBufferLength: 5,
1336
- enableWorker: true
1605
+ enableWorker: true,
1606
+ startFragPrefetch: true
1337
1607
  };
1338
1608
  var HOT_HLS_DEFAULTS = {
1339
1609
  maxBufferLength: 2,
@@ -1686,25 +1956,34 @@ var PLAY_AHEAD_MAX_CONCURRENT = 2;
1686
1956
  var PLAY_AHEAD_STAGGER_MS = 80;
1687
1957
  var _playAheadActive = 0;
1688
1958
  var _playAheadQueue = [];
1959
+ var _tokenCounter = 0;
1960
+ var _releasedTokens = /* @__PURE__ */ new Set();
1689
1961
  function acquirePlayAhead() {
1962
+ const token = ++_tokenCounter;
1963
+ const makeRelease = () => {
1964
+ let released = false;
1965
+ return () => {
1966
+ if (released) return;
1967
+ released = true;
1968
+ _releasedTokens.add(token);
1969
+ _playAheadActive = Math.max(0, _playAheadActive - 1);
1970
+ const next = _playAheadQueue.shift();
1971
+ if (next) {
1972
+ next();
1973
+ }
1974
+ };
1975
+ };
1690
1976
  if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
1691
1977
  _playAheadActive++;
1692
- return Promise.resolve();
1978
+ return Promise.resolve(makeRelease());
1693
1979
  }
1694
1980
  return new Promise((resolve) => {
1695
1981
  _playAheadQueue.push(() => {
1696
- setTimeout(resolve, PLAY_AHEAD_STAGGER_MS);
1982
+ _playAheadActive++;
1983
+ setTimeout(() => resolve(makeRelease()), PLAY_AHEAD_STAGGER_MS);
1697
1984
  });
1698
1985
  });
1699
1986
  }
1700
- function releasePlayAhead() {
1701
- _playAheadActive = Math.max(0, _playAheadActive - 1);
1702
- const next = _playAheadQueue.shift();
1703
- if (next) {
1704
- _playAheadActive++;
1705
- next();
1706
- }
1707
- }
1708
1987
  function VideoSlot({
1709
1988
  item,
1710
1989
  index,
@@ -1827,6 +2106,56 @@ function VideoSlotInner({
1827
2106
  }
1828
2107
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1829
2108
  const isReady = isHlsSource ? hlsReady : mp4Ready;
2109
+ const [capturedPoster, setCapturedPoster] = useState(null);
2110
+ useEffect(() => {
2111
+ const video = videoRef.current;
2112
+ if (!video || !shouldLoadSrc) return;
2113
+ let cancelled = false;
2114
+ const captureFrame = () => {
2115
+ if (cancelled) return;
2116
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
2117
+ if (video.videoWidth === 0 || video.videoHeight === 0) return;
2118
+ try {
2119
+ const canvas = document.createElement("canvas");
2120
+ canvas.width = video.videoWidth;
2121
+ canvas.height = video.videoHeight;
2122
+ const ctx = canvas.getContext("2d");
2123
+ if (!ctx) return;
2124
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2125
+ const dataUrl = canvas.toDataURL("image/webp", 0.85);
2126
+ if (!cancelled) {
2127
+ setCapturedPoster(dataUrl);
2128
+ }
2129
+ } catch {
2130
+ }
2131
+ };
2132
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
2133
+ captureFrame();
2134
+ } else {
2135
+ video.addEventListener("loadeddata", captureFrame, { once: true });
2136
+ }
2137
+ return () => {
2138
+ cancelled = true;
2139
+ video.removeEventListener("loadeddata", captureFrame);
2140
+ };
2141
+ }, [src, shouldLoadSrc]);
2142
+ useEffect(() => {
2143
+ setCapturedPoster(null);
2144
+ }, [src]);
2145
+ const [isVideoPlaying, setIsVideoPlaying] = useState(false);
2146
+ useEffect(() => {
2147
+ const video = videoRef.current;
2148
+ if (!video || !isActive) {
2149
+ setIsVideoPlaying(false);
2150
+ return;
2151
+ }
2152
+ const onPlaying = () => setIsVideoPlaying(true);
2153
+ video.addEventListener("playing", onPlaying);
2154
+ return () => {
2155
+ video.removeEventListener("playing", onPlaying);
2156
+ setIsVideoPlaying(false);
2157
+ };
2158
+ }, [isActive]);
1830
2159
  const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
1831
2160
  useEffect(() => {
1832
2161
  const video = videoRef.current;
@@ -1836,36 +2165,43 @@ function VideoSlotInner({
1836
2165
  const prevMuted = video.muted;
1837
2166
  video.muted = true;
1838
2167
  let cancelled = false;
2168
+ let release = null;
1839
2169
  const doPlayAhead = async () => {
1840
- await acquirePlayAhead();
2170
+ release = await acquirePlayAhead();
2171
+ if (cancelled) {
2172
+ release();
2173
+ return;
2174
+ }
1841
2175
  try {
1842
2176
  await video.play();
1843
2177
  if (cancelled) {
1844
2178
  video.pause();
1845
- releasePlayAhead();
2179
+ release();
1846
2180
  return;
1847
2181
  }
1848
2182
  const pauseAfterDecode = () => {
1849
2183
  video.pause();
1850
2184
  video.currentTime = 0;
1851
2185
  video.muted = prevMuted;
1852
- releasePlayAhead();
2186
+ release?.();
1853
2187
  if (!cancelled) {
1854
2188
  setHasPlayedAhead(true);
1855
2189
  }
1856
2190
  };
1857
2191
  setTimeout(pauseAfterDecode, 50);
1858
2192
  } catch {
1859
- releasePlayAhead();
2193
+ release?.();
1860
2194
  }
1861
2195
  };
1862
2196
  doPlayAhead();
1863
2197
  return () => {
1864
2198
  cancelled = true;
2199
+ release?.();
1865
2200
  };
1866
2201
  }, [isActive, isReady, hasPlayedAhead]);
1867
2202
  useEffect(() => {
1868
2203
  setHasPlayedAhead(false);
2204
+ setIsVideoPlaying(false);
1869
2205
  }, [src]);
1870
2206
  const wasActiveRef = useRef(false);
1871
2207
  const [isManuallyPaused, setIsManuallyPaused] = useState(false);
@@ -1942,7 +2278,7 @@ function VideoSlotInner({
1942
2278
  if (!video) return;
1943
2279
  video.muted = isActive ? isMuted : true;
1944
2280
  }, [isMuted, isActive]);
1945
- const showPosterOverlay = !isReady && !hasPlayedAhead;
2281
+ const showPosterOverlay = isActive ? !isVideoPlaying : !isReady && !hasPlayedAhead;
1946
2282
  const isPreDecoded = hasPlayedAhead;
1947
2283
  const [showMuteIndicator, setShowMuteIndicator] = useState(false);
1948
2284
  const muteIndicatorTimer = useRef(null);
@@ -2060,23 +2396,23 @@ function VideoSlotInner({
2060
2396
  height: "100%",
2061
2397
  objectFit: "cover",
2062
2398
  // Hide video until ready to avoid black frame flash.
2063
- // When pre-decoded, skip transition — first frame is already on canvas.
2399
+ // When pre-decoded or active, skip transition — first frame is already on canvas or playing.
2064
2400
  opacity: showPosterOverlay ? 0 : 1,
2065
- transition: isPreDecoded ? "none" : "opacity 0.15s ease"
2401
+ transition: isActive ? "none" : isPreDecoded ? "none" : "opacity 0.15s ease"
2066
2402
  }
2067
2403
  }
2068
2404
  ),
2069
- item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
2405
+ (capturedPoster || item.poster) && !isPreDecoded && /* @__PURE__ */ jsx(
2070
2406
  "div",
2071
2407
  {
2072
2408
  style: {
2073
2409
  position: "absolute",
2074
2410
  inset: 0,
2075
- backgroundImage: `url(${item.poster})`,
2411
+ backgroundImage: `url(${capturedPoster || item.poster})`,
2076
2412
  backgroundSize: "cover",
2077
2413
  backgroundPosition: "center",
2078
2414
  opacity: showPosterOverlay ? 1 : 0,
2079
- transition: "opacity 0.15s ease",
2415
+ transition: isActive ? "none" : "opacity 0.15s ease",
2080
2416
  pointerEvents: "none"
2081
2417
  }
2082
2418
  }
@@ -2284,7 +2620,7 @@ function ReelsFeed({
2284
2620
  }, 16);
2285
2621
  };
2286
2622
  const observer = new MutationObserver(debouncedRebuild);
2287
- observer.observe(container, { childList: true, subtree: true });
2623
+ observer.observe(container, { childList: true, subtree: false });
2288
2624
  return () => {
2289
2625
  observer.disconnect();
2290
2626
  if (rebuildTimer !== null) clearTimeout(rebuildTimer);
@@ -2521,6 +2857,56 @@ function parsePxTranslateY(el) {
2521
2857
  if (!match || !match[1]) return 0;
2522
2858
  return Number.parseFloat(match[1]);
2523
2859
  }
2860
+ function useNavigationSelector(selector) {
2861
+ const { navigationManager } = useSDK();
2862
+ const selectorRef = useRef(selector);
2863
+ selectorRef.current = selector;
2864
+ const lastSnapshot = useRef(void 0);
2865
+ const lastState = useRef(void 0);
2866
+ const getSnapshot = useCallback(() => {
2867
+ const state = navigationManager.store.getState();
2868
+ if (state !== lastState.current) {
2869
+ lastState.current = state;
2870
+ lastSnapshot.current = selectorRef.current(state);
2871
+ }
2872
+ return lastSnapshot.current;
2873
+ }, [navigationManager]);
2874
+ return useSyncExternalStore(
2875
+ navigationManager.store.subscribe,
2876
+ getSnapshot,
2877
+ getSnapshot
2878
+ );
2879
+ }
2880
+ function useNavigation() {
2881
+ const { navigationManager } = useSDK();
2882
+ const selectPhase = useCallback((s) => s.phase, []);
2883
+ const selectOpenIndex = useCallback((s) => s.openIndex, []);
2884
+ const phase = useNavigationSelector(selectPhase);
2885
+ const openIndex = useNavigationSelector(selectOpenIndex);
2886
+ const isOpen = phase === "open" || phase === "opening";
2887
+ const shouldMount = phase !== "closed";
2888
+ const open = useCallback(
2889
+ (index) => navigationManager.open(index),
2890
+ [navigationManager]
2891
+ );
2892
+ const close = useCallback(
2893
+ () => navigationManager.close(),
2894
+ [navigationManager]
2895
+ );
2896
+ const setPhase = useCallback(
2897
+ (next) => navigationManager.setPhase(next),
2898
+ [navigationManager]
2899
+ );
2900
+ return {
2901
+ phase,
2902
+ openIndex,
2903
+ isOpen,
2904
+ shouldMount,
2905
+ open,
2906
+ close,
2907
+ setPhase
2908
+ };
2909
+ }
2524
2910
  function ReelsFeedThumbnail({
2525
2911
  renderThumbnail,
2526
2912
  onThumbnailClick,
@@ -2530,33 +2916,48 @@ function ReelsFeedThumbnail({
2530
2916
  className = "grid grid-cols-2 gap-3",
2531
2917
  wrap = true,
2532
2918
  setFocusOnClick = true,
2919
+ openOnClick = false,
2533
2920
  prefetchOnHover = false,
2921
+ prewarmOnClick = true,
2534
2922
  getKey
2535
2923
  }) {
2536
2924
  const { items, loading, error, refresh } = useFeed();
2537
2925
  const { setFocusedIndexImmediate } = useResource();
2926
+ const { open } = useNavigation();
2538
2927
  const { adapters } = useSDK();
2539
2928
  const prefetchedRef = useRef(/* @__PURE__ */ new Set());
2929
+ const prewarm = useCallback(
2930
+ (item) => {
2931
+ if (!isVideoItem(item)) return;
2932
+ if (item.source.type !== "hls") return;
2933
+ const url = item.source.url;
2934
+ if (prefetchedRef.current.has(url)) return;
2935
+ prefetchedRef.current.add(url);
2936
+ adapters.videoLoader?.preloadMetadata?.(url);
2937
+ },
2938
+ [adapters.videoLoader]
2939
+ );
2540
2940
  const handleClick = useCallback(
2541
2941
  (id, item, index) => {
2942
+ if (prewarmOnClick) {
2943
+ prewarm(item);
2944
+ }
2542
2945
  if (setFocusOnClick) {
2543
2946
  setFocusedIndexImmediate(index);
2544
2947
  }
2948
+ if (openOnClick) {
2949
+ open(index);
2950
+ }
2545
2951
  onThumbnailClick?.(id, item, index);
2546
2952
  },
2547
- [setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
2953
+ [prewarmOnClick, prewarm, setFocusOnClick, setFocusedIndexImmediate, openOnClick, open, onThumbnailClick]
2548
2954
  );
2549
2955
  const handlePointerEnter = useCallback(
2550
2956
  (item) => {
2551
2957
  if (!prefetchOnHover) return;
2552
- if (!isVideoItem(item)) return;
2553
- if (item.source.type !== "hls") return;
2554
- const url = item.source.url;
2555
- if (prefetchedRef.current.has(url)) return;
2556
- prefetchedRef.current.add(url);
2557
- adapters.videoLoader?.preloadMetadata?.(url);
2958
+ prewarm(item);
2558
2959
  },
2559
- [prefetchOnHover, adapters.videoLoader]
2960
+ [prefetchOnHover, prewarm]
2560
2961
  );
2561
2962
  if (loading && items.length === 0) {
2562
2963
  if (renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
@@ -2596,6 +2997,309 @@ function ReelsFeedThumbnail({
2596
2997
  }
2597
2998
  return /* @__PURE__ */ jsx("div", { className, children: content });
2598
2999
  }
3000
+ var DEFAULT_DURATION = 300;
3001
+ var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
3002
+ var DEFAULT_DIRECTION = "up";
3003
+ var DEFAULT_Z_INDEX = 1e3;
3004
+ var DEFAULT_PREWARM_FORWARD = 2;
3005
+ function offscreenTransform(direction) {
3006
+ switch (direction) {
3007
+ case "down":
3008
+ return "translateY(-100%)";
3009
+ case "fade":
3010
+ return "translateY(0)";
3011
+ case "up":
3012
+ default:
3013
+ return "translateY(100%)";
3014
+ }
3015
+ }
3016
+ function prefersReducedMotion() {
3017
+ if (typeof window === "undefined" || !window.matchMedia) return false;
3018
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3019
+ }
3020
+ var FOCUSABLE = 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
3021
+ function ReelsModal({
3022
+ feedProps,
3023
+ animationConfig,
3024
+ renderBackdrop,
3025
+ renderCloseButton,
3026
+ onOpen,
3027
+ onClose,
3028
+ closeOnBackdropClick = true,
3029
+ closeOnEscape = true,
3030
+ lockBodyScroll = true,
3031
+ prewarmForward = DEFAULT_PREWARM_FORWARD,
3032
+ className,
3033
+ zIndex = DEFAULT_Z_INDEX,
3034
+ portalTarget
3035
+ }) {
3036
+ const { phase, openIndex, shouldMount, close, setPhase } = useNavigation();
3037
+ const { setFocusedIndexImmediate } = useResource();
3038
+ const { items } = useFeed();
3039
+ const { adapters } = useSDK();
3040
+ const duration = animationConfig?.duration ?? DEFAULT_DURATION;
3041
+ const easing = animationConfig?.easing ?? DEFAULT_EASING;
3042
+ const direction = animationConfig?.direction ?? DEFAULT_DIRECTION;
3043
+ const backdropRef = useRef(null);
3044
+ const panelRef = useRef(null);
3045
+ const animationsRef = useRef([]);
3046
+ const previouslyFocusedRef = useRef(null);
3047
+ const renderState = useMemo(
3048
+ () => ({ phase, openIndex, close }),
3049
+ [phase, openIndex, close]
3050
+ );
3051
+ const prewarmedRef = useRef(null);
3052
+ useEffect(() => {
3053
+ if (phase !== "opening") {
3054
+ if (phase === "closed") prewarmedRef.current = null;
3055
+ return;
3056
+ }
3057
+ if (openIndex == null) return;
3058
+ if (prewarmedRef.current === openIndex) return;
3059
+ prewarmedRef.current = openIndex;
3060
+ setFocusedIndexImmediate(openIndex);
3061
+ const preload = adapters.videoLoader?.preloadMetadata;
3062
+ if (!preload) return;
3063
+ const targets = /* @__PURE__ */ new Set();
3064
+ targets.add(openIndex);
3065
+ for (let d = 1; d <= prewarmForward; d++) targets.add(openIndex + d);
3066
+ targets.add(openIndex - 1);
3067
+ for (const idx of targets) {
3068
+ if (idx < 0 || idx >= items.length) continue;
3069
+ const item = items[idx];
3070
+ if (item && isVideoItem(item) && item.source.type === "hls") {
3071
+ preload(item.source.url);
3072
+ }
3073
+ }
3074
+ }, [phase, openIndex, items, adapters.videoLoader, prewarmForward, setFocusedIndexImmediate]);
3075
+ useEffect(() => {
3076
+ if (!lockBodyScroll || !shouldMount) return;
3077
+ if (typeof document === "undefined") return;
3078
+ const prev = document.body.style.overflow;
3079
+ document.body.style.overflow = "hidden";
3080
+ return () => {
3081
+ document.body.style.overflow = prev;
3082
+ };
3083
+ }, [lockBodyScroll, shouldMount]);
3084
+ const cancelAnimations = useCallback(() => {
3085
+ for (const a of animationsRef.current) a.cancel();
3086
+ animationsRef.current = [];
3087
+ }, []);
3088
+ useLayoutEffect(() => {
3089
+ const panel = panelRef.current;
3090
+ const backdrop = backdropRef.current;
3091
+ if (!panel) return;
3092
+ if (phase === "open" || phase === "closed") return;
3093
+ const reduce = prefersReducedMotion();
3094
+ const enter = phase === "opening";
3095
+ const offscreen2 = offscreenTransform(direction);
3096
+ const panelFrames = direction === "fade" ? enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }] : enter ? [{ transform: offscreen2 }, { transform: "translateY(0)" }] : [{ transform: "translateY(0)" }, { transform: offscreen2 }];
3097
+ const backdropFrames = enter ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }];
3098
+ const animDuration = reduce ? 0 : duration;
3099
+ let cancelled = false;
3100
+ cancelAnimations();
3101
+ const anims = [];
3102
+ const panelAnim = panel.animate(panelFrames, {
3103
+ duration: animDuration,
3104
+ easing,
3105
+ fill: "forwards"
3106
+ });
3107
+ anims.push(panelAnim);
3108
+ if (backdrop) {
3109
+ anims.push(
3110
+ backdrop.animate(backdropFrames, {
3111
+ duration: animDuration,
3112
+ easing,
3113
+ fill: "forwards"
3114
+ })
3115
+ );
3116
+ }
3117
+ animationsRef.current = anims;
3118
+ const finalize = () => {
3119
+ if (cancelled) return;
3120
+ if (enter) {
3121
+ if (direction === "fade") panel.style.opacity = "1";
3122
+ else panel.style.transform = "translateY(0)";
3123
+ if (backdrop) backdrop.style.opacity = "1";
3124
+ } else {
3125
+ if (direction === "fade") panel.style.opacity = "0";
3126
+ else panel.style.transform = offscreen2;
3127
+ if (backdrop) backdrop.style.opacity = "0";
3128
+ }
3129
+ for (const a of anims) a.cancel();
3130
+ animationsRef.current = [];
3131
+ setPhase(enter ? "open" : "closed");
3132
+ };
3133
+ panelAnim.addEventListener("finish", finalize);
3134
+ return () => {
3135
+ cancelled = true;
3136
+ panelAnim.removeEventListener("finish", finalize);
3137
+ cancelAnimations();
3138
+ };
3139
+ }, [phase, direction, duration, easing, setPhase, cancelAnimations]);
3140
+ const prevPhaseRef = useRef("closed");
3141
+ useEffect(() => {
3142
+ const prev = prevPhaseRef.current;
3143
+ if (prev !== "open" && phase === "open") onOpen?.(openIndex);
3144
+ if (prev !== "closed" && phase === "closed") onClose?.();
3145
+ prevPhaseRef.current = phase;
3146
+ }, [phase, openIndex, onOpen, onClose]);
3147
+ useEffect(() => {
3148
+ if (phase === "opening" && typeof document !== "undefined") {
3149
+ previouslyFocusedRef.current = document.activeElement;
3150
+ const id = requestAnimationFrame(() => {
3151
+ const panel = panelRef.current;
3152
+ if (!panel) return;
3153
+ const first = panel.querySelector(FOCUSABLE);
3154
+ (first ?? panel).focus();
3155
+ });
3156
+ return () => cancelAnimationFrame(id);
3157
+ }
3158
+ if (phase === "closed") {
3159
+ previouslyFocusedRef.current?.focus?.();
3160
+ previouslyFocusedRef.current = null;
3161
+ }
3162
+ return void 0;
3163
+ }, [phase]);
3164
+ const handleKeyDown = useCallback(
3165
+ (e) => {
3166
+ if (closeOnEscape && e.key === "Escape") {
3167
+ e.stopPropagation();
3168
+ close();
3169
+ return;
3170
+ }
3171
+ if (e.key !== "Tab") return;
3172
+ const panel = panelRef.current;
3173
+ if (!panel) return;
3174
+ const focusables = Array.from(
3175
+ panel.querySelectorAll(FOCUSABLE)
3176
+ ).filter((el) => el.offsetParent !== null);
3177
+ if (focusables.length === 0) {
3178
+ e.preventDefault();
3179
+ panel.focus();
3180
+ return;
3181
+ }
3182
+ const first = focusables[0];
3183
+ const last = focusables[focusables.length - 1];
3184
+ const active = document.activeElement;
3185
+ if (e.shiftKey && active === first) {
3186
+ e.preventDefault();
3187
+ last.focus();
3188
+ } else if (!e.shiftKey && active === last) {
3189
+ e.preventDefault();
3190
+ first.focus();
3191
+ }
3192
+ },
3193
+ [closeOnEscape, close]
3194
+ );
3195
+ const handleBackdropClick = useCallback(() => {
3196
+ if (closeOnBackdropClick) close();
3197
+ }, [closeOnBackdropClick, close]);
3198
+ if (!shouldMount) return null;
3199
+ if (typeof document === "undefined") return null;
3200
+ const target = portalTarget ?? document.body;
3201
+ if (!target) return null;
3202
+ const enterStart = phase === "opening";
3203
+ const offscreen = offscreenTransform(direction);
3204
+ const panelInitialTransform = direction === "fade" ? "translateY(0)" : enterStart ? offscreen : "translateY(0)";
3205
+ const panelInitialOpacity = direction === "fade" ? enterStart ? 0 : 1 : 1;
3206
+ const backdropInitialOpacity = enterStart ? 0 : 1;
3207
+ const rootStyle = {
3208
+ position: "fixed",
3209
+ inset: 0,
3210
+ zIndex,
3211
+ // While opening, the panel is parked off-screen but MUST stay painted so
3212
+ // the <video> decodes its first frame. overflow:hidden keeps the
3213
+ // off-screen panel from creating scrollbars / capturing stray taps.
3214
+ overflow: "hidden"
3215
+ };
3216
+ const backdropStyle = {
3217
+ position: "absolute",
3218
+ inset: 0,
3219
+ opacity: backdropInitialOpacity,
3220
+ background: renderBackdrop ? "transparent" : "rgba(0,0,0,0.85)",
3221
+ // willChange hints the compositor for the opacity fade.
3222
+ willChange: "opacity"
3223
+ };
3224
+ const panelStyle = {
3225
+ position: "absolute",
3226
+ inset: 0,
3227
+ transform: panelInitialTransform,
3228
+ opacity: panelInitialOpacity,
3229
+ willChange: direction === "fade" ? "opacity" : "transform",
3230
+ outline: "none"
3231
+ };
3232
+ return createPortal(
3233
+ /* @__PURE__ */ jsxs(
3234
+ "div",
3235
+ {
3236
+ style: rootStyle,
3237
+ onKeyDown: handleKeyDown,
3238
+ "data-reels-modal-phase": phase,
3239
+ children: [
3240
+ /* @__PURE__ */ jsx(
3241
+ "div",
3242
+ {
3243
+ ref: backdropRef,
3244
+ style: backdropStyle,
3245
+ onClick: handleBackdropClick,
3246
+ "aria-hidden": "true",
3247
+ "data-reels-modal-backdrop": true,
3248
+ children: renderBackdrop ? renderBackdrop(renderState) : null
3249
+ }
3250
+ ),
3251
+ /* @__PURE__ */ jsxs(
3252
+ "div",
3253
+ {
3254
+ ref: panelRef,
3255
+ role: "dialog",
3256
+ "aria-modal": "true",
3257
+ "aria-label": "Reels viewer",
3258
+ tabIndex: -1,
3259
+ className,
3260
+ style: panelStyle,
3261
+ "data-reels-modal-panel": true,
3262
+ children: [
3263
+ /* @__PURE__ */ jsx(ReelsFeed, { ...feedProps }),
3264
+ renderCloseButton ? renderCloseButton(renderState) : /* @__PURE__ */ jsx(DefaultCloseButton, { onClose: close })
3265
+ ]
3266
+ }
3267
+ )
3268
+ ]
3269
+ }
3270
+ ),
3271
+ target
3272
+ );
3273
+ }
3274
+ function DefaultCloseButton({ onClose }) {
3275
+ return /* @__PURE__ */ jsx(
3276
+ "button",
3277
+ {
3278
+ type: "button",
3279
+ onClick: onClose,
3280
+ "aria-label": "Close reels viewer",
3281
+ style: {
3282
+ position: "absolute",
3283
+ top: "max(12px, env(safe-area-inset-top))",
3284
+ right: 12,
3285
+ width: 40,
3286
+ height: 40,
3287
+ display: "flex",
3288
+ alignItems: "center",
3289
+ justifyContent: "center",
3290
+ borderRadius: "50%",
3291
+ border: "none",
3292
+ background: "rgba(0,0,0,0.45)",
3293
+ color: "#fff",
3294
+ fontSize: 22,
3295
+ lineHeight: 1,
3296
+ cursor: "pointer",
3297
+ zIndex: 2
3298
+ },
3299
+ children: "\u2715"
3300
+ }
3301
+ );
3302
+ }
2599
3303
  function usePlayerSelector(selector) {
2600
3304
  const { playerEngine } = useSDK();
2601
3305
  const selectorRef = useRef(selector);
@@ -3210,4 +3914,4 @@ var HttpError = class extends Error {
3210
3914
  }
3211
3915
  };
3212
3916
 
3213
- export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
3917
+ export { DEFAULT_FEED_CONFIG, DEFAULT_NAVIGATION_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, NavigationManager, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsModal, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidNavTransition, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, useNavigation, useNavigationSelector, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };