@xhub-reels/sdk 0.1.14 → 0.1.15

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
@@ -1659,6 +1659,43 @@ function skeletonCircle(size) {
1659
1659
  background: "rgba(255,255,255,0.1)"
1660
1660
  };
1661
1661
  }
1662
+ function DefaultDoubleTap({ isDoubleTap }) {
1663
+ if (!isDoubleTap) return null;
1664
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1665
+ "div",
1666
+ {
1667
+ style: {
1668
+ position: "absolute",
1669
+ inset: 0,
1670
+ display: "flex",
1671
+ alignItems: "center",
1672
+ justifyContent: "center",
1673
+ pointerEvents: "none",
1674
+ zIndex: 20
1675
+ },
1676
+ children: [
1677
+ /* @__PURE__ */ jsxRuntime.jsx(
1678
+ "div",
1679
+ {
1680
+ style: {
1681
+ fontSize: 80,
1682
+ animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1683
+ },
1684
+ children: "\u2764\uFE0F"
1685
+ }
1686
+ ),
1687
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1688
+ @keyframes reels-sdk-doubleTapHeart {
1689
+ 0% { opacity: 0; transform: scale(0.5); }
1690
+ 30% { opacity: 1; transform: scale(1.2); }
1691
+ 60% { opacity: 1; transform: scale(1.0); }
1692
+ 100% { opacity: 0; transform: scale(1.1); }
1693
+ }
1694
+ ` })
1695
+ ]
1696
+ }
1697
+ );
1698
+ }
1662
1699
  function VideoSlot({
1663
1700
  item,
1664
1701
  index,
@@ -1671,7 +1708,8 @@ function VideoSlot({
1671
1708
  showFps = false,
1672
1709
  renderOverlay,
1673
1710
  renderActions,
1674
- renderPauseIndicator
1711
+ renderPauseIndicator,
1712
+ renderDoubleTap
1675
1713
  }) {
1676
1714
  const { optimisticManager, adapters } = useSDK();
1677
1715
  if (!isVideoItem(item)) {
@@ -1708,6 +1746,7 @@ function VideoSlot({
1708
1746
  renderOverlay,
1709
1747
  renderActions,
1710
1748
  renderPauseIndicator,
1749
+ renderDoubleTap,
1711
1750
  optimisticManager,
1712
1751
  adapters
1713
1752
  }
@@ -1726,6 +1765,7 @@ function VideoSlotInner({
1726
1765
  renderOverlay,
1727
1766
  renderActions,
1728
1767
  renderPauseIndicator,
1768
+ renderDoubleTap,
1729
1769
  optimisticManager,
1730
1770
  adapters
1731
1771
  }) {
@@ -1944,18 +1984,44 @@ function VideoSlotInner({
1944
1984
  }, [isActive]);
1945
1985
  const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1946
1986
  const [isPaused, setIsPaused] = react.useState(false);
1987
+ const [isDoubleTap, setIsDoubleTap] = react.useState(false);
1988
+ const lastTapTimeRef = react.useRef(0);
1989
+ const doubleTapTimerRef = react.useRef(null);
1947
1990
  const handleTap = react.useCallback(() => {
1948
- const video = videoRef.current;
1949
- if (!video || !isActive) return;
1950
- if (video.paused) {
1951
- video.play().catch(() => {
1952
- });
1953
- setIsPaused(false);
1991
+ if (!isActive) return;
1992
+ const now = Date.now();
1993
+ const delta = now - lastTapTimeRef.current;
1994
+ lastTapTimeRef.current = now;
1995
+ if (delta < 300) {
1996
+ if (doubleTapTimerRef.current !== null) {
1997
+ clearTimeout(doubleTapTimerRef.current);
1998
+ doubleTapTimerRef.current = null;
1999
+ }
2000
+ setIsDoubleTap(true);
2001
+ setTimeout(() => setIsDoubleTap(false), 600);
1954
2002
  } else {
1955
- video.pause();
1956
- setIsPaused(true);
2003
+ doubleTapTimerRef.current = setTimeout(() => {
2004
+ doubleTapTimerRef.current = null;
2005
+ const video = videoRef.current;
2006
+ if (!video) return;
2007
+ if (video.paused) {
2008
+ video.play().catch(() => {
2009
+ });
2010
+ setIsPaused(false);
2011
+ } else {
2012
+ video.pause();
2013
+ setIsPaused(true);
2014
+ }
2015
+ }, 300);
1957
2016
  }
1958
2017
  }, [isActive]);
2018
+ react.useEffect(() => {
2019
+ return () => {
2020
+ if (doubleTapTimerRef.current !== null) {
2021
+ clearTimeout(doubleTapTimerRef.current);
2022
+ }
2023
+ };
2024
+ }, []);
1959
2025
  react.useEffect(() => {
1960
2026
  if (isActive) setIsPaused(false);
1961
2027
  }, [isActive]);
@@ -2029,6 +2095,18 @@ function VideoSlotInner({
2029
2095
  }
2030
2096
  }
2031
2097
  ),
2098
+ (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsxRuntime.jsx(
2099
+ "div",
2100
+ {
2101
+ style: {
2102
+ position: "absolute",
2103
+ inset: 0,
2104
+ pointerEvents: "none",
2105
+ zIndex: 20
2106
+ },
2107
+ children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsxRuntime.jsx(DefaultDoubleTap, { isDoubleTap })
2108
+ }
2109
+ ),
2032
2110
  isPaused && /* @__PURE__ */ jsxRuntime.jsx(
2033
2111
  "div",
2034
2112
  {
@@ -2152,6 +2230,7 @@ function ReelsFeed({
2152
2230
  renderOverlay,
2153
2231
  renderActions,
2154
2232
  renderPauseIndicator,
2233
+ renderDoubleTap,
2155
2234
  renderLoading,
2156
2235
  renderEmpty,
2157
2236
  renderError: _renderError,
@@ -2398,7 +2477,8 @@ function ReelsFeed({
2398
2477
  showFps: showFps && isActive,
2399
2478
  renderOverlay,
2400
2479
  renderActions,
2401
- renderPauseIndicator
2480
+ renderPauseIndicator,
2481
+ renderDoubleTap
2402
2482
  }
2403
2483
  ) : null
2404
2484
  },
@@ -3033,6 +3113,7 @@ exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
3033
3113
  exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
3034
3114
  exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
3035
3115
  exports.DefaultActions = DefaultActions;
3116
+ exports.DefaultDoubleTap = DefaultDoubleTap;
3036
3117
  exports.DefaultOverlay = DefaultOverlay;
3037
3118
  exports.DefaultSkeleton = DefaultSkeleton;
3038
3119
  exports.FeedManager = FeedManager;
package/dist/index.d.cts CHANGED
@@ -334,6 +334,13 @@ interface ReelsFeedProps {
334
334
  * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
335
  */
336
336
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
337
+ /**
338
+ * Custom double-tap overlay rendered when user double-taps a video slot.
339
+ * Receives isDoubleTap boolean (true for the duration of the animation window).
340
+ * Return ReactNode — positioned absolute center (full inset) by SDK.
341
+ * If not provided, SDK uses DefaultDoubleTap (animated ❤️ heart).
342
+ */
343
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
337
344
  /**
338
345
  * Custom loading skeleton shown during initial feed load.
339
346
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -724,7 +731,7 @@ interface ReelsProviderProps {
724
731
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
725
732
  declare function useSDK(): SDKContextValue;
726
733
 
727
- declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
734
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
728
735
 
729
736
  /**
730
737
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -801,8 +808,9 @@ interface VideoSlotProps {
801
808
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
802
809
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
810
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
811
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
804
812
  }
805
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
813
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
806
814
 
807
815
  declare function DefaultOverlay({ item }: {
808
816
  item: ContentItem;
@@ -820,6 +828,16 @@ declare function DefaultActions({ item, actions }: {
820
828
  */
821
829
  declare function DefaultSkeleton(): react_jsx_runtime.JSX.Element;
822
830
 
831
+ /**
832
+ * DefaultDoubleTap — Default double-tap heart animation overlay
833
+ *
834
+ * Shown when user double-taps a video slot and no renderDoubleTap is provided.
835
+ * Renders an animated ❤️ that fades in/scales up then fades out.
836
+ */
837
+ declare function DefaultDoubleTap({ isDoubleTap }: {
838
+ isDoubleTap: boolean;
839
+ }): react_jsx_runtime.JSX.Element | null;
840
+
823
841
  declare function useFeedSelector<T>(selector: (state: FeedState) => T): T;
824
842
  declare function useFeed(): {
825
843
  items: NonNullable<ContentItem | undefined>[];
@@ -1090,4 +1108,4 @@ declare class HttpError extends Error {
1090
1108
  constructor(status: number, message: string, body?: string | undefined);
1091
1109
  }
1092
1110
 
1093
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1111
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.d.ts CHANGED
@@ -334,6 +334,13 @@ interface ReelsFeedProps {
334
334
  * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
335
  */
336
336
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
337
+ /**
338
+ * Custom double-tap overlay rendered when user double-taps a video slot.
339
+ * Receives isDoubleTap boolean (true for the duration of the animation window).
340
+ * Return ReactNode — positioned absolute center (full inset) by SDK.
341
+ * If not provided, SDK uses DefaultDoubleTap (animated ❤️ heart).
342
+ */
343
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
337
344
  /**
338
345
  * Custom loading skeleton shown during initial feed load.
339
346
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -724,7 +731,7 @@ interface ReelsProviderProps {
724
731
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
725
732
  declare function useSDK(): SDKContextValue;
726
733
 
727
- declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
734
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
728
735
 
729
736
  /**
730
737
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -801,8 +808,9 @@ interface VideoSlotProps {
801
808
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
802
809
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
810
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
811
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
804
812
  }
805
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
813
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
806
814
 
807
815
  declare function DefaultOverlay({ item }: {
808
816
  item: ContentItem;
@@ -820,6 +828,16 @@ declare function DefaultActions({ item, actions }: {
820
828
  */
821
829
  declare function DefaultSkeleton(): react_jsx_runtime.JSX.Element;
822
830
 
831
+ /**
832
+ * DefaultDoubleTap — Default double-tap heart animation overlay
833
+ *
834
+ * Shown when user double-taps a video slot and no renderDoubleTap is provided.
835
+ * Renders an animated ❤️ that fades in/scales up then fades out.
836
+ */
837
+ declare function DefaultDoubleTap({ isDoubleTap }: {
838
+ isDoubleTap: boolean;
839
+ }): react_jsx_runtime.JSX.Element | null;
840
+
823
841
  declare function useFeedSelector<T>(selector: (state: FeedState) => T): T;
824
842
  declare function useFeed(): {
825
843
  items: NonNullable<ContentItem | undefined>[];
@@ -1090,4 +1108,4 @@ declare class HttpError extends Error {
1090
1108
  constructor(status: number, message: string, body?: string | undefined);
1091
1109
  }
1092
1110
 
1093
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1111
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.js CHANGED
@@ -1653,6 +1653,43 @@ function skeletonCircle(size) {
1653
1653
  background: "rgba(255,255,255,0.1)"
1654
1654
  };
1655
1655
  }
1656
+ function DefaultDoubleTap({ isDoubleTap }) {
1657
+ if (!isDoubleTap) return null;
1658
+ return /* @__PURE__ */ jsxs(
1659
+ "div",
1660
+ {
1661
+ style: {
1662
+ position: "absolute",
1663
+ inset: 0,
1664
+ display: "flex",
1665
+ alignItems: "center",
1666
+ justifyContent: "center",
1667
+ pointerEvents: "none",
1668
+ zIndex: 20
1669
+ },
1670
+ children: [
1671
+ /* @__PURE__ */ jsx(
1672
+ "div",
1673
+ {
1674
+ style: {
1675
+ fontSize: 80,
1676
+ animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1677
+ },
1678
+ children: "\u2764\uFE0F"
1679
+ }
1680
+ ),
1681
+ /* @__PURE__ */ jsx("style", { children: `
1682
+ @keyframes reels-sdk-doubleTapHeart {
1683
+ 0% { opacity: 0; transform: scale(0.5); }
1684
+ 30% { opacity: 1; transform: scale(1.2); }
1685
+ 60% { opacity: 1; transform: scale(1.0); }
1686
+ 100% { opacity: 0; transform: scale(1.1); }
1687
+ }
1688
+ ` })
1689
+ ]
1690
+ }
1691
+ );
1692
+ }
1656
1693
  function VideoSlot({
1657
1694
  item,
1658
1695
  index,
@@ -1665,7 +1702,8 @@ function VideoSlot({
1665
1702
  showFps = false,
1666
1703
  renderOverlay,
1667
1704
  renderActions,
1668
- renderPauseIndicator
1705
+ renderPauseIndicator,
1706
+ renderDoubleTap
1669
1707
  }) {
1670
1708
  const { optimisticManager, adapters } = useSDK();
1671
1709
  if (!isVideoItem(item)) {
@@ -1702,6 +1740,7 @@ function VideoSlot({
1702
1740
  renderOverlay,
1703
1741
  renderActions,
1704
1742
  renderPauseIndicator,
1743
+ renderDoubleTap,
1705
1744
  optimisticManager,
1706
1745
  adapters
1707
1746
  }
@@ -1720,6 +1759,7 @@ function VideoSlotInner({
1720
1759
  renderOverlay,
1721
1760
  renderActions,
1722
1761
  renderPauseIndicator,
1762
+ renderDoubleTap,
1723
1763
  optimisticManager,
1724
1764
  adapters
1725
1765
  }) {
@@ -1938,18 +1978,44 @@ function VideoSlotInner({
1938
1978
  }, [isActive]);
1939
1979
  const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1940
1980
  const [isPaused, setIsPaused] = useState(false);
1981
+ const [isDoubleTap, setIsDoubleTap] = useState(false);
1982
+ const lastTapTimeRef = useRef(0);
1983
+ const doubleTapTimerRef = useRef(null);
1941
1984
  const handleTap = useCallback(() => {
1942
- const video = videoRef.current;
1943
- if (!video || !isActive) return;
1944
- if (video.paused) {
1945
- video.play().catch(() => {
1946
- });
1947
- setIsPaused(false);
1985
+ if (!isActive) return;
1986
+ const now = Date.now();
1987
+ const delta = now - lastTapTimeRef.current;
1988
+ lastTapTimeRef.current = now;
1989
+ if (delta < 300) {
1990
+ if (doubleTapTimerRef.current !== null) {
1991
+ clearTimeout(doubleTapTimerRef.current);
1992
+ doubleTapTimerRef.current = null;
1993
+ }
1994
+ setIsDoubleTap(true);
1995
+ setTimeout(() => setIsDoubleTap(false), 600);
1948
1996
  } else {
1949
- video.pause();
1950
- setIsPaused(true);
1997
+ doubleTapTimerRef.current = setTimeout(() => {
1998
+ doubleTapTimerRef.current = null;
1999
+ const video = videoRef.current;
2000
+ if (!video) return;
2001
+ if (video.paused) {
2002
+ video.play().catch(() => {
2003
+ });
2004
+ setIsPaused(false);
2005
+ } else {
2006
+ video.pause();
2007
+ setIsPaused(true);
2008
+ }
2009
+ }, 300);
1951
2010
  }
1952
2011
  }, [isActive]);
2012
+ useEffect(() => {
2013
+ return () => {
2014
+ if (doubleTapTimerRef.current !== null) {
2015
+ clearTimeout(doubleTapTimerRef.current);
2016
+ }
2017
+ };
2018
+ }, []);
1953
2019
  useEffect(() => {
1954
2020
  if (isActive) setIsPaused(false);
1955
2021
  }, [isActive]);
@@ -2023,6 +2089,18 @@ function VideoSlotInner({
2023
2089
  }
2024
2090
  }
2025
2091
  ),
2092
+ (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsx(
2093
+ "div",
2094
+ {
2095
+ style: {
2096
+ position: "absolute",
2097
+ inset: 0,
2098
+ pointerEvents: "none",
2099
+ zIndex: 20
2100
+ },
2101
+ children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsx(DefaultDoubleTap, { isDoubleTap })
2102
+ }
2103
+ ),
2026
2104
  isPaused && /* @__PURE__ */ jsx(
2027
2105
  "div",
2028
2106
  {
@@ -2146,6 +2224,7 @@ function ReelsFeed({
2146
2224
  renderOverlay,
2147
2225
  renderActions,
2148
2226
  renderPauseIndicator,
2227
+ renderDoubleTap,
2149
2228
  renderLoading,
2150
2229
  renderEmpty,
2151
2230
  renderError: _renderError,
@@ -2392,7 +2471,8 @@ function ReelsFeed({
2392
2471
  showFps: showFps && isActive,
2393
2472
  renderOverlay,
2394
2473
  renderActions,
2395
- renderPauseIndicator
2474
+ renderPauseIndicator,
2475
+ renderDoubleTap
2396
2476
  }
2397
2477
  ) : null
2398
2478
  },
@@ -3023,4 +3103,4 @@ var HttpError = class extends Error {
3023
3103
  }
3024
3104
  };
3025
3105
 
3026
- export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
3106
+ export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",