react-motion-gallery 2.0.19 → 2.0.20

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.
Files changed (216) hide show
  1. package/LICENSE.md +13 -3
  2. package/README.md +677 -286
  3. package/THIRD_PARTY_NOTICES.md +31 -0
  4. package/dist/FullscreenRuntime-GNH22QHC.mjs +4 -0
  5. package/dist/FullscreenRuntime-QX6YELBN.css +1 -0
  6. package/dist/GridSkeleton-Dn7N-TEh.d.mts +23 -0
  7. package/dist/MasonrySkeleton-ftJdtDLs.d.mts +29 -0
  8. package/dist/chunk-3YENO5LQ.mjs +1 -0
  9. package/dist/{chunk-NABNX5HB.mjs → chunk-4NT4UVB5.mjs} +1 -1
  10. package/dist/chunk-4VHNCVVB.mjs +0 -0
  11. package/dist/chunk-6CJ7JFOA.mjs +1 -0
  12. package/dist/chunk-6FFRWH2C.mjs +1 -0
  13. package/dist/chunk-6ZNRPJC2.mjs +6 -0
  14. package/dist/chunk-ADIHG7AT.mjs +1 -0
  15. package/dist/chunk-ASH5AOA4.mjs +1 -0
  16. package/dist/chunk-AX2FSVFD.mjs +2 -0
  17. package/dist/chunk-B4CC5AGE.mjs +1 -0
  18. package/dist/chunk-BJBHSWMF.mjs +1 -0
  19. package/dist/chunk-C6PKH3FH.mjs +1 -0
  20. package/dist/chunk-CR2MZG3Q.mjs +1 -0
  21. package/dist/chunk-D3T6HIS2.mjs +1 -0
  22. package/dist/chunk-DCUCXQHE.mjs +3 -0
  23. package/dist/chunk-DCY4ZVYI.mjs +1 -0
  24. package/dist/chunk-EFXHC36P.mjs +0 -0
  25. package/dist/chunk-GUNIA4DZ.mjs +1 -0
  26. package/dist/chunk-H6XFG3CJ.mjs +1 -0
  27. package/dist/chunk-HGY3QLCE.mjs +1 -0
  28. package/dist/chunk-HK2DPKES.mjs +1 -0
  29. package/dist/chunk-IT7HWE4G.mjs +1 -0
  30. package/dist/chunk-J2ZX5I7E.mjs +5 -0
  31. package/dist/chunk-JJMFOLJZ.mjs +1 -0
  32. package/dist/chunk-L2HRIINV.mjs +1 -0
  33. package/dist/chunk-L4TRPKGX.mjs +4 -0
  34. package/dist/chunk-NHIKOJLU.mjs +1 -0
  35. package/dist/chunk-NQI246HG.mjs +1 -0
  36. package/dist/chunk-OHD2HQP7.mjs +4 -0
  37. package/dist/chunk-OWKZOHPK.mjs +3 -0
  38. package/dist/chunk-PFEGIWQJ.mjs +1 -0
  39. package/dist/chunk-Q5WUKZ2J.mjs +4 -0
  40. package/dist/chunk-R6EGYRTJ.mjs +6 -0
  41. package/dist/chunk-RLT5FULN.mjs +0 -0
  42. package/dist/chunk-RNLUNA5L.mjs +2 -0
  43. package/dist/chunk-STRS7UNJ.mjs +1 -0
  44. package/dist/chunk-TIQVSK5S.mjs +1 -0
  45. package/dist/chunk-UML6FCOQ.mjs +1 -0
  46. package/dist/chunk-UP6P6CQS.mjs +2 -0
  47. package/dist/chunk-UUAWLGWO.mjs +1 -0
  48. package/dist/chunk-V25YIPLC.mjs +1 -0
  49. package/dist/chunk-V7DPXRZF.mjs +1 -0
  50. package/dist/chunk-VEXMXZJM.mjs +4 -0
  51. package/dist/chunk-VGXO2IAF.mjs +1 -0
  52. package/dist/chunk-VWEQRZ24.mjs +1 -0
  53. package/dist/chunk-VXMW2JT5.mjs +1 -0
  54. package/dist/chunk-WGVWASZM.mjs +1 -0
  55. package/dist/chunk-WMG2LTLR.mjs +1 -0
  56. package/dist/chunk-X4HEGEZV.mjs +1 -0
  57. package/dist/chunk-XUQO5F2F.mjs +1 -0
  58. package/dist/chunk-Y7NUGXTR.mjs +1 -0
  59. package/dist/chunk-Z34PSRMG.mjs +1 -0
  60. package/dist/chunk-ZCCYTID7.mjs +1 -0
  61. package/dist/chunk-ZOFTC6YV.mjs +1 -0
  62. package/dist/core.d.mts +22 -22
  63. package/dist/core.mjs +1 -1
  64. package/dist/entries.css +1 -1
  65. package/dist/entries.d.mts +50 -17
  66. package/dist/entries.mjs +1 -1
  67. package/dist/force-C5m1QpdF.d.mts +7 -0
  68. package/dist/fullscreen-captions.css +1 -0
  69. package/dist/fullscreen-captions.d.mts +19 -0
  70. package/dist/fullscreen-captions.mjs +1 -0
  71. package/dist/fullscreen-controls.d.mts +19 -0
  72. package/dist/fullscreen-controls.mjs +1 -0
  73. package/dist/fullscreen-crossfade.d.mts +19 -0
  74. package/dist/fullscreen-crossfade.mjs +1 -0
  75. package/dist/fullscreen-lazy-load.css +1 -0
  76. package/dist/fullscreen-lazy-load.d.mts +19 -0
  77. package/dist/fullscreen-lazy-load.mjs +1 -0
  78. package/dist/fullscreen-slider.css +1 -0
  79. package/dist/fullscreen-slider.d.mts +19 -0
  80. package/dist/fullscreen-slider.mjs +1 -0
  81. package/dist/fullscreen-thumbnails.d.mts +19 -0
  82. package/dist/fullscreen-thumbnails.mjs +1 -0
  83. package/dist/fullscreen-video.css +1 -0
  84. package/dist/fullscreen-video.d.mts +19 -0
  85. package/dist/fullscreen-video.mjs +1 -0
  86. package/dist/fullscreen-zoom-pan.d.mts +19 -0
  87. package/dist/fullscreen-zoom-pan.mjs +1 -0
  88. package/dist/fullscreen.css +1 -1
  89. package/dist/fullscreen.d.mts +27 -67
  90. package/dist/fullscreen.mjs +1 -1
  91. package/dist/fullscreenThumbnails.css +1 -1
  92. package/dist/fullscreenThumbnails.d.mts +8 -8
  93. package/dist/fullscreenThumbnails.mjs +1 -1
  94. package/dist/grid-ready.d.mts +13 -0
  95. package/dist/grid-ready.mjs +1 -0
  96. package/dist/grid.css +1 -1
  97. package/dist/grid.d.mts +9 -29
  98. package/dist/grid.mjs +1 -1
  99. package/dist/index-Cp40fdvU.d.mts +21 -0
  100. package/dist/{index-CwwxTQKa.d.mts → index-YphJztdR.d.mts} +5 -6
  101. package/dist/index.css +1 -1
  102. package/dist/index.d.mts +26 -28
  103. package/dist/index.mjs +1 -1
  104. package/dist/layout-DoYnPD0I.d.mts +137 -0
  105. package/dist/lazy-dGoYpcRa.d.mts +14 -0
  106. package/dist/masonry-ready.d.mts +13 -0
  107. package/dist/masonry-ready.mjs +1 -0
  108. package/dist/masonry.css +1 -1
  109. package/dist/masonry.d.mts +15 -13
  110. package/dist/masonry.mjs +1 -1
  111. package/dist/{plyrTypes-Cq4C3ul5.d.mts → media.d.mts} +1 -8
  112. package/dist/media.mjs +1 -0
  113. package/dist/metafile-esm.json +1 -1
  114. package/dist/plyrTypes-DhzgHNfX.d.mts +9 -0
  115. package/dist/responsive-Ch5b4LC-.d.mts +526 -0
  116. package/dist/responsive.d.mts +15 -0
  117. package/dist/responsive.mjs +1 -0
  118. package/dist/responsiveNumber-CouEMJ9O.d.mts +5 -0
  119. package/dist/skeleton-base.css +1 -0
  120. package/dist/skeleton-base.d.mts +52 -0
  121. package/dist/skeleton-base.mjs +1 -0
  122. package/dist/skeleton-grid.css +1 -0
  123. package/dist/skeleton-grid.d.mts +64 -0
  124. package/dist/skeleton-grid.mjs +1 -0
  125. package/dist/skeleton-masonry.css +1 -0
  126. package/dist/skeleton-masonry.d.mts +63 -0
  127. package/dist/skeleton-masonry.mjs +1 -0
  128. package/dist/skeleton-slider.css +1 -0
  129. package/dist/skeleton-slider.d.mts +208 -0
  130. package/dist/skeleton-slider.mjs +40 -0
  131. package/dist/slider-arrows.d.mts +9 -0
  132. package/dist/slider-arrows.mjs +1 -0
  133. package/dist/slider-auto-height.d.mts +9 -0
  134. package/dist/slider-auto-height.mjs +1 -0
  135. package/dist/slider-auto-play.d.mts +9 -0
  136. package/dist/slider-auto-play.mjs +1 -0
  137. package/dist/slider-auto-scroll.d.mts +9 -0
  138. package/dist/slider-auto-scroll.mjs +1 -0
  139. package/dist/slider-crossfade.d.mts +9 -0
  140. package/dist/slider-crossfade.mjs +1 -0
  141. package/dist/slider-dots.css +1 -0
  142. package/dist/slider-dots.d.mts +9 -0
  143. package/dist/slider-dots.mjs +1 -0
  144. package/dist/slider-fade.d.mts +9 -0
  145. package/dist/slider-fade.mjs +1 -0
  146. package/dist/slider-fullscreen.d.mts +9 -0
  147. package/dist/slider-fullscreen.mjs +1 -0
  148. package/dist/slider-lazy-load.css +1 -0
  149. package/dist/slider-lazy-load.d.mts +9 -0
  150. package/dist/slider-lazy-load.mjs +1 -0
  151. package/dist/slider-loading.css +1 -0
  152. package/dist/slider-loading.d.mts +9 -0
  153. package/dist/slider-loading.mjs +1 -0
  154. package/dist/slider-parallax.d.mts +9 -0
  155. package/dist/slider-parallax.mjs +1 -0
  156. package/dist/slider-progress.d.mts +9 -0
  157. package/dist/slider-progress.mjs +1 -0
  158. package/dist/slider-ready.d.mts +14 -0
  159. package/dist/slider-ready.mjs +1 -0
  160. package/dist/slider-ripple.d.mts +9 -0
  161. package/dist/slider-ripple.mjs +1 -0
  162. package/dist/slider-scale.d.mts +9 -0
  163. package/dist/slider-scale.mjs +1 -0
  164. package/dist/slider-scrollbar.css +1 -0
  165. package/dist/slider-scrollbar.d.mts +9 -0
  166. package/dist/slider-scrollbar.mjs +1 -0
  167. package/dist/slider.css +1 -1
  168. package/dist/slider.d.mts +7 -81
  169. package/dist/slider.mjs +1 -1
  170. package/dist/text-BBcRGVzn.d.mts +10 -0
  171. package/dist/thumbnails.css +1 -1
  172. package/dist/thumbnails.d.mts +8 -11
  173. package/dist/thumbnails.mjs +1 -1
  174. package/dist/transitions-DU3ftmIq.d.mts +6 -0
  175. package/dist/{types-ROPjU8Nl.d.mts → types-B7AiQJkM.d.mts} +3 -2
  176. package/dist/types-BlFwyRVQ.d.mts +43 -0
  177. package/dist/types-CfvTYIyd.d.mts +450 -0
  178. package/dist/{types-CHUayqcj.d.mts → types-DP7ogmr4.d.mts} +5 -3
  179. package/dist/types-DWzjXjYR.d.mts +48 -0
  180. package/dist/video.css +1 -1
  181. package/dist/video.d.mts +2 -1
  182. package/dist/video.mjs +1 -1
  183. package/dist/zoomPan.d.mts +2 -4
  184. package/dist/zoomPan.mjs +1 -1
  185. package/package.json +135 -6
  186. package/dist/chunk-5HIHJGIV.mjs +0 -45
  187. package/dist/chunk-6TPHLAUP.mjs +0 -1
  188. package/dist/chunk-BIDZ4WZB.mjs +0 -2
  189. package/dist/chunk-DBIFLX6Y.mjs +0 -6
  190. package/dist/chunk-ECQ74X24.mjs +0 -1
  191. package/dist/chunk-FJYYM5TH.mjs +0 -1
  192. package/dist/chunk-GSEIEFRW.mjs +0 -1
  193. package/dist/chunk-GT6IL37J.mjs +0 -1
  194. package/dist/chunk-J4E4PKE5.mjs +0 -1
  195. package/dist/chunk-JD3VAF3N.mjs +0 -4
  196. package/dist/chunk-JMFDRKTX.mjs +0 -2
  197. package/dist/chunk-K6PQU6HF.mjs +0 -1
  198. package/dist/chunk-KSOQWCCL.mjs +0 -6
  199. package/dist/chunk-NEJ27O2B.mjs +0 -2
  200. package/dist/chunk-Q2PY6ZMU.mjs +0 -2
  201. package/dist/chunk-TKPLWDPW.mjs +0 -7
  202. package/dist/chunk-UV2SUN5V.mjs +0 -1
  203. package/dist/chunk-VXSRNAH4.mjs +0 -1
  204. package/dist/chunk-WLWVKQPL.mjs +0 -4
  205. package/dist/chunk-WZWMG4ZT.mjs +0 -1
  206. package/dist/chunk-XOS5AXSR.mjs +0 -4
  207. package/dist/chunk-ZX5E327W.mjs +0 -1
  208. package/dist/controls-SpWg1Kgt.d.mts +0 -44
  209. package/dist/layout-CR6f2aPH.d.mts +0 -95
  210. package/dist/responsive-D_xhZmVI.d.mts +0 -186
  211. package/dist/sliderSub-Bo6Y8as_.d.mts +0 -45
  212. package/dist/text-Cl2tR8oO.d.mts +0 -4
  213. package/dist/types-DY058l5M.d.mts +0 -301
  214. package/dist/types-VULXzSa2.d.mts +0 -68
  215. package/dist/types-XEr8LRal.d.mts +0 -65
  216. package/dist/types-_1D0QtfD.d.mts +0 -174
package/README.md CHANGED
@@ -1,24 +1,54 @@
1
1
  # React Motion Gallery
2
2
 
3
- Simple, motion-first React gallery primitives for sliders, grids, masonry layouts, fullscreen media, structured entries, and video. The package stays composable: `Slider`, `Grid`, and `Masonry` render children directly, `Entries` renders structured data, `GalleryCore` coordinates fullscreen state, and `Video` handles Plyr-backed video media.
3
+ Composable React media gallery primitives for production interfaces: sliders, grids, masonry, structured entries, fullscreen, thumbnails, video, zoom/pan, and loading states that are designed around the layout they protect.
4
4
 
5
- ## Export Gzip Sizes
5
+ The package stays close to React composition. `Slider`, `Grid`, and `Masonry` render children directly; `Entries` renders structured data; `GalleryCore` coordinates fullscreen state; `Video` handles Plyr-backed media; `ZoomPanImage` gives you a standalone zoom surface; and `Skeleton` can be used inside or outside gallery layouts. For loading-state precision, the repo also includes a development-time browser measurement workflow that turns real rendered text into stable skeleton text authoring data, including reflow-sensitive layouts such as masonry.
6
6
 
7
- This table reports local gzip measurements for each exported runtime surface. The script rebundles one runtime export at a time from the published root entry, excludes peer and runtime externals, and gzips the resulting JS bundle. Run `npm run build && npm run size:readme` in `packages/react-motion-gallery` to refresh it.
7
+ ## Runtime Gzip Sizes
8
+
9
+ This table reports local gzip measurements for selected runtime surfaces. Type-only imports are erased and add no JS; the responsive row measures `BREAKPOINT_MAP`, and feature subpath rows measure only that feature entry point. The script rebundles one export at a time from its published ESM entry point, excludes peer and runtime externals, and gzips the resulting JS bundle. If a surface creates async chunks, the row reports initial plus async JS. Run `npm run build && npm run size:readme` in `packages/react-motion-gallery` to refresh it.
8
10
 
9
11
  <!-- bundle-size:start -->
10
- | Export | JS gzip |
12
+ | Surface | JS gzip |
11
13
  | --- | --- |
12
- | `Entries` | 9.3kB |
13
- | `FullscreenThumbnailSlider` | 18.8kB |
14
- | `GalleryCore` | 2.5kB |
15
- | `Grid` | 13.8kB |
16
- | `Masonry` | 16.2kB |
17
- | `Slider` | 38.4kB |
18
- | `ThumbnailSlider` | 17.4kB |
19
- | `useFullscreenController` | 55.5kB |
20
- | `Video` | 12.2kB |
21
- | `ZoomPanImage` | 8.5kB |
14
+ | `Entries` | 13.0kB |
15
+ | `FullscreenThumbnailSlider` | 20.3kB |
16
+ | `GalleryCore` | 2.6kB |
17
+ | `Grid` | 8.8kB |
18
+ | `grid/ready` | 323.0B |
19
+ | `Masonry` | 8.9kB |
20
+ | `masonry/ready` | 323.0B |
21
+ | `Skeleton base` | 8.1kB |
22
+ | `skeleton/slider` | 16.5kB |
23
+ | `skeleton/grid` | 10.4kB |
24
+ | `skeleton/masonry` | 17.8kB |
25
+ | `Slider core` | 18.4kB |
26
+ | `slider/ready` | 894.0B |
27
+ | `slider/arrows` | 1.2kB |
28
+ | `slider/dots` | 932.0B |
29
+ | `slider/progress` | 892.0B |
30
+ | `slider/scrollbar` | 1.2kB |
31
+ | `slider/auto-height` | 1.3kB |
32
+ | `slider/lazy-load` | 3.9kB |
33
+ | `slider/parallax` | 1.4kB |
34
+ | `slider/scale` | 1.2kB |
35
+ | `slider/fade` | 1.2kB |
36
+ | `slider/crossfade` | 2.8kB |
37
+ | `slider/fullscreen` | 959.0B |
38
+ | `ThumbnailSlider` | 18.9kB |
39
+ | `useFullscreenController` | 4.9kB |
40
+ | `fullscreen/slider` | 35.3kB |
41
+ | `fullscreen/controls` | 173.0B |
42
+ | `fullscreen/captions` | 13.1kB |
43
+ | `fullscreen/zoom-pan` | 9.9kB |
44
+ | `fullscreen/video` | 16.3kB |
45
+ | `fullscreen/lazy-load` | 13.1kB |
46
+ | `fullscreen/crossfade` | 181.0B |
47
+ | `fullscreen/thumbnails` | 160.0B |
48
+ | `Video` | 12.7kB |
49
+ | `ZoomPanImage` | 8.7kB |
50
+ | `media / toMediaItems` | 260.0B |
51
+ | `responsive / BREAKPOINT_MAP` | 85.0B |
22
52
  <!-- bundle-size:end -->
23
53
 
24
54
  ## Overview
@@ -43,6 +73,7 @@ Mental model:
43
73
  - `GalleryCore` and `useFullscreenController` power fullscreen behavior.
44
74
  - `Video` is the gallery-ready video primitive.
45
75
  - `ZoomPanImage` attaches click-to-zoom, drag pan, ctrl-wheel pinch, and touch pinch to one clipped image surface.
76
+ - `Skeleton` renders standalone placeholders or wraps real content with shared loading-layer timing.
46
77
 
47
78
  `MediaItem` accepts three shapes:
48
79
 
@@ -54,7 +85,8 @@ Mental model:
54
85
 
55
86
  ```typescript
56
87
  import "react-motion-gallery/styles.css";
57
- import { Slider, toMediaItems, type MediaItem } from "react-motion-gallery";
88
+ import { toMediaItems, type MediaItem } from "react-motion-gallery/media";
89
+ import { Slider } from "react-motion-gallery/slider";
58
90
 
59
91
  const items: MediaItem[] = toMediaItems([
60
92
  "https://picsum.photos/id/1015/1600/900",
@@ -84,7 +116,93 @@ export function QuickStart() {
84
116
 
85
117
  Responsive numeric props in this package accept either a plain number or a breakpoint map like `{ 0: 1, md: 2, 1200: 3 }`. Named breakpoints resolve from the internal map: `xs: 0`, `sm: 600`, `md: 900`, `lg: 1200`, `xl: 1536`.
86
118
 
87
- The package root now exports the primary public components, helper functions, and companion prop types. Subpath entrypoints are also available when you want narrower imports: `react-motion-gallery/core`, `react-motion-gallery/slider`, `react-motion-gallery/grid`, `react-motion-gallery/masonry`, `react-motion-gallery/entries`, `react-motion-gallery/fullscreen`, `react-motion-gallery/thumbnails`, `react-motion-gallery/fullscreenThumbnails`, `react-motion-gallery/video`, and `react-motion-gallery/zoomPan`.
119
+ The package root exports the primary public components, helper functions, and companion prop types. Use it when one module needs several gallery surfaces. Prefer subpaths for routes or components that only need one surface, such as `react-motion-gallery/media` or `react-motion-gallery/slider`.
120
+
121
+ Subpaths give bundlers a smaller graph than the root. Less JS to transfer, parse, evaluate, and hydrate can improve first loads, cache misses, slower devices, and perceived speed.
122
+
123
+ | Entry point | Main surface |
124
+ | --- | --- |
125
+ | `react-motion-gallery/media` | `toMediaItems`, `MediaItem`, `MediaInput` |
126
+ | `react-motion-gallery/responsive` | `BREAKPOINT_MAP` and responsive value types |
127
+ | `react-motion-gallery/core` | `GalleryCore`, `GalleryCoreProvider`, `useGalleryCore` |
128
+ | `react-motion-gallery/slider` | `Slider`, `createSliderIndexChannel`, slider types |
129
+ | `react-motion-gallery/slider/ready` | `useSliderReady` |
130
+ | `react-motion-gallery/slider/arrows` | `sliderArrows` |
131
+ | `react-motion-gallery/slider/dots` | `sliderDots` |
132
+ | `react-motion-gallery/slider/progress` | `sliderProgress` |
133
+ | `react-motion-gallery/slider/scrollbar` | `sliderScrollbar` |
134
+ | `react-motion-gallery/slider/ripple` | `sliderRipple` |
135
+ | `react-motion-gallery/slider/auto-play` | `sliderAutoPlay` |
136
+ | `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll` |
137
+ | `react-motion-gallery/slider/auto-height` | `sliderAutoHeight` |
138
+ | `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad` |
139
+ | `react-motion-gallery/slider/parallax` | `sliderParallax` |
140
+ | `react-motion-gallery/slider/scale` | `sliderScale` |
141
+ | `react-motion-gallery/slider/fade` | `sliderFade` |
142
+ | `react-motion-gallery/slider/crossfade` | `sliderCrossfade` |
143
+ | `react-motion-gallery/slider/fullscreen` | `sliderFullscreen` |
144
+ | `react-motion-gallery/slider/loading` | `sliderLoading` |
145
+ | `react-motion-gallery/grid` | `Grid`, `Grid.Item`, grid types |
146
+ | `react-motion-gallery/grid/ready` | `useGridReady` |
147
+ | `react-motion-gallery/masonry` | `Masonry`, `Masonry.Item`, masonry types |
148
+ | `react-motion-gallery/masonry/ready` | `useMasonryReady` |
149
+ | `react-motion-gallery/entries` | `Entries`, `flattenEntries`, entry media container helpers |
150
+ | `react-motion-gallery/skeleton/base` | Standalone `Skeleton` and generic skeleton authoring types |
151
+ | `react-motion-gallery/skeleton/slider` | `SliderSkeleton` and slider skeleton authoring types |
152
+ | `react-motion-gallery/skeleton/grid` | `GridSkeleton` and grid skeleton authoring types |
153
+ | `react-motion-gallery/skeleton/masonry` | `MasonrySkeleton` and masonry skeleton authoring types |
154
+ | `react-motion-gallery/fullscreen` | `useFullscreenController` and fullscreen types |
155
+ | `react-motion-gallery/fullscreen/slider` | `fullscreenSlider` |
156
+ | `react-motion-gallery/fullscreen/controls` | `fullscreenControls` |
157
+ | `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions` |
158
+ | `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan` |
159
+ | `react-motion-gallery/fullscreen/video` | `fullscreenVideo` |
160
+ | `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad` |
161
+ | `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade` |
162
+ | `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails` |
163
+ | `react-motion-gallery/thumbnails` | `ThumbnailSlider`, thumbnail sync helpers |
164
+ | `react-motion-gallery/fullscreenThumbnails` | `FullscreenThumbnailSlider` |
165
+ | `react-motion-gallery/video` | `Video` and optional Plyr-backed video types |
166
+ | `react-motion-gallery/zoomPan` | `ZoomPanImage` and zoom/pan types |
167
+
168
+ ## Acknowledgements
169
+
170
+ React Motion Gallery's slider engine includes portions of code derived from [Embla Carousel](https://github.com/davidjerleke/embla-carousel), which is MIT licensed. Those portions have been substantially adapted for React Motion Gallery's React architecture, public API, transition system, fullscreen integration, loading layers, and media workflows.
171
+
172
+ See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md) for the preserved Embla Carousel copyright and MIT license notice.
173
+
174
+ ## Core
175
+
176
+ `GalleryCore` is the shared state boundary for fullscreen-aware galleries. Wrap a layout in it when you need shared breakpoints, a normalized fullscreen media list, fullscreen-open state, or programmatic fullscreen opening. `useGalleryCore()` is the public hook for reading that core state from descendants.
177
+
178
+ ### `GalleryCore` props
179
+
180
+ | Option | Type | Default | Notes |
181
+ | --- | --- | --- | --- |
182
+ | `children` | `React.ReactNode` | `—` | The gallery tree using the shared core. |
183
+ | `layout` | `"slider" \| "grid" \| "masonry" \| "entries"` | `—` | Declares the owning base layout. Omit it for standalone fullscreen/core usage. |
184
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Breakpoint map shared with descendants. |
185
+ | `fullscreenItems` | `MediaItem[] \| string[]` | `[]` | Normalized fullscreen media list. |
186
+ | `nodes` | `ReactNode \| ReactNode[]` | `—` | Advanced initial node list used by the slider-backed imperative state. |
187
+
188
+ ### `useGalleryCore` API
189
+
190
+ `GalleryApi` is the public alias for `GalleryCoreApi`. It covers core fullscreen state and programmatic fullscreen opening. Slider item mutation lives on `SliderHandle` and `SliderApi`.
191
+
192
+ | Field / Method | Type | Notes |
193
+ | --- | --- | --- |
194
+ | `layout` | `"slider" \| "grid" \| "masonry" \| "entries" \| null` | Current owning layout, or `null` for standalone fullscreen/core usage. |
195
+ | `effectiveBreakpoints` | `Record<string, number>` | Breakpoint map after merging custom `GalleryCore.breakpoints` with defaults. |
196
+ | `normalizedItems` | `MediaItem[]` | Fullscreen item list normalized from `fullscreenItems`. |
197
+ | `fsEnabled` | `boolean` | `true` when a mounted fullscreen controller has enabled fullscreen behavior. |
198
+ | `setFsEnabled` | `(enabled: boolean) => void` | Enables or disables fullscreen behavior. Usually handled by `useFullscreenController`. |
199
+ | `isFullscreenOpen` | `boolean` | `true` while fullscreen is open. |
200
+ | `isFullscreenOpenRef` | `React.RefObject<boolean>` | Ref mirror for handlers that need the current fullscreen-open state. |
201
+ | `setFullscreenOpen` | `(open: boolean) => void` | Updates fullscreen-open state. Usually handled by the fullscreen runtime. |
202
+ | `openFullscreenAt` | `({ index, method?, event? }) => void` | Opens fullscreen at a normalized fullscreen item index. Pass the source event for scale-origin detection. |
203
+ | `notifyBaseVisibleIndex` | `(index: number) => void` | Emits the visible base media index for fullscreen lazy-load/prewarm coordination. |
204
+ | `notifyFsVisibleIndex` | `(index: number) => void` | Emits the active fullscreen index back to base media. |
205
+ | `registerExpandableImage` | `(index: number, node: HTMLElement \| null) => void` | Registers an origin surface for layoutless scale transitions. |
88
206
 
89
207
  ## ZoomPanImage
90
208
 
@@ -108,10 +226,68 @@ export function ZoomPanCard() {
108
226
 
109
227
  `ZoomPanImage` is the lightweight standalone zoom surface. The component root is the clipping container, so border radius, aspect ratio, and overflow all live on the same element.
110
228
 
229
+ ## Skeleton
230
+
231
+ ```typescript
232
+ import { Skeleton, type SkeletonNode } from "react-motion-gallery/skeleton/base";
233
+
234
+ const shellSkeleton: SkeletonNode = {
235
+ kind: "rect",
236
+ style: { width: "100%", height: 320 },
237
+ };
238
+
239
+ export function LoadingShell({ ready, children }: { ready: boolean; children: React.ReactNode }) {
240
+ return (
241
+ <Skeleton
242
+ layout={shellSkeleton}
243
+ ready={ready}
244
+ timing={{ exitMs: 520, minVisibleMs: 220 }}
245
+ force={false}
246
+ ariaLabel={ready ? undefined : "Loading content"}
247
+ >
248
+ {children}
249
+ </Skeleton>
250
+ );
251
+ }
252
+ ```
253
+
254
+ `Skeleton` can render a standalone placeholder by itself, or it can wrap real content and own the loading transition. Wrapper mode is enabled when `children` are provided.
255
+
256
+ | Option | Type | Default | Notes |
257
+ | --- | --- | --- | --- |
258
+ | `layout` | `SkeletonNode` | `—` | Structured placeholder layout tree. |
259
+ | `children` | `React.ReactNode` | `—` | Real content. When present, `Skeleton` renders content and loading layers. |
260
+ | `ready` | `boolean` | `false` | Reveals content and exits the skeleton once true. |
261
+ | `enabled` | `boolean` | `true` | Set false to render content immediately with no skeleton layer. |
262
+ | `force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `false` | Keeps the skeleton visible. Set `showContent: true` to preview ready content under the skeleton, and tune the overlay with `skeletonOpacity`. |
263
+ | `timing.exitMs` | `number` | `600` | Keeps the skeleton layer mounted for this long after exit starts and controls the opacity transition. |
264
+ | `timing.minVisibleMs` | `number` | `220` | Minimum time the skeleton stays visible before exit can begin. |
265
+ | `shellClassName` / `shellStyle` | `string` / `CSSProperties` | `—` | Wrapper-layer class and style for content+skeleton mode. |
266
+ | `contentClassName` / `contentStyle` | `string` / `CSSProperties` | `—` | Content-layer class and style for wrapper mode. |
267
+
268
+ The wrapper timing model matches the gallery loading layers: content begins fading in as soon as the skeleton exit starts; it does not wait for the skeleton to unmount.
269
+
270
+ ### Browser-measured skeleton text authoring
271
+
272
+ Responsive text is one of the easiest places for a polished loading state to drift away from the real UI. React Motion Gallery's skeleton text workflow measures real DOM text in a live page with headless Chrome, then emits `lines`, `barWidth`, `lastBarWidth`, and optional `barHeight`/`lineHeight` values for the skeleton `text` nodes used by `Slider`, `Grid`, `Masonry`, `Entries`, and standalone `Skeleton` layouts.
273
+
274
+ This is development-time authoring support, not production client code. It is especially useful for multiline cards, responsive grids, equal-height sliders, and reflow-sensitive masonry surfaces where a generic text placeholder can otherwise change row height, item height, or column packing when real content appears.
275
+
276
+ ```bash
277
+ npm run --silent generate:skeleton-text-module -- \
278
+ --input ./path/to/example.skeleton-text.browser.manifest.json \
279
+ --analysis-output ./path/to/example.skeleton-text.measurements.json
280
+ ```
281
+
282
+ Use `responsiveBy: "container"` when text wrapping follows the card or cell width more closely than the viewport. For equal-height card sliders, the browser analyzer can also measure all canonical slider items and emit `rowHeightCompensation` so unseen cards cannot surprise the skeleton row height. See [`docs/skeleton-text-authoring.md`](./docs/skeleton-text-authoring.md) for manifest fields, command options, and the Codex-friendly workflow.
283
+
111
284
  ## Slider
112
285
 
286
+ The default `Slider` is the small synchronous core: children, drag, wheel navigation, snapping, grouping, looping, index channels, intro, and the imperative ref API. Heavier behavior is opt-in through first-party plugins, so importing one feature, such as arrows or parallax, does not pull in the rest of the slider feature set. Structured slider skeletons and restore behavior are owned by `SliderSkeleton`, composed with `useSliderReady()`.
287
+
113
288
  ```typescript
114
- import { Slider } from "react-motion-gallery";
289
+ import { Slider } from "react-motion-gallery/slider";
290
+ import { sliderArrows } from "react-motion-gallery/slider/arrows";
115
291
 
116
292
  const slides = [
117
293
  "https://picsum.photos/id/1015/1600/900",
@@ -121,7 +297,7 @@ const slides = [
121
297
 
122
298
  export function BasicSlider() {
123
299
  return (
124
- <Slider>
300
+ <Slider plugins={[sliderArrows()]}>
125
301
  {slides.map((src, index) => (
126
302
  <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
127
303
  ))}
@@ -135,97 +311,89 @@ export function BasicSlider() {
135
311
  | Option | Type | Default | Notes |
136
312
  | --- | --- | --- | --- |
137
313
  | `children` | `React.ReactNode` | `—` | Slide content rendered in order. |
314
+ | `initialIndex` | `number` | `0` | Selects the slide index used for the first layout and intro fade-in. |
138
315
  | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Merged with the internal breakpoint map for responsive values. |
139
- | `expandableImageRefs` | `React.RefObject<(HTMLImageElement | null)[]>` | internal ref | Supplies origin images for fullscreen scale transitions. |
140
316
  | `indexChannel` | `SliderIndexChannel` | internal channel | Share index state with thumbnails or sibling sliders. |
317
+ | `plugins` | `SliderPlugin[]` | `[]` | Explicit first-party slider features such as arrows, dots, auto-height, effects, fullscreen, or lazy-load. |
141
318
 
142
319
  ### Slider layout and scroll options
143
320
 
144
321
  | Option | Type | Default | Notes |
145
322
  | --- | --- | --- | --- |
146
- | `layout.gap` | `number` | `20` | Gap between cells. |
323
+ | `layout.gap` | `number \| Record<string, number>` | `20` | Responsive gap between cells. |
147
324
  | `layout.cellsPerSlide` | `number \| Record<string, number>` | `—` | Groups multiple cells into a slide page. |
148
325
  | `direction.dir` | `"ltr" \| "rtl"` | `"ltr"` | Text direction and arrow direction. |
149
326
  | `direction.axis` | `"x" \| "y"` | `"x"` | Horizontal or vertical slider axis. |
150
327
  | `align` | `"start" \| "center"` | `"start"` | Slide alignment inside the viewport. |
151
328
  | `scroll.groupCells` | `boolean` | `false` | Scrolls by grouped cells instead of every cell. |
152
- | `scroll.skipSnaps` | `boolean` | `false` | Allows momentum to skip snap points. |
329
+ | `scroll.skipSnaps` | `boolean \| { enabled?: boolean; threshold?: number }` | `false` | Allows momentum to skip snap points. Object form enables skip snaps by default and `threshold` requires release force to reach a multiple of the adjacent snap distance before multi-snap momentum is used. |
330
+ | `scroll.strictSnaps` | `boolean` | `false` | Prevents one drag release from settling more than one snap away from where the drag started. Overrides `scroll.skipSnaps`. |
153
331
  | `scroll.freeScroll` | `boolean` | `false` | Enables free dragging instead of strict snapping. |
154
332
  | `scroll.loop` | `boolean` | `false` | Wraps around at the ends. |
155
333
 
156
- ### Slider element and lazy-load options
334
+ ### Slider element and plugin options
335
+
336
+ `elements`, `motion`, and `transitions.intro` stay in the core slider. Controls, autoplay, lazy media, effects, auto-height, fullscreen, and loading overlays are explicit plugin imports.
157
337
 
158
338
  | Option | Type | Default | Notes |
159
339
  | --- | --- | --- | --- |
160
340
  | `elements.viewport` | `ElementStyle` | `—` | Class and inline style for the viewport element. |
161
341
  | `elements.container` | `ElementStyle` | `—` | Class and inline style for the moving slider container. |
162
- | `lazyLoad.enabled` | `boolean` | `false` | Enables slide-level lazy image and video loading. |
163
- | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `true` | `false` disables the built-in spinner. |
164
- | `lazyLoad.spinnerClassName` | `string` | `""` | Applied to the spinner wrapper. |
165
- | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `{}` | Inline styles for the spinner wrapper. |
166
-
167
- ### Slider control options
168
-
169
- | Option | Type | Default | Notes |
170
- | --- | --- | --- | --- |
171
- | `controls.arrows.enabled` | `boolean` | `true` | Toggles previous and next arrows. |
172
- | `controls.arrows.arrow` | `ElementStyle` | `{}` | Shared arrow class and style. |
173
- | `controls.arrows.prev` | `ElementStyle` | `{}` | Previous-arrow override. |
174
- | `controls.arrows.next` | `ElementStyle` | `{}` | Next-arrow override. |
175
- | `controls.arrows.render` | `(args) => ReactNode` | `—` | Custom renderer for both arrows. |
176
- | `controls.arrows.renderPrev` | `(args) => ReactNode` | `—` | Custom previous arrow. |
177
- | `controls.arrows.renderNext` | `(args) => ReactNode` | `—` | Custom next arrow. |
178
- | `controls.dots.enabled` | `boolean` | `true` | Toggles pagination dots. |
179
- | `controls.dots.root` | `ElementStyle` | `{}` | Dot container class and style. |
180
- | `controls.dots.dot` | `ElementStyle` | `{}` | Individual dot class and style. |
181
- | `controls.dots.render` | `(args) => ReactNode` | `—` | Full custom dots UI. |
182
- | `controls.progress.enabled` | `boolean` | `false` | Toggles the progress bar. |
183
- | `controls.progress.root` | `ElementStyle` | `{}` | Progress track class and style. |
184
- | `controls.progress.bar` | `ElementStyle` | `{}` | Progress fill class and style. |
185
- | `controls.progress.render` | `(args) => ReactNode` | `—` | Full custom progress UI. |
186
- | `controls.ripple.enabled` | `boolean` | `true` | Toggles control ripple feedback. |
187
- | `controls.ripple.className` | `string` | `""` | Custom ripple class. |
188
-
189
- ### Slider auto and transition options
190
-
191
- | Option | Type | Default | Notes |
192
- | --- | --- | --- | --- |
193
- | `auto.play.enabled` | `boolean` | `false` | Timed slide changes. |
194
- | `auto.play.speedMs` | `number` | `3000` | Delay between autoplay advances. |
195
- | `auto.play.pauseMs` | `number` | `1000` | Delay after interaction before autoplay resumes. |
196
- | `auto.play.pauseOnHover` | `boolean` | `true` | Pauses autoplay while hovering. |
197
- | `auto.scroll.enabled` | `boolean` | `false` | Continuous timed scrolling. |
198
- | `auto.scroll.speedMs` | `number` | `0.3` | Continuous auto-scroll speed. |
199
- | `auto.scroll.pauseMs` | `number` | `1000` | Delay after interaction before auto-scroll resumes. |
200
- | `auto.scroll.pauseOnHover` | `boolean` | `true` | Pauses while hovering. |
201
- | `transitions.loading.enabled` | `boolean` | `—` | Enables the loading skeleton layer. |
202
- | `transitions.loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
203
- | `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive skeleton slot count. |
204
- | `transitions.loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
205
- | `transitions.loading.skeleton` | `SliderSkeletonSpec` | `—` | Built-in skeleton spec, including per-slot overrides with `layout.slots` and centered peek support via `centering: "first"`. |
206
- | `transitions.loading.timing.exitMs` | `number` | `600` | Keeps the loading layer mounted for this long after exit starts. |
207
- | `transitions.loading.timing.minVisibleMs` | `number` | `220` | Minimum time the loading layer stays visible before exit can begin. |
208
342
  | `transitions.intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
209
343
  | `transitions.intro.staggerMs` | `number` | `—` | Delay between item fade-ins. |
210
344
  | `transitions.intro.durationMs` | `number` | `—` | Intro fade duration. |
211
345
  | `transitions.intro.easing` | `string` | `—` | Intro fade easing. |
212
346
 
347
+ ### Slider plugins
348
+
349
+ Each plugin is imported from its own subpath and passed to `plugins`. There is no aggregate controls or effects helper; this keeps one-feature imports as small as possible.
350
+
351
+ ```typescript
352
+ import { Slider } from "react-motion-gallery/slider";
353
+ import { sliderArrows } from "react-motion-gallery/slider/arrows";
354
+ import { sliderParallax } from "react-motion-gallery/slider/parallax";
355
+
356
+ <Slider plugins={[sliderArrows(), sliderParallax({ bleedPct: "8%" })]}>
357
+ {slides}
358
+ </Slider>;
359
+ ```
360
+
361
+ | Import | Factory | Notes |
362
+ | --- | --- | --- |
363
+ | `react-motion-gallery/slider/arrows` | `sliderArrows(options)` | Previous/next arrows. |
364
+ | `react-motion-gallery/slider/dots` | `sliderDots(options)` | Pagination dots. |
365
+ | `react-motion-gallery/slider/progress` | `sliderProgress(options)` | Progress bar or custom progress renderer. |
366
+ | `react-motion-gallery/slider/scrollbar` | `sliderScrollbar(options)` | Range-style position control. |
367
+ | `react-motion-gallery/slider/ripple` | `sliderRipple(options)` | Enables ripple feedback for controls that call `createRipple`. |
368
+ | `react-motion-gallery/slider/auto-play` | `sliderAutoPlay(options)` | Timed slide changes. |
369
+ | `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll(options)` | Timed continuous advancement. |
370
+ | `react-motion-gallery/slider/auto-height` | `sliderAutoHeight(options)` | Measures active slide height and gates slider readiness until measured. |
371
+ | `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad(options)` | Adds lazy media attributes to slide images and videos. |
372
+ | `react-motion-gallery/slider/parallax` | `sliderParallax(options)` | Parallax slide wrapper. |
373
+ | `react-motion-gallery/slider/scale` | `sliderScale(options)` | Scales non-active slides. |
374
+ | `react-motion-gallery/slider/fade` | `sliderFade(options)` | Fades non-active slides. |
375
+ | `react-motion-gallery/slider/crossfade` | `sliderCrossfade(options)` | Enables crossfade-aware control navigation. |
376
+ | `react-motion-gallery/slider/fullscreen` | `sliderFullscreen()` | Bridges a `GalleryCore layout="slider"` slider to fullscreen. |
377
+ | `react-motion-gallery/slider/loading` | `sliderLoading(options)` | Basic custom loading overlay. Prefer `SliderSkeleton` for structured skeleton and restore. |
378
+
213
379
  ### Slider loading skeletons
214
380
 
215
- `transitions.loading.skeleton` lets you describe a placeholder layout that mirrors the final slider instead of falling back to generic blocks. This is especially useful for variable-width slides, mixed aspect ratios, and center-aligned peek carousels.
381
+ Use `SliderSkeleton` to own slider loading. `useSliderReady()` exposes the slider ref plus a settled `ready` flag; `isSlidesBuilt()` remains a lower-level DOM-built signal and is not the right fade-out trigger.
216
382
 
217
383
  `layout.slots` is the per-slide override system. Define the shared placeholder once with `layout.item` and `layout.itemWrapStyle`, then override any individual slot with `slots[index]`. Slot `itemWrapStyle` values merge on top of the base wrap style, while `slot.item` can replace the placeholder node entirely for that slot.
218
384
 
219
385
  `itemWrapStyle` now supports wrapper-only `border` and `boxShadow` values. Wrapper `width`, `height`, and `aspectRatio` are treated as outer border-box dimensions, so the inner placeholder shrinks by the border thickness. Use simple uniform border shorthands such as `1px solid #cbd5e1` when you want the built-in sizing math to account for the border width.
220
386
 
221
- `text` nodes render one skeleton bar per `lines` value. `lines` can be a single number or a numeric min-width map such as `{ 0: 3, 767: 2, 1200: 1 }`. Use `lineWidth` to override the shortened final-line width; it defaults to `68%` of the text block width and can also be responsive with numeric min-width keys.
387
+ `text` nodes render one skeleton bar per `lines` value. `barHeight` controls the bar height and can be a single number or a numeric min-width map. `lineHeight` remains the full line-box multiplier and now accepts the same numeric min-width maps. `lines` can be a single number or a numeric min-width map such as `{ 0: 3, 767: 2, 1200: 1 }`. Use `lastBarWidth` to override the shortened trailing bar width; it defaults to `68%` of the text block width and can also be responsive with numeric min-width keys.
222
388
 
223
- `centering: "first"` is designed for center-aligned peek sliders. When the real slider uses `align="center"` and the skeleton uses `mode: "peek"` with `layout.kind: "slider"`, the built-in skeleton renderer inserts the leading spacer needed to center the first visible placeholder. You should not add that spacer manually, and it does not apply when you replace the built-in skeleton with `transitions.loading.renderLoading`.
389
+ `centering: "first"` is designed for center-aligned peek sliders. When the real slider uses `align="center"` and the skeleton uses `mode: "peek"` with `layout.kind: "slider"`, the skeleton renderer inserts the leading spacer needed to center the first visible placeholder. You should not add that spacer manually.
224
390
 
225
- When you provide `transitions.loading.timing`, `exitMs` controls both how long the loading layer remains mounted after exit starts and its opacity transition duration. The real slider intro begins as soon as the loading exit starts; it does not wait for the loading layer to finish unmounting.
391
+ When you provide `SliderSkeleton.timing`, `exitMs` controls both how long the loading layer remains mounted after exit starts and its opacity transition duration.
226
392
 
227
393
  ```typescript
228
- import { Slider } from "react-motion-gallery";
394
+ import { SliderSkeleton } from "react-motion-gallery/skeleton/slider";
395
+ import { Slider } from "react-motion-gallery/slider";
396
+ import { useSliderReady } from "react-motion-gallery/slider/ready";
229
397
 
230
398
  const slides = [
231
399
  { src: "https://picsum.photos/id/1020/660/960", width: 220, height: 320 },
@@ -234,47 +402,47 @@ const slides = [
234
402
  ];
235
403
 
236
404
  export function VariableWidthSkeletonSlider() {
405
+ const { ref: sliderRef, ready: sliderReady } = useSliderReady();
406
+
237
407
  return (
238
- <Slider
239
- align="center"
240
- transitions={{
241
- loading: {
242
- skeletonCount: 2,
243
- skeleton: {
244
- mode: "peek",
245
- centering: "first",
246
- layout: {
247
- kind: "slider",
248
- direction: "row",
249
- style: { gap: 20 },
250
- item: {
251
- kind: "rect",
252
- style: {
253
- width: "100%",
254
- height: "100%",
255
- borderRadius: 12,
256
- },
257
- },
258
- slots: slides.map((slide) => ({
259
- itemWrapStyle: {
260
- width: slide.width,
261
- height: slide.height,
262
- },
263
- })),
408
+ <SliderSkeleton
409
+ ready={sliderReady}
410
+ layout={{
411
+ mode: "peek",
412
+ centering: "first",
413
+ visibleCount: 2,
414
+ layout: {
415
+ kind: "slider",
416
+ direction: "row",
417
+ style: { gap: 20 },
418
+ item: {
419
+ kind: "rect",
420
+ style: {
421
+ width: "100%",
422
+ height: "100%",
423
+ borderRadius: 12,
264
424
  },
265
425
  },
426
+ slots: slides.map((slide) => ({
427
+ itemWrapStyle: {
428
+ width: slide.width,
429
+ height: slide.height,
430
+ },
431
+ })),
266
432
  },
267
433
  }}
268
434
  >
269
- {slides.map((slide, index) => (
270
- <img
271
- key={slide.src}
272
- src={slide.src}
273
- alt={`Slide ${index + 1}`}
274
- style={{ width: slide.width, height: slide.height, objectFit: "cover" }}
275
- />
276
- ))}
277
- </Slider>
435
+ <Slider ref={sliderRef} align="center">
436
+ {slides.map((slide, index) => (
437
+ <img
438
+ key={slide.src}
439
+ src={slide.src}
440
+ alt={`Slide ${index + 1}`}
441
+ style={{ width: slide.width, height: slide.height, objectFit: "cover" }}
442
+ />
443
+ ))}
444
+ </Slider>
445
+ </SliderSkeleton>
278
446
  );
279
447
  }
280
448
  ```
@@ -285,6 +453,7 @@ export function VariableWidthSkeletonSlider() {
285
453
  | --- | --- | --- |
286
454
  | `mode` | `"fit" \| "peek"` | `"peek"` preserves partial next or previous slide visibility in the loading state. |
287
455
  | `centering` | `"first"` | Adds the leading spacer needed for the first visible slot when using the built-in centered peek skeleton flow. |
456
+ | `visibleCount` | `number \| Record<string, number>` | Responsive count of visible skeleton slots. |
288
457
  | `className` | `string \| undefined` | Applied to the skeleton overlay root. |
289
458
  | `style` | `React.CSSProperties \| undefined` | Inline styles for the skeleton overlay root. |
290
459
  | `layout` | `SliderSkeletonNode \| undefined` | Structured placeholder layout tree. Use `kind: "slider"` to model slide tracks. |
@@ -298,7 +467,7 @@ export function VariableWidthSkeletonSlider() {
298
467
  | --- | --- | --- |
299
468
  | `kind` | `"slider"` | Slider-specific skeleton layout root. |
300
469
  | `style` | `SkeletonContainerStyle \| Record<string, SkeletonContainerStyle>` | Track-level container styles such as `gap`, `padding`, `align`, `justify`, `width`, and `maxWidth`. |
301
- | `count` | `number \| undefined` | Optional explicit slot count for the layout. Falls back to `transitions.loading.skeletonCount`. |
470
+ | `count` | `number \| undefined` | Optional explicit slot count for the layout. Falls back to `visibleCount` on the surrounding slider skeleton spec. |
302
471
  | `item` | `SkeletonNode` | Default placeholder node rendered in each slot. |
303
472
  | `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Shared wrapper size, margin, border, and box-shadow rules for every slot. Border sizing is border-box. |
304
473
  | `slots` | `SliderSkeletonSlot[] \| undefined` | Per-slot overrides for variable widths, heights, aspect ratios, or custom placeholder nodes. |
@@ -312,23 +481,15 @@ export function VariableWidthSkeletonSlider() {
312
481
  | `item` | `SkeletonNode \| undefined` | Replaces the base `layout.item` for one slot. |
313
482
  | `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Merges on top of the base `layout.itemWrapStyle` for one slot, including wrapper borders and shadows. |
314
483
 
315
- `SkeletonNode` supports these building blocks: `rect`, `square`, `circle`, `text`, `media`, `row`, `col`, and `stack`. `text.lines` controls how many wrapped skeleton rows render for that text block, and `text.lineWidth` controls the trailing line width.
484
+ `SkeletonNode` supports these building blocks: `rect`, `square`, `circle`, `text`, `media`, `row`, `col`, and `stack`. `text.barHeight` controls the bar height, `text.lines` controls how many wrapped skeleton rows render for that text block, and `text.lastBarWidth` controls the trailing bar width.
316
485
 
317
- ### Slider motion and effect options
486
+ ### Slider motion options
318
487
 
319
488
  | Option | Type | Default | Notes |
320
489
  | --- | --- | --- | --- |
321
490
  | `motion.selectDuration` | `number` | `25` | Duration for snapped selection motion. |
322
491
  | `motion.freeScrollDuration` | `number` | `43` | Duration for free-scroll settling. |
323
492
  | `motion.friction` | `number` | `0.68` | Drag and settling friction. |
324
- | `effects.parallax.enabled` | `boolean` | `—` | Enables the parallax slide treatment. |
325
- | `effects.parallax.bleedPct` | `string` | `—` | Extra image bleed around the viewport. |
326
- | `effects.parallax.borderRadius` | `string` | `—` | Radius for the parallax frame. |
327
- | `effects.parallax.sideWidth` | `string` | `—` | Side crop width used by the effect. |
328
- | `effects.scale.enabled` | `boolean` | `—` | Scales neighboring slides. |
329
- | `effects.scale.amount` | `number` | `—` | Scale multiplier for the scale effect. |
330
- | `effects.fade.enabled` | `boolean` | `—` | Fades slides based on position. |
331
- | `effects.fade.minOpacity` | `number` | `0.36` | Minimum opacity used for the fade effect, clamped from `0` to `1`. |
332
493
 
333
494
  ### Slider render callback args
334
495
 
@@ -387,12 +548,22 @@ export function VariableWidthSkeletonSlider() {
387
548
  | `onSlidesBuilt` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when slide nodes are ready. |
388
549
  | `whenSlidesBuilt` | `() => Promise<HTMLElement[]>` | Promise form of `onSlidesBuilt`. |
389
550
  | `isSlidesBuilt` | `() => boolean` | `true` once the slide list is ready. |
551
+ | `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when the slider has built, measured, committed its index, and all plugin ready gates have cleared. |
552
+ | `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of `onReady`. |
553
+ | `isReady` | `() => boolean` | `true` once the settled slider ready signal has fired. |
390
554
  | `scrollNext` | `(mode?: IndexMode) => void` | Advances one step. |
391
555
  | `scrollPrev` | `(mode?: IndexMode) => void` | Moves backward one step. |
392
556
  | `canScrollNext` | `() => boolean` | Whether next navigation is available. |
393
557
  | `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
394
558
  | `scrollProgress` | `() => number` | Current progress from `0` to `1`. |
395
559
  | `cellsInView` | `() => number[]` | Canonical cell indexes currently visible. |
560
+ | `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
561
+ | `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
562
+ | `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
563
+ | `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
564
+ | `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
565
+ | `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
566
+ | `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
396
567
  | `getInternals` | `() => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping }` | Low-level internals used by fullscreen and advanced sync code. |
397
568
 
398
569
  ### `createSliderIndexChannel`
@@ -473,7 +644,7 @@ export function SliderWithThumbnails() {
473
644
  }
474
645
  ```
475
646
 
476
- The component forwards a ref to its outer thumbnail shell. The explicit `layout`, `scroll`, and `motion` defaults below are also exported as `DEFAULT_THUMBNAILS`.
647
+ The component forwards a ref to its outer thumbnail shell.
477
648
 
478
649
  ### ThumbnailSlider component props
479
650
 
@@ -532,7 +703,7 @@ The component forwards a ref to its outer thumbnail shell. The explicit `layout`
532
703
  | Option | Type | Default | Notes |
533
704
  | --- | --- | --- | --- |
534
705
  | `transitions.loading.enabled` | `boolean` | `true` | Enables the thumbnail loading layer. |
535
- | `transitions.loading.force` | `boolean` | `false` | Forces the loading layer to remain visible. |
706
+ | `transitions.loading.force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `false` | Forces the loading layer to remain visible. Set `showContent: true` to preview the real thumbnails under the skeleton, and tune the loading overlay with `skeletonOpacity`. |
536
707
  | `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive count for the built-in loading placeholders. |
537
708
  | `transitions.loading.mode` | `"fit" \| "peek"` | `"peek"` | `"peek"` keeps fixed-size thumbnail placeholders when width or height is explicitly set; `"fit"` divides the rail evenly across the visible count. |
538
709
  | `transitions.loading.elements.container` | `ElementStyle` | `—` | Class and inline style for the built-in loading overlay container. |
@@ -588,16 +759,28 @@ export function BasicGrid() {
588
759
 
589
760
  | Option | Type | Default | Notes |
590
761
  | --- | --- | --- | --- |
591
- | `children` | `React.ReactNode` | `—` | Grid items rendered in order. |
762
+ | `children` | `React.ReactNode` | `—` | Grid items rendered in order. Wrap individual cards in `Grid.Item` when they need custom spans or wrapper props. |
592
763
  | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
593
764
  | `gridItemBaseClass` | `string` | `"rmg__grid-item"` | Internal item base class override. |
594
765
  | `renderMode` | `"wrap" \| "passthrough"` | `"wrap"` | `wrap` adds an item wrapper; `passthrough` keeps child structure closer to the source node. |
595
766
 
767
+ ### Grid.Item props
768
+
769
+ `Grid.Item` is a metadata wrapper. It renders only its children, while Grid reads the wrapper props and applies them to the generated item shell.
770
+
771
+ | Option | Type | Default | Notes |
772
+ | --- | --- | --- | --- |
773
+ | `children` | `React.ReactNode` | `—` | The grid card content. |
774
+ | `span` | `number \| "full" \| Record<string, number \| "full">` | `1` | Per-item track span. `"full"` renders `grid-column: 1 / -1`; numeric values render `grid-column: span n / span n`. |
775
+ | `className` | `string` | `—` | Extra class name merged onto the grid item wrapper. |
776
+ | `style` | `React.CSSProperties` | `—` | Inline styles merged onto the grid item wrapper. |
777
+
596
778
  ### Grid options
597
779
 
598
780
  | Option | Type | Default | Notes |
599
781
  | --- | --- | --- | --- |
600
782
  | `columns` | `number \| Record<string, number>` | `—` | Fixed responsive column count. When omitted, Grid auto-fits using `minColumnWidth`. |
783
+ | `templateColumns` | `string \| Record<string, string>` | `—` | Explicit `grid-template-columns` value. Takes precedence over `columns` and `minColumnWidth`. |
601
784
  | `minColumnWidth` | `number \| string` | `160` | Minimum width used by auto-fit mode. |
602
785
  | `gap` | `number \| Record<string, number>` | `8` | Responsive grid gap. |
603
786
  | `rootClassName` | `string` | `—` | Class name for the grid root. |
@@ -607,15 +790,9 @@ export function BasicGrid() {
607
790
  | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
608
791
  | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
609
792
  | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
610
- | `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
611
- | `loading.force` | `boolean` | `—` | Keeps the loading layer visible even when media is ready. |
612
- | `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
613
- | `loading.skeleton` | `GridSkeletonSpec` | `—` | Built-in grid skeleton spec. |
614
- | `loading.timing.exitMs` | `number` | `600` | Keeps the loading layer mounted for this long after exit starts. |
615
- | `loading.timing.minVisibleMs` | `number` | `220` | Minimum time the loading layer stays visible before exit can begin. |
616
793
  | `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
617
- | `intro.staggerMs` | `number` | `40` | Reveal stagger for the fade-in. |
618
- | `intro.durationMs` | `number` | `300` | Intro fade duration. |
794
+ | `intro.staggerMs` | `number` | `60` | Reveal stagger for the fade-in. |
795
+ | `intro.durationMs` | `number` | `600` | Intro fade duration. |
619
796
  | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
620
797
  | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
621
798
 
@@ -623,9 +800,92 @@ When `lazyLoad.enabled` is true, Grid rewrites trackable image `src` values into
623
800
 
624
801
  Grid fullscreen behavior is provided by `GalleryCore` and `useFullscreenController`; Grid itself does not expose a ref-based imperative API.
625
802
 
626
- Grid skeleton `text` nodes use the same wrapped-line treatment as slider skeletons, including responsive `lines` maps and the configurable trailing `lineWidth`.
803
+ Wrap a card in `Grid.Item` when it should span tracks or needs wrapper styling:
804
+
805
+ ```typescript
806
+ <Grid columns={{ 0: 1, 720: 6, 1100: 12 }} gap={{ 0: 12, 1100: 18 }}>
807
+ <Grid.Item span={{ 0: "full", 720: 3, 1100: 6 }} className="feature-card">
808
+ <FeatureCard />
809
+ </Grid.Item>
810
+ <Grid.Item span={{ 0: "full", 720: 3, 1100: 3 }}>
811
+ <ProductCard />
812
+ </Grid.Item>
813
+ <Grid.Item span="full">
814
+ <WideEditorialCard />
815
+ </Grid.Item>
816
+ </Grid>
817
+ ```
818
+
819
+ Grid spans require explicit tracks: use `columns` or `templateColumns`. If Grid is in auto-fit mode through `minColumnWidth`, item spans are ignored because there is no stable track count to span. Responsive span maps use the same breakpoint keys as responsive numeric props, so named keys such as `md` and numeric keys such as `900` are both valid.
820
+
821
+ Use `templateColumns` when the tracks themselves need custom proportions:
822
+
823
+ ```typescript
824
+ <Grid
825
+ templateColumns={{
826
+ 0: "1fr",
827
+ 900: "minmax(0, 1.4fr) minmax(0, 1fr)",
828
+ 1200: "minmax(0, 2fr) repeat(2, minmax(0, 1fr))",
829
+ }}
830
+ gap={{ 0: 12, 1200: 18 }}
831
+ >
832
+ <Grid.Item span={{ 0: "full", 900: 2 }}>
833
+ <FeatureCard />
834
+ </Grid.Item>
835
+ </Grid>
836
+ ```
837
+
838
+ Grid no longer owns loading UI. Use `useGridReady` and wrap Grid with `GridSkeleton`, the same composition pattern used by Slider and Masonry.
839
+
840
+ Grid skeletons live in `react-motion-gallery/skeleton/grid`. Their `text` nodes use the same wrapped-line treatment as slider skeletons, including responsive `barHeight` and `lines` maps plus the configurable trailing `lastBarWidth`.
627
841
 
628
- Grid uses the same loading timing model as Slider: `loading.timing.exitMs` controls both how long the loading layer stays mounted after exit starts and its opacity transition, and the real grid intro begins as soon as exit starts.
842
+ Grid skeletons inherit real item spans by default. Slot overrides in the Skeleton layout can change individual placeholder nodes or wrapper styles without losing the span applied by `Grid.Item`.
843
+
844
+ When Grid is wrapped in `GridSkeleton`, `GridSkeleton.timing.exitMs` controls both how long the loading layer stays mounted after exit starts and its opacity transition, and the real grid intro begins as soon as exit starts.
845
+
846
+ ```typescript
847
+ import { Grid, useGridReady } from "react-motion-gallery";
848
+ import { GridSkeleton, type GridSkeletonSpec } from "react-motion-gallery/skeleton/grid";
849
+
850
+ const gridSkeleton: GridSkeletonSpec = {
851
+ radius: 14,
852
+ layout: {
853
+ kind: "grid",
854
+ count: 6,
855
+ item: {
856
+ kind: "rect",
857
+ style: { aspectRatio: "4 / 5" },
858
+ },
859
+ },
860
+ };
861
+
862
+ function GridWithSkeleton({ images }: { images: { src: string; alt: string }[] }) {
863
+ const { ref: gridRef, ready: gridReady } = useGridReady();
864
+
865
+ return (
866
+ <GridSkeleton
867
+ layout={gridSkeleton}
868
+ ready={gridReady}
869
+ timing={{ minVisibleMs: 220, exitMs: 600 }}
870
+ grid={{
871
+ count: images.length,
872
+ columns: { 0: 1, 640: 2, 960: 3 },
873
+ gap: { 0: 12, 960: 20 },
874
+ }}
875
+ >
876
+ <Grid
877
+ ref={gridRef}
878
+ columns={{ 0: 1, 640: 2, 960: 3 }}
879
+ gap={{ 0: 12, 960: 20 }}
880
+ >
881
+ {images.map((image) => (
882
+ <img key={image.src} src={image.src} alt={image.alt} />
883
+ ))}
884
+ </Grid>
885
+ </GridSkeleton>
886
+ );
887
+ }
888
+ ```
629
889
 
630
890
  ## Masonry
631
891
 
@@ -654,144 +914,154 @@ export function BasicMasonry() {
654
914
 
655
915
  | Option | Type | Default | Notes |
656
916
  | --- | --- | --- | --- |
657
- | `children` | `React.ReactNode` | `—` | Masonry items rendered in order. |
917
+ | `children` | `React.ReactNode` | `—` | Masonry items rendered in order. Wrap individual cards in `Masonry.Item` when they need custom spans or wrapper props. |
658
918
  | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
659
919
 
920
+ ### Masonry.Item props
921
+
922
+ | Option | Type | Default | Notes |
923
+ | --- | --- | --- | --- |
924
+ | `children` | `React.ReactNode` | `—` | The masonry card content. |
925
+ | `span` | `number \| "full" \| Record<string, number \| "full">` | `1` | Per-item track span. `"full"` resolves to the active column count and numeric values clamp to the current track count. |
926
+ | `className` | `string` | `—` | Extra class name merged onto the masonry item wrapper. |
927
+ | `style` | `React.CSSProperties` | `—` | Inline styles merged onto the masonry item wrapper. |
928
+
660
929
  ### Masonry options
661
930
 
662
931
  | Option | Type | Default | Notes |
663
932
  | --- | --- | --- | --- |
664
933
  | `columns` | `number \| Record<string, number>` | `—` | Responsive column count. |
665
934
  | `gap` | `number \| Record<string, number>` | `—` | Responsive gap between columns and items. |
666
- | `placement` | `"balanced" \| "roundRobin"` | `"balanced"` | `balanced` aims for even column heights. |
667
- | `estimatedItemHeight` | `number` | `—` | Hint used before measurements settle. |
935
+ | `placement` | `"balanced" \| "roundRobin" \| "horizontalOrder"` | `"balanced"` | `balanced` packs into the shortest fitting column group, `roundRobin` cycles start columns deterministically, and `horizontalOrder` preserves a stronger left-to-right scan when spans are involved. |
936
+ | `fullscreenTrigger` | `"item" \| "media"` | `"media"` | Opens fullscreen from the clicked media node or the entire masonry item shell. |
668
937
  | `itemWrapClassName` | `string` | `—` | Class name added to the masonry item wrapper. |
669
938
  | `itemWrapStyle` | `React.CSSProperties` | `—` | Inline styles applied to the masonry item wrapper. |
670
939
  | `as` | `React.ElementType` | `"div"` | Root HTML element or custom component. |
671
940
  | `rootRef` | `React.Ref<HTMLDivElement>` | `—` | Ref to the masonry root. |
672
941
  | `classNames.root` | `string` | `—` | Root class name. |
673
- | `classNames.column` | `string` | `—` | Column class name. |
942
+ | `classNames.column` | `string` | `—` | Retained for backwards compatibility with the legacy column-wrapper renderer. |
674
943
  | `classNames.item` | `string` | `—` | Item class name. |
675
944
  | `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
676
945
  | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
677
946
  | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
678
947
  | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
679
- | `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
680
- | `loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
681
- | `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
682
- | `loading.skeleton` | `MasonrySkeletonSpec` | `—` | Built-in masonry skeleton spec. |
683
948
  | `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
684
- | `intro.staggerMs` | `number` | `40` | Reveal stagger for the fade-in. |
685
- | `intro.durationMs` | `number` | `300` | Intro fade duration. |
949
+ | `intro.staggerMs` | `number` | `160` | Reveal stagger for the fade-in. |
950
+ | `intro.durationMs` | `number` | `600` | Intro fade duration. |
686
951
  | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
687
952
  | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
688
953
 
689
954
  When `lazyLoad.enabled` is true, Masonry uses the same image shell behavior as Slider: trackable image `src` values move into `data-rmg-lazy-src`, the real images load on intersection, and the item only fades in after decode and spinner exit.
690
955
 
691
- Masonry already accepts arbitrary React children, including text-containing JSX. The new wrapper props are only for styling the built-in masonry item shell.
956
+ Masonry already accepts arbitrary React children, including text-containing JSX. The wrapper props are only for styling the built-in masonry item shell.
692
957
 
693
- Masonry skeletons can now use a structured `layout` spec with the same inner node vocabulary as Grid skeletons, including `text` nodes and `itemWrapStyle`.
694
-
695
- `layout.slots` gives Masonry the same per-card override escape hatch that slider skeletons have. Use a slot when one card needs a different placeholder tree, wrapper styling, or outer height. `slot.ratio` maps to Masonry's card-height rhythm, while `slot.heightPx` lets you pin a specific shell height when you need an exact placeholder.
958
+ Wrap a card in `Masonry.Item` when it needs its own span, wrapper `className`, or wrapper `style`:
696
959
 
697
960
  ```typescript
698
961
  <Masonry
699
- columns={{ 0: 1, 700: 2, 1100: 3 }}
700
- gap={{ 0: 12, 1100: 20 }}
701
- itemWrapStyle={{
702
- padding: "6px",
703
- borderRadius: "28px",
704
- }}
705
- loading={{
706
- enabled: true,
707
- skeleton: {
708
- ratios: [118, 126, 102, 146],
709
- layout: {
710
- kind: "masonry",
711
- itemWrapStyle: {
712
- padding: 14,
713
- borderRadius: 20,
714
- boxShadow: "0 18px 36px rgba(15, 23, 42, 0.08)",
962
+ columns={{ 0: 1, 760: 2, 1160: 4 }}
963
+ gap={{ 0: 12, 1160: 18 }}
964
+ placement="horizontalOrder"
965
+ >
966
+ <Masonry.Item span={{ 0: 1, 760: 2, 1160: 2 }}>
967
+ <FeatureCard />
968
+ </Masonry.Item>
969
+ <Masonry.Item span={1}>
970
+ <StandardCard />
971
+ </Masonry.Item>
972
+ </Masonry>
973
+ ```
974
+
975
+ Choose a placement based on what should feel stable:
976
+
977
+ - `balanced`: best when visual balance and the shortest overall columns matter most.
978
+ - `roundRobin`: best when deterministic column assignment matters more than tight packing.
979
+ - `horizontalOrder`: best when wider cards should still read in a mostly left-to-right order.
980
+
981
+ Masonry no longer owns loading UI. Use `useMasonryReady` and wrap Masonry with `MasonrySkeleton`, the same composition pattern used by Slider and Grid.
982
+
983
+ Masonry skeletons live in `react-motion-gallery/skeleton/masonry` and can use a structured `layout` spec with the same inner node vocabulary as Grid skeletons, including `text` nodes and `itemWrapStyle`.
984
+
985
+ Live Masonry content mounts invisibly until the current item set has completed an initial measurement pass. The Skeleton wrapper stays visible during that handoff, so the first revealed layout is based on measured DOM geometry rather than approximate height hints.
986
+
987
+ `layout.slots` gives Masonry the same per-card override escape hatch that slider skeletons have. Use a slot when one card needs a different placeholder tree, wrapper styling, span, or outer height. `slot.span` can override the corresponding `Masonry.Item` span for the placeholder, `slot.ratio` maps to Masonry's card-height rhythm, and `slot.heightPx` lets you pin a specific shell height when you need an exact placeholder.
988
+
989
+ ```typescript
990
+ import { Masonry, useMasonryReady } from "react-motion-gallery";
991
+ import {
992
+ MasonrySkeleton,
993
+ type MasonrySkeletonSpec,
994
+ } from "react-motion-gallery/skeleton/masonry";
995
+
996
+ const masonrySkeleton: MasonrySkeletonSpec = {
997
+ ratios: [118, 126, 102, 146],
998
+ layout: {
999
+ kind: "masonry",
1000
+ itemWrapStyle: {
1001
+ padding: 14,
1002
+ borderRadius: 20,
1003
+ boxShadow: "0 18px 36px rgba(15, 23, 42, 0.08)",
1004
+ },
1005
+ item: {
1006
+ kind: "col",
1007
+ style: { gap: 12 },
1008
+ children: [
1009
+ {
1010
+ kind: "rect",
1011
+ style: { width: "100%", height: 180, borderRadius: 16 },
1012
+ },
1013
+ {
1014
+ kind: "text",
1015
+ barHeight: 14,
1016
+ lineHeight: 1.55,
1017
+ lines: 3,
1018
+ lastBarWidth: "74%",
1019
+ style: { width: "100%" },
715
1020
  },
1021
+ ],
1022
+ },
1023
+ slots: [
1024
+ {
1025
+ ratio: 182,
1026
+ span: { 0: 1, 1100: 2 },
716
1027
  item: {
717
- kind: "col",
718
- style: { gap: 12 },
719
- children: [
720
- {
721
- kind: "rect",
722
- style: { width: "100%", height: 180, borderRadius: 16 },
723
- },
724
- {
725
- kind: "text",
726
- fontSize: 12,
727
- lineHeight: 1.4,
728
- lines: 1,
729
- lineWidth: "36%",
730
- style: { width: "34%", borderRadius: 999 },
731
- },
732
- {
733
- kind: "text",
734
- fontSize: 18,
735
- lineHeight: 1.35,
736
- lines: { 0: 2, 900: 1 },
737
- lineWidth: "64%",
738
- style: { width: "88%" },
739
- },
740
- {
741
- kind: "text",
742
- fontSize: 14,
743
- lineHeight: 1.55,
744
- lines: 3,
745
- lineWidth: "74%",
746
- style: { width: "100%" },
747
- },
748
- ],
1028
+ kind: "rect",
1029
+ style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 },
749
1030
  },
750
- slots: [
751
- {
752
- ratio: 182,
753
- item: {
754
- kind: "col",
755
- style: { gap: 12 },
756
- children: [
757
- {
758
- kind: "rect",
759
- style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 },
760
- },
761
- {
762
- kind: "text",
763
- fontSize: 12,
764
- lineHeight: 1.4,
765
- lines: 1,
766
- lineWidth: "36%",
767
- style: { width: "28%", borderRadius: 999 },
768
- },
769
- {
770
- kind: "text",
771
- fontSize: 18,
772
- lineHeight: 1.35,
773
- lines: 1,
774
- lineWidth: "64%",
775
- style: { width: "72%" },
776
- },
777
- {
778
- kind: "text",
779
- fontSize: 14,
780
- lineHeight: 1.55,
781
- lines: 2,
782
- lineWidth: "78%",
783
- style: { width: "100%" },
784
- },
785
- ],
786
- },
787
- },
788
- ],
789
1031
  },
790
- },
791
- }}
792
- >
793
- {items}
794
- </Masonry>
1032
+ ],
1033
+ },
1034
+ };
1035
+
1036
+ function MasonryWithSkeleton({ items }: { items: React.ReactNode[] }) {
1037
+ const { ref: masonryRef, ready: masonryReady } = useMasonryReady();
1038
+
1039
+ return (
1040
+ <MasonrySkeleton
1041
+ layout={masonrySkeleton}
1042
+ ready={masonryReady}
1043
+ timing={{ minVisibleMs: 220, exitMs: 600 }}
1044
+ masonry={{
1045
+ count: items.length,
1046
+ columns: { 0: 1, 700: 2, 1100: 3 },
1047
+ gap: { 0: 12, 1100: 20 },
1048
+ placement: "balanced",
1049
+ }}
1050
+ >
1051
+ <Masonry
1052
+ ref={masonryRef}
1053
+ columns={{ 0: 1, 700: 2, 1100: 3 }}
1054
+ gap={{ 0: 12, 1100: 20 }}
1055
+ itemWrapStyle={{
1056
+ padding: "6px",
1057
+ borderRadius: "28px",
1058
+ }}
1059
+ >
1060
+ {items}
1061
+ </Masonry>
1062
+ </MasonrySkeleton>
1063
+ );
1064
+ }
795
1065
  ```
796
1066
 
797
1067
  ## Entries
@@ -863,6 +1133,16 @@ export function EntryGallery() {
863
1133
  }
864
1134
  ```
865
1135
 
1136
+ ### Entry loading, decode, and reveal flow
1137
+
1138
+ When `loading.enabled` is true, entries use two viewport gates instead of one generic fade-in. `loading.nearMargin` marks a row as near the viewport, mounts the real entry content, and starts the entry media work early. `loading.viewMargin` and `loading.threshold` record when the row has actually entered view.
1139
+
1140
+ With `loading.waitForDecode` enabled, an entry does not reveal as soon as it intersects. The built-in gate waits for every trackable media URL in that entry to load and decode; in the current entry-level gate, that means image media in the entry’s `media` array. It falls back after `loading.decodeTimeoutMs`, and entries without image media are decode-ready immediately. The row fades from skeleton to content only after both conditions are true: the row has entered view and the entry media decode gate is ready.
1141
+
1142
+ Reveal timing is assigned when each entry becomes ready, so entries fade in by actual load/decode completion order as well as viewport intersection. A later row that loads quickly can take the next reveal slot while a slower row keeps its skeleton visible until its media is ready.
1143
+
1144
+ Fullscreen close has a matching entry-aware path. If the user closes fullscreen from a slide whose owning entry has not been viewed yet, the runtime resolves the flattened fullscreen index back to the owner entry, shows a temporary loading spinner while that row mounts and decodes, scrolls the owner entry into view, forces the skeleton/content layers to their final revealed state, and then runs the close animation back to the now-visible entry media. This keeps the close animation from landing on an unrevealed skeleton or an offscreen row.
1145
+
866
1146
  ### `Entries` component props
867
1147
 
868
1148
  | Option | Type | Default | Notes |
@@ -886,11 +1166,14 @@ export function EntryGallery() {
886
1166
  | `mediaLayout` | `"slider" \| "grid" \| "masonry"` | `"slider"` | Declares the intended media layout. |
887
1167
  | `render.card` | `({ entry, entryIndex, media }) => ReactNode` | `—` | Wraps the media container in custom card UI. |
888
1168
  | `render.media` | `({ entry, entryIndex, media, mediaIndex }) => ReactNode` | `—` | Custom media renderer per media item. |
889
- | `render.overlay` | `({ entry, entryIndex, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
1169
+ | `render.overlay` | `({ entry, entryIndex, media, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
890
1170
  | `render.skeleton` | `({ entry, entryIndex }) => ReactNode` | `—` | Declared in the type, but the current runtime uses `loading.skeleton` instead. |
891
1171
  | `overlay` | `ElementStyle` | `—` | Styles the fullscreen overlay container that wraps `render.overlay`. |
1172
+ | `overlay.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"overlay"` | Selects whether fullscreen entry changes fade only the rendered overlay content or the whole overlay layer. |
1173
+ | `overlay.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen entry overlay crossfades. |
1174
+ | `overlay.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen entry overlay crossfades. |
892
1175
  | `loading.enabled` | `boolean` | `—` | Enables entry loading and decode gating. |
893
- | `loading.force` | `boolean` | `—` | Forces entry skeletons to remain visible. |
1176
+ | `loading.force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `—` | Forces entry skeletons to remain visible. Set `showContent: true` to preview mounted, ready entry content under the skeleton, and tune the loading overlay with `skeletonOpacity`. |
894
1177
  | `loading.skeleton` | `EntrySkeletonSpec \| ((args) => EntrySkeletonSpec \| null \| undefined)` | `—` | Built-in skeleton spec or resolver. |
895
1178
  | `loading.minHeight` | `number \| string` | `"260px"` | Minimum reserved height while loading. |
896
1179
  | `loading.nearMargin` | `string` | `"700px 0px"` | Preload margin used before entries enter view. |
@@ -907,7 +1190,7 @@ export function EntryGallery() {
907
1190
  | `entryList` | `ElementStyle` | `—` | Styles the entry list container. |
908
1191
  | `entryRow` | `ElementStyle` | `—` | Styles each entry row container. |
909
1192
 
910
- Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching the slider and grid skeleton behavior, including responsive line counts and configurable trailing `lineWidth`.
1193
+ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching the slider and grid skeleton behavior, including responsive `barHeight` and line counts plus configurable trailing `lastBarWidth`.
911
1194
 
912
1195
  ### Entry-related callback and helper types
913
1196
 
@@ -941,6 +1224,7 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
941
1224
  | --- | --- | --- |
942
1225
  | `entry` | `EntryItem` | Entry owning the active fullscreen slide. |
943
1226
  | `entryIndex` | `number` | Entry index. |
1227
+ | `media` | `MediaItem \| null` | Media item for the active fullscreen slide, when available. |
944
1228
  | `mediaIndex` | `number \| null` | Media index inside the entry when available. |
945
1229
  | `link` | `MediaEntryLink \| null` | Flattened link back to the entry/media pair. |
946
1230
  | `opacity` | `number` | Overlay opacity supplied by the runtime. |
@@ -981,9 +1265,86 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
981
1265
 
982
1266
  Fullscreen is compositional. `GalleryCore` owns the normalized fullscreen item list, your layout opens slides through that core, and `useFullscreenController` renders the portal UI.
983
1267
 
1268
+ ### Standalone fullscreen
1269
+
1270
+ Use `GalleryCore` without a `layout` prop when your own markup owns the visible surface. Call `openFullscreenAt` with the matching item index, and render the fullscreen portal once inside the core.
1271
+
1272
+ ```typescript
1273
+ import * as React from "react";
1274
+ import { GalleryCore, useGalleryCore } from "react-motion-gallery/core";
1275
+ import { useFullscreenController } from "react-motion-gallery/fullscreen";
1276
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
1277
+ import { toMediaItems } from "react-motion-gallery/media";
1278
+
1279
+ const images = [
1280
+ {
1281
+ src: "https://picsum.photos/id/1015/1600/900",
1282
+ alt: "Mountain lake",
1283
+ },
1284
+ {
1285
+ src: "https://picsum.photos/id/1018/1600/900",
1286
+ alt: "Forest path",
1287
+ },
1288
+ ];
1289
+
1290
+ const fullscreenItems = toMediaItems(images);
1291
+
1292
+ function FullscreenPortal() {
1293
+ const { fullscreenNode } = useFullscreenController({
1294
+ plugins: [fullscreenSlider()],
1295
+ fullscreen: { enabled: true },
1296
+ });
1297
+
1298
+ return <>{fullscreenNode}</>;
1299
+ }
1300
+
1301
+ function ImageButton(props: {
1302
+ image: (typeof images)[number];
1303
+ index: number;
1304
+ }) {
1305
+ const gallery = useGalleryCore();
1306
+
1307
+ const open = (event: React.MouseEvent<HTMLButtonElement>) => {
1308
+ gallery.openFullscreenAt({
1309
+ index: props.index,
1310
+ event: event.nativeEvent,
1311
+ });
1312
+ };
1313
+
1314
+ return (
1315
+ <button type="button" onClick={open}>
1316
+ <img
1317
+ src={props.image.src}
1318
+ alt={props.image.alt}
1319
+ style={{
1320
+ display: "block",
1321
+ width: 180,
1322
+ aspectRatio: "16 / 9",
1323
+ objectFit: "cover",
1324
+ }}
1325
+ />
1326
+ </button>
1327
+ );
1328
+ }
1329
+
1330
+ export function StandaloneFullscreen() {
1331
+ return (
1332
+ <GalleryCore fullscreenItems={fullscreenItems}>
1333
+ {images.map((image, index) => (
1334
+ <ImageButton key={image.src} image={image} index={index} />
1335
+ ))}
1336
+ <FullscreenPortal />
1337
+ </GalleryCore>
1338
+ );
1339
+ }
1340
+ ```
1341
+
1342
+ ### Slider fullscreen
1343
+
984
1344
  ```typescript
985
1345
  import * as React from "react";
986
1346
  import { GalleryCore, Slider, useFullscreenController } from "react-motion-gallery";
1347
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
987
1348
 
988
1349
  const slides = [
989
1350
  "https://picsum.photos/id/1015/1600/900",
@@ -993,6 +1354,7 @@ const slides = [
993
1354
 
994
1355
  function FullscreenAddon() {
995
1356
  const { fullscreenNode } = useFullscreenController({
1357
+ plugins: [fullscreenSlider()],
996
1358
  fullscreen: { enabled: true },
997
1359
  });
998
1360
 
@@ -1013,13 +1375,28 @@ export function SliderWithFullscreen() {
1013
1375
  }
1014
1376
  ```
1015
1377
 
1378
+ ### Fullscreen lazy-load handshake
1379
+
1380
+ Fullscreen keeps the base layout and fullscreen surface as separate render trees joined by one canonical index. The base layout can render thumbnails, cropped images, cards, or entries while `GalleryCore.fullscreenItems` provides the media that fullscreen renders for the same positions.
1381
+
1382
+ That index is also the communication channel for lazy loading. When a base item becomes visible, `GalleryCore` emits a base-visible event. If `fullscreen.lazyLoad.images.enabled` or `fullscreen.lazyLoad.videos.enabled` is active through `fullscreenLazyLoad()`, the fullscreen runtime listens for that event and prewarms the matching fullscreen media: images are fetched and decoded with high priority, and videos can prewarm their poster/source before being force-mounted.
1383
+
1384
+ Once the modal is open, the fullscreen slider index becomes the live gate. `fsSub` changes recompute which canonical image or video is allowed to mount or apply its source, then notify the lazy slide listeners. The active slide is always allowed; decoded images and prepared videos stay warm, and videos that were prewarmed from the base layout remain in the allowed set so navigation can land on prepared media.
1385
+
1386
+ Fullscreen also emits its visible index back through `GalleryCore`. Base media primitives use the core fullscreen state to suspend while fullscreen is active, and can use the visible fullscreen index to prewarm their matching media. Captions, overlays, and thumbnail rails stay synchronized through the same index contract.
1387
+
1388
+ For custom fullscreen images, `fullscreen.renderImage` must render a real descendant `<img>`. With `fullscreenLazyLoad({ images: { enabled: true } })`, that custom renderer participates in the same mount, spinner, load, and decode flow instead of mounting every fullscreen image eagerly.
1389
+
1016
1390
  Add fullscreen thumbnails by rendering `FullscreenThumbnailSlider` with the bridge returned from `useFullscreenController`.
1017
1391
 
1018
1392
  ```typescript
1019
1393
  import { FullscreenThumbnailSlider, useFullscreenController } from "react-motion-gallery";
1394
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
1395
+ import { fullscreenThumbnails } from "react-motion-gallery/fullscreen/thumbnails";
1020
1396
 
1021
1397
  function FullscreenWithThumbs({ thumbs }: { thumbs: string[] }) {
1022
1398
  const { fullscreenNode, fullscreenThumbnailBridge } = useFullscreenController({
1399
+ plugins: [fullscreenSlider(), fullscreenThumbnails()],
1023
1400
  fullscreen: {
1024
1401
  enabled: true,
1025
1402
  slider: {
@@ -1047,6 +1424,7 @@ Set `fullscreen.slider.direction` when fullscreen should mirror RTL interaction:
1047
1424
 
1048
1425
  ```typescript
1049
1426
  useFullscreenController({
1427
+ plugins: [fullscreenSlider()],
1050
1428
  fullscreen: {
1051
1429
  enabled: true,
1052
1430
  slider: {
@@ -1056,22 +1434,52 @@ useFullscreenController({
1056
1434
  });
1057
1435
  ```
1058
1436
 
1059
- ### `GalleryCore` props
1437
+ Set `fullscreen.slider.gap` to add space between fullscreen slides. It accepts the same responsive number form as the base slider, using the `GalleryCore.breakpoints` map for named breakpoint keys:
1060
1438
 
1061
- | Option | Type | Default | Notes |
1062
- | --- | --- | --- | --- |
1063
- | `children` | `React.ReactNode` | `—` | The gallery tree using the shared core. |
1064
- | `layout` | `"slider" \| "grid" \| "masonry" \| "entries"` | `—` | Declares the owning base layout. |
1065
- | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Breakpoint map shared with descendants. |
1066
- | `fullscreenItems` | `MediaItem[] \| string[]` | `[]` | Normalized fullscreen media list. |
1067
- | `nodes` | `ReactNode \| ReactNode[]` | `—` | Advanced initial node list for imperative gallery state. |
1439
+ ```typescript
1440
+ useFullscreenController({
1441
+ plugins: [fullscreenSlider()],
1442
+ fullscreen: {
1443
+ enabled: true,
1444
+ slider: {
1445
+ gap: { 0: 12, md: 20, 1200: 28 },
1446
+ },
1447
+ },
1448
+ });
1449
+ ```
1450
+
1451
+ Import `fullscreenVideo` from `react-motion-gallery/fullscreen/video` for fullscreen video slides. Set `fullscreen.video.playOnOpen` to start a Plyr-backed fullscreen video when fullscreen opens directly onto that video slide:
1452
+
1453
+ ```typescript
1454
+ useFullscreenController({
1455
+ plugins: [fullscreenSlider(), fullscreenVideo()],
1456
+ fullscreen: {
1457
+ enabled: true,
1458
+ video: {
1459
+ playOnOpen: true,
1460
+ },
1461
+ },
1462
+ });
1463
+ ```
1068
1464
 
1069
1465
  ### `useFullscreenController` args
1070
1466
 
1071
1467
  | Option | Type | Default | Notes |
1072
1468
  | --- | --- | --- | --- |
1469
+ | `plugins` | `FullscreenPlugin[]` | `[]` | Explicit first-party fullscreen features. At minimum, import `fullscreenSlider()` to mount the fullscreen runtime. |
1073
1470
  | `fullscreen` | `FullscreenOptions` | `—` | Fullscreen behavior and rendering options. |
1074
1471
 
1472
+ | Import | Factory | Notes |
1473
+ | --- | --- | --- |
1474
+ | `react-motion-gallery/fullscreen/slider` | `fullscreenSlider(options)` | Mounts the fullscreen slider runtime and accepts `fullscreen.slider` options. |
1475
+ | `react-motion-gallery/fullscreen/controls` | `fullscreenControls(options)` | Option plugin for close, arrows, and counter options. Use with `fullscreenSlider()`. |
1476
+ | `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions(options)` | Adds caption rendering, placement, and caption motion runtime. Use with `fullscreenSlider()`. |
1477
+ | `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan(options)` | Adds fullscreen click zoom, pan, and pinch runtime. Use with `fullscreenSlider()`. |
1478
+ | `react-motion-gallery/fullscreen/video` | `fullscreenVideo(options)` | Adds fullscreen Plyr rendering, source/options, and `playOnOpen` runtime. Use with `fullscreenSlider()`. |
1479
+ | `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad(options)` | Adds fullscreen image and video lazy-load gates. Use with `fullscreenSlider()`. |
1480
+ | `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade(options)` | Option plugin for fullscreen crossfade controls, drag, and wheel behavior. Use with `fullscreenSlider()`. |
1481
+ | `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails()` | Option-only plugin for fullscreen thumbnail bridge behavior. Use with `fullscreenSlider()`. |
1482
+
1075
1483
  ### Recommended `useFullscreenController` return values
1076
1484
 
1077
1485
  | Field | Type | Notes |
@@ -1092,9 +1500,10 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1092
1500
  | --- | --- | --- | --- |
1093
1501
  | `enabled` | `boolean` | `false` | Master switch for fullscreen UI. |
1094
1502
  | `items` | `MediaItem[] \| string[]` | `—` | Declared in the type, but current fullscreen media resolution comes from `GalleryCore.fullscreenItems`. |
1095
- | `renderImage` | `({ item, index, isZoomed, className, baseStyle }) => ReactNode` | `—` | Custom fullscreen image renderer. Must render a real descendant `<img>`. |
1503
+ | `renderImage` | `({ item, index, isZoomed, className, baseStyle }) => ReactNode` | `—` | Custom fullscreen image renderer. Must render a real descendant `<img>`. With `lazyLoad.images.enabled`, the renderer is mounted only when the slide is allowed and the runtime watches that descendant image for load/decode readiness. |
1096
1504
  | `video.source` | `(item: MediaItem, index: number) => Plyr.SourceInfo` | `—` | Builds fullscreen Plyr sources for video items. |
1097
1505
  | `video.options` | `Plyr.Options \| ((item: MediaItem, index: number) => Plyr.Options)` | `—` | Builds fullscreen Plyr options. |
1506
+ | `video.playOnOpen` | `boolean` | `false` | Attempts to play the fullscreen Plyr video when fullscreen opens directly onto a video slide. Browser autoplay rules still apply. |
1098
1507
  | `video.style` | `React.CSSProperties` | `—` | Fullscreen player inline style. |
1099
1508
  | `video.className` | `string` | `—` | Fullscreen player class. |
1100
1509
  | `controls.close.enabled` | `boolean` | `true` | Toggles the close button. |
@@ -1120,6 +1529,9 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1120
1529
  | `caption.breakpoint` | `number` | `—` | Viewport cutoff for switching placement logic. |
1121
1530
  | `caption.render` | `({ item, index, isZoomed }) => ReactNode` | `—` | Custom caption renderer. |
1122
1531
  | `caption.layout` | `"overlay" \| "slide"` | `—` | Chooses whether the caption overlays the media or lives in the slide layout. |
1532
+ | `caption.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"content"` | Selects whether overlay caption changes fade only the rendered caption content or the whole overlay layer. |
1533
+ | `caption.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen overlay caption crossfades. |
1534
+ | `caption.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen overlay caption crossfades. |
1123
1535
  | `caption.zoomFade` | `boolean` | `true` | Fades captions out on fullscreen zoom-in and back in on zoom-out. |
1124
1536
  | `caption.zoomFadeDurationMs` | `number` | `300` | Duration for fullscreen caption zoom fades. |
1125
1537
  | `caption.zoomFadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen caption zoom fades. |
@@ -1128,6 +1540,7 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1128
1540
  | `slider.duration` | `number` | `25` | Fullscreen slider motion duration. |
1129
1541
  | `slider.friction` | `number` | `0.68` | Fullscreen slider friction. |
1130
1542
  | `slider.direction` | `"ltr" \| "rtl"` | `"ltr"` | Fullscreen slider interaction direction. |
1543
+ | `slider.gap` | `number \| Record<string, number>` | `0` | Responsive pixel gap between fullscreen slides. Named keys resolve from `GalleryCore.breakpoints`. |
1131
1544
  | `zoom.clickZoomLevel` | `number` | `2.5` | Zoom level used for click-to-zoom. |
1132
1545
  | `zoom.maxZoomLevel` | `number` | `3` | Maximum allowed zoom level. |
1133
1546
  | `zoom.panDuration` | `number` | `43` | Pan settling duration. |
@@ -1135,18 +1548,27 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1135
1548
  | `effects.introDuration` | `number` | `300` | Open animation duration. |
1136
1549
  | `effects.introEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Open animation easing. |
1137
1550
  | `effects.introFade` | `boolean` | `false` | Forces fade intro behavior. |
1138
- | `effects.slideFade` | `boolean` | `false` | Fades between fullscreen slides. |
1139
- | `effects.slideFadeDuration` | `number` | `120` | Slide-fade duration. |
1140
- | `effects.slideFadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Slide-fade easing. |
1141
- | `lazyLoad.images.enabled` | `boolean` | `—` | Enables fullscreen image lazy loading. |
1551
+ | `effects.crossfade.controls` | `boolean` | `false` | Uses crossfade transitions for fullscreen arrow navigation and animated slide requests. Also enables wheel crossfade unless `effects.crossfade.wheel` is provided. |
1552
+ | `effects.crossfade.drag` | `boolean` | `false` | Scrubs adjacent fullscreen slides with crossfade during drag instead of moving the track. |
1553
+ | `effects.crossfade.wheel` | `boolean \| CrossFadeWheelOptions` | `effects.crossfade.controls` | Uses wheel or touchpad travel as a one-slide-at-a-time fullscreen crossfade gesture. Set `false` to keep arrow crossfades while using normal wheel scrolling. |
1554
+ | `effects.crossfade.wheel.enabled` | `boolean` | `true` when object form is used | Enables or disables fullscreen wheel crossfade when using the object form. |
1555
+ | `effects.crossfade.wheel.sensitivity` | `number` | `5` | Multiplies wheel delta into virtual drag progress. Higher values reach the commit threshold sooner. |
1556
+ | `effects.crossfade.wheel.commitThreshold` | `number` | `0.38` | Progress needed to commit to the previous or next fullscreen slide. Values are clamped from `0` to below `0.5`. |
1557
+ | `effects.crossfade.wheel.durationMs` | `number` | `effects.crossfade.durationMs` | Fade duration after fullscreen wheel crossfade commits. |
1558
+ | `effects.crossfade.wheel.sessionGapMs` | `number` | `24` | Short quiet window used to distinguish same-direction touchpad tail from a fresh fullscreen wheel gesture after a committed wheel crossfade. |
1559
+ | `effects.crossfade.durationMs` | `number` | `120` | Shared fullscreen crossfade duration for controls, drag release, and wheel commit unless wheel overrides it. |
1560
+ | `effects.crossfade.easing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Shared fullscreen crossfade easing. |
1561
+ | `lazyLoad.images.enabled` | `boolean` | `—` | Enables fullscreen image lazy loading. Base-visible indices predecode matching fullscreen images, and fullscreen index changes allow the active image slide to mount or apply its source. |
1142
1562
  | `lazyLoad.images.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen images. |
1143
1563
  | `lazyLoad.images.spinnerClassName` | `string` | `—` | Spinner class for image slides. |
1144
1564
  | `lazyLoad.images.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for image slides. |
1145
- | `lazyLoad.videos.enabled` | `boolean` | `—` | Enables fullscreen video lazy loading. |
1565
+ | `lazyLoad.videos.enabled` | `boolean` | `—` | Opts fullscreen videos into lazy mounting. Base-visible indices prewarm matching video posters/sources and fullscreen index changes mount the active or already-prepared video slide. By default fullscreen Plyr videos mount eagerly in the hidden fullscreen tree. |
1146
1566
  | `lazyLoad.videos.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen videos. |
1147
1567
  | `lazyLoad.videos.spinnerClassName` | `string` | `—` | Spinner class for video slides. |
1148
1568
  | `lazyLoad.videos.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for video slides. |
1149
1569
 
1570
+ Fullscreen `effects.crossfade.wheel` uses the same `true`, `false`, or object form as slider wheel crossfade. Its `durationMs` default follows fullscreen `effects.crossfade.durationMs`, which defaults to `120`.
1571
+
1150
1572
  ### Fullscreen callback and helper types
1151
1573
 
1152
1574
  #### `FsCounterArgs`
@@ -1255,37 +1677,6 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1255
1677
  | `fadeDurationMs` | `number \| undefined` | Slot fade duration. |
1256
1678
  | `fadeEasing` | `string \| undefined` | Slot fade easing. |
1257
1679
 
1258
- ### `GalleryApi`
1259
-
1260
- `GalleryApi` is exported as a type from the package root. The package also exposes `GalleryCore` and `useGalleryCore()` for core-context access, but it does not expose a dedicated hook that returns a `GalleryApi`-typed instance directly.
1261
-
1262
- | Method | Signature | Notes |
1263
- | --- | --- | --- |
1264
- | `rootNode` | `() => HTMLElement \| null` | Gallery root node. |
1265
- | `containerNode` | `() => HTMLElement \| null` | Moving or content container node. |
1266
- | `getViewportNode` | `() => HTMLDivElement \| null` | Viewport node. |
1267
- | `slideNodes` | `() => HTMLElement[]` | Current slide elements. |
1268
- | `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Subscribes to readiness. |
1269
- | `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of readiness. |
1270
- | `isReady` | `() => boolean` | `true` after readiness resolves. |
1271
- | `scrollTo` | `(index: number, jump?: boolean) => void` | Navigates to a slide. |
1272
- | `scrollNext` | `(jump?: boolean) => void` | Advances to the next slide. |
1273
- | `scrollPrev` | `(jump?: boolean) => void` | Moves to the previous slide. |
1274
- | `canScrollNext` | `() => boolean` | Whether next navigation is available. |
1275
- | `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
1276
- | `getIndex` | `() => number` | Current active index. |
1277
- | `selectCell` | `(index: number, jump?: boolean) => void` | Selects a cell by canonical index. |
1278
- | `scrollProgress` | `() => number` | Scroll progress from `0` to `1`. |
1279
- | `cellsInView` | `() => number[]` | Canonical cells currently visible. |
1280
- | `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
1281
- | `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
1282
- | `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
1283
- | `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
1284
- | `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
1285
- | `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
1286
- | `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
1287
- | `openFullscreenAt` | `({ index, method?, event? }) => void` | Programmatically opens fullscreen at an index. |
1288
-
1289
1680
  ## Video
1290
1681
 
1291
1682
  `Video` is the gallery-aware video primitive. It mounts Plyr lazily, syncs with gallery visibility, and can be used inside `Slider`, `Grid`, `Masonry`, `Entries`, and fullscreen flows.