react-motion-gallery 2.0.19 → 2.0.21

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 (224) hide show
  1. package/LICENSE.md +14 -4
  2. package/README.md +714 -295
  3. package/THIRD_PARTY_NOTICES.md +31 -0
  4. package/dist/FullscreenRuntime-DVRVGK5F.mjs +4 -0
  5. package/dist/FullscreenRuntime-QX6YELBN.css +1 -0
  6. package/dist/GridSkeleton-B5wWBN9L.d.mts +23 -0
  7. package/dist/MasonrySkeleton-D8aZRUiv.d.mts +29 -0
  8. package/dist/{chunk-NABNX5HB.mjs → chunk-4NT4UVB5.mjs} +1 -1
  9. package/dist/chunk-4VHNCVVB.mjs +0 -0
  10. package/dist/chunk-5ZLGEQ55.mjs +1 -0
  11. package/dist/chunk-6PUPWNGD.mjs +1 -0
  12. package/dist/chunk-6XG7U4FJ.mjs +1 -0
  13. package/dist/chunk-ADIHG7AT.mjs +1 -0
  14. package/dist/chunk-AX2FSVFD.mjs +2 -0
  15. package/dist/chunk-B4CC5AGE.mjs +1 -0
  16. package/dist/chunk-BJBHSWMF.mjs +1 -0
  17. package/dist/chunk-C6PKH3FH.mjs +1 -0
  18. package/dist/chunk-CKXN2H7S.mjs +1 -0
  19. package/dist/chunk-D3T6HIS2.mjs +1 -0
  20. package/dist/chunk-DCUCXQHE.mjs +3 -0
  21. package/dist/chunk-EFXHC36P.mjs +0 -0
  22. package/dist/chunk-G5G54AZD.mjs +4 -0
  23. package/dist/chunk-GNGOOOVY.mjs +1 -0
  24. package/dist/chunk-GTS3HDGY.mjs +1 -0
  25. package/dist/chunk-H6XFG3CJ.mjs +1 -0
  26. package/dist/chunk-HGY3QLCE.mjs +1 -0
  27. package/dist/chunk-HK2DPKES.mjs +1 -0
  28. package/dist/chunk-HMT6GZUO.mjs +4 -0
  29. package/dist/chunk-JJMFOLJZ.mjs +1 -0
  30. package/dist/chunk-L2HRIINV.mjs +1 -0
  31. package/dist/chunk-L4TRPKGX.mjs +4 -0
  32. package/dist/chunk-LVMYHM4T.mjs +1 -0
  33. package/dist/chunk-MKCNUQFD.mjs +1 -0
  34. package/dist/chunk-NHIKOJLU.mjs +1 -0
  35. package/dist/chunk-NQI246HG.mjs +1 -0
  36. package/dist/chunk-ONCDUVQT.mjs +5 -0
  37. package/dist/chunk-OZSG45IP.mjs +4 -0
  38. package/dist/chunk-PFEGIWQJ.mjs +1 -0
  39. package/dist/chunk-PPFUWP7C.mjs +3 -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-VGXO2IAF.mjs +1 -0
  51. package/dist/chunk-VPFAU2TE.mjs +1 -0
  52. package/dist/chunk-VWEQRZ24.mjs +1 -0
  53. package/dist/chunk-WGVWASZM.mjs +1 -0
  54. package/dist/chunk-WMG2LTLR.mjs +1 -0
  55. package/dist/chunk-X4HEGEZV.mjs +1 -0
  56. package/dist/chunk-XUQO5F2F.mjs +1 -0
  57. package/dist/chunk-Y7NUGXTR.mjs +1 -0
  58. package/dist/chunk-YIUXD3CO.mjs +1 -0
  59. package/dist/chunk-Z34PSRMG.mjs +1 -0
  60. package/dist/chunk-ZCCYTID7.mjs +1 -0
  61. package/dist/chunk-ZFDPVAPM.mjs +6 -0
  62. package/dist/chunk-ZXW3RIAC.mjs +1 -0
  63. package/dist/core.d.mts +22 -22
  64. package/dist/core.mjs +1 -1
  65. package/dist/entries.css +1 -1
  66. package/dist/entries.d.mts +49 -17
  67. package/dist/entries.mjs +1 -1
  68. package/dist/force-C5m1QpdF.d.mts +7 -0
  69. package/dist/fullscreen-captions.css +1 -0
  70. package/dist/fullscreen-captions.d.mts +19 -0
  71. package/dist/fullscreen-captions.mjs +1 -0
  72. package/dist/fullscreen-controls.d.mts +19 -0
  73. package/dist/fullscreen-controls.mjs +1 -0
  74. package/dist/fullscreen-crossfade.d.mts +19 -0
  75. package/dist/fullscreen-crossfade.mjs +1 -0
  76. package/dist/fullscreen-lazy-load.css +1 -0
  77. package/dist/fullscreen-lazy-load.d.mts +19 -0
  78. package/dist/fullscreen-lazy-load.mjs +1 -0
  79. package/dist/fullscreen-slider.css +1 -0
  80. package/dist/fullscreen-slider.d.mts +19 -0
  81. package/dist/fullscreen-slider.mjs +1 -0
  82. package/dist/fullscreen-thumbnails.d.mts +19 -0
  83. package/dist/fullscreen-thumbnails.mjs +1 -0
  84. package/dist/fullscreen-video.css +1 -0
  85. package/dist/fullscreen-video.d.mts +19 -0
  86. package/dist/fullscreen-video.mjs +1 -0
  87. package/dist/fullscreen-zoom-pan.d.mts +19 -0
  88. package/dist/fullscreen-zoom-pan.mjs +1 -0
  89. package/dist/fullscreen.css +1 -1
  90. package/dist/fullscreen.d.mts +29 -67
  91. package/dist/fullscreen.mjs +1 -1
  92. package/dist/fullscreenThumbnails.css +1 -1
  93. package/dist/fullscreenThumbnails.d.mts +8 -8
  94. package/dist/fullscreenThumbnails.mjs +1 -1
  95. package/dist/grid-lazy-load.css +1 -0
  96. package/dist/grid-lazy-load.d.mts +9 -0
  97. package/dist/grid-lazy-load.mjs +1 -0
  98. package/dist/grid-ready.d.mts +12 -0
  99. package/dist/grid-ready.mjs +1 -0
  100. package/dist/grid.css +1 -1
  101. package/dist/grid.d.mts +8 -29
  102. package/dist/grid.mjs +1 -1
  103. package/dist/index-DG19CAvz.d.mts +21 -0
  104. package/dist/{index-CwwxTQKa.d.mts → index-lEnLoQv4.d.mts} +5 -6
  105. package/dist/index.css +1 -1
  106. package/dist/index.d.mts +25 -28
  107. package/dist/index.mjs +1 -1
  108. package/dist/layout-DoYnPD0I.d.mts +137 -0
  109. package/dist/lazy-dGoYpcRa.d.mts +14 -0
  110. package/dist/masonry-lazy-load.css +1 -0
  111. package/dist/masonry-lazy-load.d.mts +9 -0
  112. package/dist/masonry-lazy-load.mjs +1 -0
  113. package/dist/masonry-ready.d.mts +12 -0
  114. package/dist/masonry-ready.mjs +1 -0
  115. package/dist/masonry.css +1 -1
  116. package/dist/masonry.d.mts +14 -13
  117. package/dist/masonry.mjs +1 -1
  118. package/dist/{plyrTypes-Cq4C3ul5.d.mts → media.d.mts} +1 -8
  119. package/dist/media.mjs +1 -0
  120. package/dist/metafile-esm.json +1 -1
  121. package/dist/plyrTypes-DhzgHNfX.d.mts +9 -0
  122. package/dist/responsive-CrESIWcm.d.mts +530 -0
  123. package/dist/responsive.d.mts +15 -0
  124. package/dist/responsive.mjs +1 -0
  125. package/dist/responsiveNumber-CouEMJ9O.d.mts +5 -0
  126. package/dist/skeleton-base.css +1 -0
  127. package/dist/skeleton-base.d.mts +52 -0
  128. package/dist/skeleton-base.mjs +1 -0
  129. package/dist/skeleton-grid.css +1 -0
  130. package/dist/skeleton-grid.d.mts +63 -0
  131. package/dist/skeleton-grid.mjs +1 -0
  132. package/dist/skeleton-masonry.css +1 -0
  133. package/dist/skeleton-masonry.d.mts +62 -0
  134. package/dist/skeleton-masonry.mjs +1 -0
  135. package/dist/skeleton-slider.css +1 -0
  136. package/dist/skeleton-slider.d.mts +208 -0
  137. package/dist/skeleton-slider.mjs +40 -0
  138. package/dist/slider-arrows.d.mts +9 -0
  139. package/dist/slider-arrows.mjs +1 -0
  140. package/dist/slider-auto-height.d.mts +9 -0
  141. package/dist/slider-auto-height.mjs +1 -0
  142. package/dist/slider-auto-play.d.mts +9 -0
  143. package/dist/slider-auto-play.mjs +1 -0
  144. package/dist/slider-auto-scroll.d.mts +9 -0
  145. package/dist/slider-auto-scroll.mjs +1 -0
  146. package/dist/slider-crossfade.d.mts +9 -0
  147. package/dist/slider-crossfade.mjs +1 -0
  148. package/dist/slider-dots.css +1 -0
  149. package/dist/slider-dots.d.mts +9 -0
  150. package/dist/slider-dots.mjs +1 -0
  151. package/dist/slider-fade.d.mts +9 -0
  152. package/dist/slider-fade.mjs +1 -0
  153. package/dist/slider-fullscreen.d.mts +9 -0
  154. package/dist/slider-fullscreen.mjs +1 -0
  155. package/dist/slider-lazy-load.css +1 -0
  156. package/dist/slider-lazy-load.d.mts +9 -0
  157. package/dist/slider-lazy-load.mjs +1 -0
  158. package/dist/slider-loading.css +1 -0
  159. package/dist/slider-loading.d.mts +9 -0
  160. package/dist/slider-loading.mjs +1 -0
  161. package/dist/slider-parallax.d.mts +9 -0
  162. package/dist/slider-parallax.mjs +1 -0
  163. package/dist/slider-progress.d.mts +9 -0
  164. package/dist/slider-progress.mjs +1 -0
  165. package/dist/slider-ready.d.mts +14 -0
  166. package/dist/slider-ready.mjs +1 -0
  167. package/dist/slider-ripple.d.mts +9 -0
  168. package/dist/slider-ripple.mjs +1 -0
  169. package/dist/slider-scale.d.mts +9 -0
  170. package/dist/slider-scale.mjs +1 -0
  171. package/dist/slider-scrollbar.css +1 -0
  172. package/dist/slider-scrollbar.d.mts +9 -0
  173. package/dist/slider-scrollbar.mjs +1 -0
  174. package/dist/slider.css +1 -1
  175. package/dist/slider.d.mts +7 -81
  176. package/dist/slider.mjs +1 -1
  177. package/dist/styles.css +4 -0
  178. package/dist/text-BBcRGVzn.d.mts +10 -0
  179. package/dist/thumbnails.css +1 -1
  180. package/dist/thumbnails.d.mts +8 -11
  181. package/dist/thumbnails.mjs +1 -1
  182. package/dist/transitions-DU3ftmIq.d.mts +6 -0
  183. package/dist/types-BiXSaEk7.d.mts +451 -0
  184. package/dist/types-Br27DWP7.d.mts +61 -0
  185. package/dist/{types-ROPjU8Nl.d.mts → types-DNd5jSkS.d.mts} +3 -2
  186. package/dist/{types-CHUayqcj.d.mts → types-DXFoG8LC.d.mts} +5 -3
  187. package/dist/types-Do4Pq-Td.d.mts +57 -0
  188. package/dist/video.css +1 -1
  189. package/dist/video.d.mts +2 -1
  190. package/dist/video.mjs +1 -1
  191. package/dist/zoomPan.d.mts +2 -4
  192. package/dist/zoomPan.mjs +1 -1
  193. package/package.json +146 -9
  194. package/dist/chunk-5HIHJGIV.mjs +0 -45
  195. package/dist/chunk-6TPHLAUP.mjs +0 -1
  196. package/dist/chunk-BIDZ4WZB.mjs +0 -2
  197. package/dist/chunk-DBIFLX6Y.mjs +0 -6
  198. package/dist/chunk-ECQ74X24.mjs +0 -1
  199. package/dist/chunk-FJYYM5TH.mjs +0 -1
  200. package/dist/chunk-GSEIEFRW.mjs +0 -1
  201. package/dist/chunk-GT6IL37J.mjs +0 -1
  202. package/dist/chunk-J4E4PKE5.mjs +0 -1
  203. package/dist/chunk-JD3VAF3N.mjs +0 -4
  204. package/dist/chunk-JMFDRKTX.mjs +0 -2
  205. package/dist/chunk-K6PQU6HF.mjs +0 -1
  206. package/dist/chunk-KSOQWCCL.mjs +0 -6
  207. package/dist/chunk-NEJ27O2B.mjs +0 -2
  208. package/dist/chunk-Q2PY6ZMU.mjs +0 -2
  209. package/dist/chunk-TKPLWDPW.mjs +0 -7
  210. package/dist/chunk-UV2SUN5V.mjs +0 -1
  211. package/dist/chunk-VXSRNAH4.mjs +0 -1
  212. package/dist/chunk-WLWVKQPL.mjs +0 -4
  213. package/dist/chunk-WZWMG4ZT.mjs +0 -1
  214. package/dist/chunk-XOS5AXSR.mjs +0 -4
  215. package/dist/chunk-ZX5E327W.mjs +0 -1
  216. package/dist/controls-SpWg1Kgt.d.mts +0 -44
  217. package/dist/layout-CR6f2aPH.d.mts +0 -95
  218. package/dist/responsive-D_xhZmVI.d.mts +0 -186
  219. package/dist/sliderSub-Bo6Y8as_.d.mts +0 -45
  220. package/dist/text-Cl2tR8oO.d.mts +0 -4
  221. package/dist/types-DY058l5M.d.mts +0 -301
  222. package/dist/types-VULXzSa2.d.mts +0 -68
  223. package/dist/types-XEr8LRal.d.mts +0 -65
  224. package/dist/types-_1D0QtfD.d.mts +0 -174
package/README.md CHANGED
@@ -1,24 +1,56 @@
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.1kB |
15
+ | `FullscreenThumbnailSlider` | 20.3kB |
16
+ | `GalleryCore` | 2.6kB |
17
+ | `Grid` | 6.3kB |
18
+ | `grid/ready` | 323.0B |
19
+ | `grid/lazy-load` | 3.3kB |
20
+ | `Masonry` | 6.5kB |
21
+ | `masonry/ready` | 323.0B |
22
+ | `masonry/lazy-load` | 3.3kB |
23
+ | `Skeleton base` | 8.1kB |
24
+ | `skeleton/slider` | 16.9kB |
25
+ | `skeleton/grid` | 10.4kB |
26
+ | `skeleton/masonry` | 17.8kB |
27
+ | `Slider core` | 18.7kB |
28
+ | `slider/ready` | 894.0B |
29
+ | `slider/arrows` | 1.2kB |
30
+ | `slider/dots` | 932.0B |
31
+ | `slider/progress` | 892.0B |
32
+ | `slider/scrollbar` | 1.2kB |
33
+ | `slider/auto-height` | 1.3kB |
34
+ | `slider/lazy-load` | 3.9kB |
35
+ | `slider/parallax` | 1.4kB |
36
+ | `slider/scale` | 1.2kB |
37
+ | `slider/fade` | 1.2kB |
38
+ | `slider/crossfade` | 2.8kB |
39
+ | `slider/fullscreen` | 959.0B |
40
+ | `ThumbnailSlider` | 18.9kB |
41
+ | `useFullscreenController` | 4.9kB |
42
+ | `fullscreen/slider` | 35.8kB |
43
+ | `fullscreen/controls` | 173.0B |
44
+ | `fullscreen/captions` | 13.1kB |
45
+ | `fullscreen/zoom-pan` | 9.9kB |
46
+ | `fullscreen/video` | 16.3kB |
47
+ | `fullscreen/lazy-load` | 13.1kB |
48
+ | `fullscreen/crossfade` | 181.0B |
49
+ | `fullscreen/thumbnails` | 160.0B |
50
+ | `Video` | 12.7kB |
51
+ | `ZoomPanImage` | 8.7kB |
52
+ | `media / toMediaItems` | 260.0B |
53
+ | `responsive / BREAKPOINT_MAP` | 85.0B |
22
54
  <!-- bundle-size:end -->
23
55
 
24
56
  ## Overview
@@ -43,6 +75,7 @@ Mental model:
43
75
  - `GalleryCore` and `useFullscreenController` power fullscreen behavior.
44
76
  - `Video` is the gallery-ready video primitive.
45
77
  - `ZoomPanImage` attaches click-to-zoom, drag pan, ctrl-wheel pinch, and touch pinch to one clipped image surface.
78
+ - `Skeleton` renders standalone placeholders or wraps real content with shared loading-layer timing.
46
79
 
47
80
  `MediaItem` accepts three shapes:
48
81
 
@@ -54,7 +87,8 @@ Mental model:
54
87
 
55
88
  ```typescript
56
89
  import "react-motion-gallery/styles.css";
57
- import { Slider, toMediaItems, type MediaItem } from "react-motion-gallery";
90
+ import { toMediaItems, type MediaItem } from "react-motion-gallery/media";
91
+ import { Slider } from "react-motion-gallery/slider";
58
92
 
59
93
  const items: MediaItem[] = toMediaItems([
60
94
  "https://picsum.photos/id/1015/1600/900",
@@ -84,7 +118,95 @@ export function QuickStart() {
84
118
 
85
119
  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
120
 
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`.
121
+ 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`.
122
+
123
+ 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.
124
+
125
+ | Entry point | Main surface |
126
+ | --- | --- |
127
+ | `react-motion-gallery/media` | `toMediaItems`, `MediaItem`, `MediaInput` |
128
+ | `react-motion-gallery/responsive` | `BREAKPOINT_MAP` and responsive value types |
129
+ | `react-motion-gallery/core` | `GalleryCore`, `GalleryCoreProvider`, `useGalleryCore` |
130
+ | `react-motion-gallery/slider` | `Slider`, `createSliderIndexChannel`, slider types |
131
+ | `react-motion-gallery/slider/ready` | `useSliderReady` |
132
+ | `react-motion-gallery/slider/arrows` | `sliderArrows` |
133
+ | `react-motion-gallery/slider/dots` | `sliderDots` |
134
+ | `react-motion-gallery/slider/progress` | `sliderProgress` |
135
+ | `react-motion-gallery/slider/scrollbar` | `sliderScrollbar` |
136
+ | `react-motion-gallery/slider/ripple` | `sliderRipple` |
137
+ | `react-motion-gallery/slider/auto-play` | `sliderAutoPlay` |
138
+ | `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll` |
139
+ | `react-motion-gallery/slider/auto-height` | `sliderAutoHeight` |
140
+ | `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad` |
141
+ | `react-motion-gallery/slider/parallax` | `sliderParallax` |
142
+ | `react-motion-gallery/slider/scale` | `sliderScale` |
143
+ | `react-motion-gallery/slider/fade` | `sliderFade` |
144
+ | `react-motion-gallery/slider/crossfade` | `sliderCrossfade` |
145
+ | `react-motion-gallery/slider/fullscreen` | `sliderFullscreen` |
146
+ | `react-motion-gallery/slider/loading` | `sliderLoading` |
147
+ | `react-motion-gallery/grid` | `Grid`, `Grid.Item`, grid types |
148
+ | `react-motion-gallery/grid/ready` | `useGridReady` |
149
+ | `react-motion-gallery/grid/lazy-load` | `gridLazyLoad` |
150
+ | `react-motion-gallery/masonry` | `Masonry`, `Masonry.Item`, masonry types |
151
+ | `react-motion-gallery/masonry/ready` | `useMasonryReady` |
152
+ | `react-motion-gallery/masonry/lazy-load` | `masonryLazyLoad` |
153
+ | `react-motion-gallery/entries` | `Entries`, `flattenEntries`, entry media container helpers |
154
+ | `react-motion-gallery/skeleton/base` | Standalone `Skeleton` and generic skeleton authoring types |
155
+ | `react-motion-gallery/skeleton/slider` | `SliderSkeleton` and slider skeleton authoring types |
156
+ | `react-motion-gallery/skeleton/grid` | `GridSkeleton` and grid skeleton authoring types |
157
+ | `react-motion-gallery/skeleton/masonry` | `MasonrySkeleton` and masonry skeleton authoring types |
158
+ | `react-motion-gallery/fullscreen` | `useFullscreenController` and fullscreen types |
159
+ | `react-motion-gallery/fullscreen/slider` | `fullscreenSlider` |
160
+ | `react-motion-gallery/fullscreen/controls` | `fullscreenControls` |
161
+ | `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions` |
162
+ | `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan` |
163
+ | `react-motion-gallery/fullscreen/video` | `fullscreenVideo` |
164
+ | `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad` |
165
+ | `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade` |
166
+ | `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails` |
167
+ | `react-motion-gallery/thumbnails` | `ThumbnailSlider`, thumbnail sync helpers |
168
+ | `react-motion-gallery/fullscreenThumbnails` | `FullscreenThumbnailSlider` |
169
+ | `react-motion-gallery/video` | `Video` and optional Plyr-backed video types |
170
+ | `react-motion-gallery/zoomPan` | `ZoomPanImage` and zoom/pan types |
171
+
172
+ ## Acknowledgements
173
+
174
+ 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.
175
+
176
+ See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md) for the preserved Embla Carousel copyright and MIT license notice.
177
+
178
+ ## Core
179
+
180
+ `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.
181
+
182
+ ### `GalleryCore` props
183
+
184
+ | Option | Type | Default | Notes |
185
+ | --- | --- | --- | --- |
186
+ | `children` | `React.ReactNode` | `—` | The gallery tree using the shared core. |
187
+ | `layout` | `"slider" \| "grid" \| "masonry" \| "entries"` | `—` | Declares the owning base layout. Omit it for standalone fullscreen/core usage. |
188
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Breakpoint map shared with descendants. |
189
+ | `fullscreenItems` | `MediaItem[] \| string[]` | `[]` | Normalized fullscreen media list. |
190
+ | `nodes` | `ReactNode \| ReactNode[]` | `—` | Advanced initial node list used by the slider-backed imperative state. |
191
+
192
+ ### `useGalleryCore` API
193
+
194
+ `GalleryApi` is the public alias for `GalleryCoreApi`. It covers core fullscreen state and programmatic fullscreen opening. Slider item mutation lives on `SliderHandle` and `SliderApi`.
195
+
196
+ | Field / Method | Type | Notes |
197
+ | --- | --- | --- |
198
+ | `layout` | `"slider" \| "grid" \| "masonry" \| "entries" \| null` | Current owning layout, or `null` for standalone fullscreen/core usage. |
199
+ | `effectiveBreakpoints` | `Record<string, number>` | Breakpoint map after merging custom `GalleryCore.breakpoints` with defaults. |
200
+ | `normalizedItems` | `MediaItem[]` | Fullscreen item list normalized from `fullscreenItems`. |
201
+ | `fsEnabled` | `boolean` | `true` when a mounted fullscreen controller has enabled fullscreen behavior. |
202
+ | `setFsEnabled` | `(enabled: boolean) => void` | Enables or disables fullscreen behavior. Usually handled by `useFullscreenController`. |
203
+ | `isFullscreenOpen` | `boolean` | `true` while fullscreen is open. |
204
+ | `isFullscreenOpenRef` | `React.RefObject<boolean>` | Ref mirror for handlers that need the current fullscreen-open state. |
205
+ | `setFullscreenOpen` | `(open: boolean) => void` | Updates fullscreen-open state. Usually handled by the fullscreen runtime. |
206
+ | `openFullscreenAt` | `({ index, method?, event? }) => void` | Opens fullscreen at a normalized fullscreen item index. Pass the source event for scale-origin detection. |
207
+ | `notifyBaseVisibleIndex` | `(index: number) => void` | Emits the visible base media index for fullscreen lazy-load/prewarm coordination. |
208
+ | `notifyFsVisibleIndex` | `(index: number) => void` | Emits the active fullscreen index back to base media. |
209
+ | `registerExpandableImage` | `(index: number, node: HTMLElement \| null) => void` | Registers an origin surface for layoutless scale transitions. |
88
210
 
89
211
  ## ZoomPanImage
90
212
 
@@ -108,10 +230,68 @@ export function ZoomPanCard() {
108
230
 
109
231
  `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
232
 
233
+ ## Skeleton
234
+
235
+ ```typescript
236
+ import { Skeleton, type SkeletonNode } from "react-motion-gallery/skeleton/base";
237
+
238
+ const shellSkeleton: SkeletonNode = {
239
+ kind: "rect",
240
+ style: { width: "100%", height: 320 },
241
+ };
242
+
243
+ export function LoadingShell({ ready, children }: { ready: boolean; children: React.ReactNode }) {
244
+ return (
245
+ <Skeleton
246
+ layout={shellSkeleton}
247
+ ready={ready}
248
+ timing={{ exitMs: 520, minVisibleMs: 220 }}
249
+ force={false}
250
+ ariaLabel={ready ? undefined : "Loading content"}
251
+ >
252
+ {children}
253
+ </Skeleton>
254
+ );
255
+ }
256
+ ```
257
+
258
+ `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.
259
+
260
+ | Option | Type | Default | Notes |
261
+ | --- | --- | --- | --- |
262
+ | `layout` | `SkeletonNode` | `—` | Structured placeholder layout tree. |
263
+ | `children` | `React.ReactNode` | `—` | Real content. When present, `Skeleton` renders content and loading layers. |
264
+ | `ready` | `boolean` | `false` | Reveals content and exits the skeleton once true. |
265
+ | `enabled` | `boolean` | `true` | Set false to render content immediately with no skeleton layer. |
266
+ | `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`. |
267
+ | `timing.exitMs` | `number` | `600` | Keeps the skeleton layer mounted for this long after exit starts and controls the opacity transition. |
268
+ | `timing.minVisibleMs` | `number` | `220` | Minimum time the skeleton stays visible before exit can begin. |
269
+ | `shellClassName` / `shellStyle` | `string` / `CSSProperties` | `—` | Wrapper-layer class and style for content+skeleton mode. |
270
+ | `contentClassName` / `contentStyle` | `string` / `CSSProperties` | `—` | Content-layer class and style for wrapper mode. |
271
+
272
+ 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.
273
+
274
+ ### Browser-measured skeleton text authoring
275
+
276
+ 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.
277
+
278
+ 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.
279
+
280
+ ```bash
281
+ npm run --silent generate:skeleton-text-module -- \
282
+ --input ./path/to/example.skeleton-text.browser.manifest.json \
283
+ --analysis-output ./path/to/example.skeleton-text.measurements.json
284
+ ```
285
+
286
+ 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.
287
+
111
288
  ## Slider
112
289
 
290
+ 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()`.
291
+
113
292
  ```typescript
114
- import { Slider } from "react-motion-gallery";
293
+ import { Slider } from "react-motion-gallery/slider";
294
+ import { sliderArrows } from "react-motion-gallery/slider/arrows";
115
295
 
116
296
  const slides = [
117
297
  "https://picsum.photos/id/1015/1600/900",
@@ -121,7 +301,7 @@ const slides = [
121
301
 
122
302
  export function BasicSlider() {
123
303
  return (
124
- <Slider>
304
+ <Slider plugins={[sliderArrows()]}>
125
305
  {slides.map((src, index) => (
126
306
  <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
127
307
  ))}
@@ -135,97 +315,89 @@ export function BasicSlider() {
135
315
  | Option | Type | Default | Notes |
136
316
  | --- | --- | --- | --- |
137
317
  | `children` | `React.ReactNode` | `—` | Slide content rendered in order. |
318
+ | `initialIndex` | `number` | `0` | Selects the slide index used for the first layout and intro fade-in. |
138
319
  | `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
320
  | `indexChannel` | `SliderIndexChannel` | internal channel | Share index state with thumbnails or sibling sliders. |
321
+ | `plugins` | `SliderPlugin[]` | `[]` | Explicit first-party slider features such as arrows, dots, auto-height, effects, fullscreen, or lazy-load. |
141
322
 
142
323
  ### Slider layout and scroll options
143
324
 
144
325
  | Option | Type | Default | Notes |
145
326
  | --- | --- | --- | --- |
146
- | `layout.gap` | `number` | `20` | Gap between cells. |
327
+ | `layout.gap` | `number \| Record<string, number>` | `20` | Responsive gap between cells. |
147
328
  | `layout.cellsPerSlide` | `number \| Record<string, number>` | `—` | Groups multiple cells into a slide page. |
148
329
  | `direction.dir` | `"ltr" \| "rtl"` | `"ltr"` | Text direction and arrow direction. |
149
330
  | `direction.axis` | `"x" \| "y"` | `"x"` | Horizontal or vertical slider axis. |
150
331
  | `align` | `"start" \| "center"` | `"start"` | Slide alignment inside the viewport. |
151
332
  | `scroll.groupCells` | `boolean` | `false` | Scrolls by grouped cells instead of every cell. |
152
- | `scroll.skipSnaps` | `boolean` | `false` | Allows momentum to skip snap points. |
333
+ | `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. |
334
+ | `scroll.strictSnaps` | `boolean` | `false` | Prevents one drag release from settling more than one snap away from where the drag started. Overrides `scroll.skipSnaps`. |
153
335
  | `scroll.freeScroll` | `boolean` | `false` | Enables free dragging instead of strict snapping. |
154
336
  | `scroll.loop` | `boolean` | `false` | Wraps around at the ends. |
155
337
 
156
- ### Slider element and lazy-load options
338
+ ### Slider element and plugin options
339
+
340
+ `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
341
 
158
342
  | Option | Type | Default | Notes |
159
343
  | --- | --- | --- | --- |
160
344
  | `elements.viewport` | `ElementStyle` | `—` | Class and inline style for the viewport element. |
161
345
  | `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
346
  | `transitions.intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
209
347
  | `transitions.intro.staggerMs` | `number` | `—` | Delay between item fade-ins. |
210
348
  | `transitions.intro.durationMs` | `number` | `—` | Intro fade duration. |
211
349
  | `transitions.intro.easing` | `string` | `—` | Intro fade easing. |
212
350
 
351
+ ### Slider plugins
352
+
353
+ 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.
354
+
355
+ ```typescript
356
+ import { Slider } from "react-motion-gallery/slider";
357
+ import { sliderArrows } from "react-motion-gallery/slider/arrows";
358
+ import { sliderParallax } from "react-motion-gallery/slider/parallax";
359
+
360
+ <Slider plugins={[sliderArrows(), sliderParallax({ bleedPct: "8%" })]}>
361
+ {slides}
362
+ </Slider>;
363
+ ```
364
+
365
+ | Import | Factory | Notes |
366
+ | --- | --- | --- |
367
+ | `react-motion-gallery/slider/arrows` | `sliderArrows(options)` | Previous/next arrows. |
368
+ | `react-motion-gallery/slider/dots` | `sliderDots(options)` | Pagination dots. |
369
+ | `react-motion-gallery/slider/progress` | `sliderProgress(options)` | Progress bar or custom progress renderer. |
370
+ | `react-motion-gallery/slider/scrollbar` | `sliderScrollbar(options)` | Range-style position control. |
371
+ | `react-motion-gallery/slider/ripple` | `sliderRipple(options)` | Enables ripple feedback for controls that call `createRipple`. |
372
+ | `react-motion-gallery/slider/auto-play` | `sliderAutoPlay(options)` | Timed slide changes. |
373
+ | `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll(options)` | Timed continuous advancement. |
374
+ | `react-motion-gallery/slider/auto-height` | `sliderAutoHeight(options)` | Measures active slide height and gates slider readiness until measured. |
375
+ | `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad(options)` | Adds lazy media attributes to slide images and videos. |
376
+ | `react-motion-gallery/slider/parallax` | `sliderParallax(options)` | Parallax slide wrapper. |
377
+ | `react-motion-gallery/slider/scale` | `sliderScale(options)` | Scales non-active slides. |
378
+ | `react-motion-gallery/slider/fade` | `sliderFade(options)` | Fades non-active slides. |
379
+ | `react-motion-gallery/slider/crossfade` | `sliderCrossfade(options)` | Enables crossfade-aware control navigation. |
380
+ | `react-motion-gallery/slider/fullscreen` | `sliderFullscreen()` | Bridges a `GalleryCore layout="slider"` slider to fullscreen. |
381
+ | `react-motion-gallery/slider/loading` | `sliderLoading(options)` | Basic custom loading overlay. Prefer `SliderSkeleton` for structured skeleton and restore. |
382
+
213
383
  ### Slider loading skeletons
214
384
 
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.
385
+ 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
386
 
217
387
  `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
388
 
219
389
  `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
390
 
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.
391
+ `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
392
 
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`.
393
+ `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
394
 
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.
395
+ When you provide `SliderSkeleton.timing`, `exitMs` controls both how long the loading layer remains mounted after exit starts and its opacity transition duration.
226
396
 
227
397
  ```typescript
228
- import { Slider } from "react-motion-gallery";
398
+ import { SliderSkeleton } from "react-motion-gallery/skeleton/slider";
399
+ import { Slider } from "react-motion-gallery/slider";
400
+ import { useSliderReady } from "react-motion-gallery/slider/ready";
229
401
 
230
402
  const slides = [
231
403
  { src: "https://picsum.photos/id/1020/660/960", width: 220, height: 320 },
@@ -234,47 +406,47 @@ const slides = [
234
406
  ];
235
407
 
236
408
  export function VariableWidthSkeletonSlider() {
409
+ const { ref: sliderRef, ready: sliderReady } = useSliderReady();
410
+
237
411
  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
- })),
412
+ <SliderSkeleton
413
+ ready={sliderReady}
414
+ layout={{
415
+ mode: "peek",
416
+ centering: "first",
417
+ visibleCount: 2,
418
+ layout: {
419
+ kind: "slider",
420
+ direction: "row",
421
+ style: { gap: 20 },
422
+ item: {
423
+ kind: "rect",
424
+ style: {
425
+ width: "100%",
426
+ height: "100%",
427
+ borderRadius: 12,
264
428
  },
265
429
  },
430
+ slots: slides.map((slide) => ({
431
+ itemWrapStyle: {
432
+ width: slide.width,
433
+ height: slide.height,
434
+ },
435
+ })),
266
436
  },
267
437
  }}
268
438
  >
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>
439
+ <Slider ref={sliderRef} align="center">
440
+ {slides.map((slide, index) => (
441
+ <img
442
+ key={slide.src}
443
+ src={slide.src}
444
+ alt={`Slide ${index + 1}`}
445
+ style={{ width: slide.width, height: slide.height, objectFit: "cover" }}
446
+ />
447
+ ))}
448
+ </Slider>
449
+ </SliderSkeleton>
278
450
  );
279
451
  }
280
452
  ```
@@ -285,6 +457,7 @@ export function VariableWidthSkeletonSlider() {
285
457
  | --- | --- | --- |
286
458
  | `mode` | `"fit" \| "peek"` | `"peek"` preserves partial next or previous slide visibility in the loading state. |
287
459
  | `centering` | `"first"` | Adds the leading spacer needed for the first visible slot when using the built-in centered peek skeleton flow. |
460
+ | `visibleCount` | `number \| Record<string, number>` | Responsive count of visible skeleton slots. |
288
461
  | `className` | `string \| undefined` | Applied to the skeleton overlay root. |
289
462
  | `style` | `React.CSSProperties \| undefined` | Inline styles for the skeleton overlay root. |
290
463
  | `layout` | `SliderSkeletonNode \| undefined` | Structured placeholder layout tree. Use `kind: "slider"` to model slide tracks. |
@@ -298,7 +471,7 @@ export function VariableWidthSkeletonSlider() {
298
471
  | --- | --- | --- |
299
472
  | `kind` | `"slider"` | Slider-specific skeleton layout root. |
300
473
  | `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`. |
474
+ | `count` | `number \| undefined` | Optional explicit slot count for the layout. Falls back to `visibleCount` on the surrounding slider skeleton spec. |
302
475
  | `item` | `SkeletonNode` | Default placeholder node rendered in each slot. |
303
476
  | `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Shared wrapper size, margin, border, and box-shadow rules for every slot. Border sizing is border-box. |
304
477
  | `slots` | `SliderSkeletonSlot[] \| undefined` | Per-slot overrides for variable widths, heights, aspect ratios, or custom placeholder nodes. |
@@ -312,23 +485,15 @@ export function VariableWidthSkeletonSlider() {
312
485
  | `item` | `SkeletonNode \| undefined` | Replaces the base `layout.item` for one slot. |
313
486
  | `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Merges on top of the base `layout.itemWrapStyle` for one slot, including wrapper borders and shadows. |
314
487
 
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.
488
+ `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
489
 
317
- ### Slider motion and effect options
490
+ ### Slider motion options
318
491
 
319
492
  | Option | Type | Default | Notes |
320
493
  | --- | --- | --- | --- |
321
494
  | `motion.selectDuration` | `number` | `25` | Duration for snapped selection motion. |
322
495
  | `motion.freeScrollDuration` | `number` | `43` | Duration for free-scroll settling. |
323
496
  | `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
497
 
333
498
  ### Slider render callback args
334
499
 
@@ -387,12 +552,22 @@ export function VariableWidthSkeletonSlider() {
387
552
  | `onSlidesBuilt` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when slide nodes are ready. |
388
553
  | `whenSlidesBuilt` | `() => Promise<HTMLElement[]>` | Promise form of `onSlidesBuilt`. |
389
554
  | `isSlidesBuilt` | `() => boolean` | `true` once the slide list is ready. |
555
+ | `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when the slider has built, measured, committed its index, and all plugin ready gates have cleared. |
556
+ | `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of `onReady`. |
557
+ | `isReady` | `() => boolean` | `true` once the settled slider ready signal has fired. |
390
558
  | `scrollNext` | `(mode?: IndexMode) => void` | Advances one step. |
391
559
  | `scrollPrev` | `(mode?: IndexMode) => void` | Moves backward one step. |
392
560
  | `canScrollNext` | `() => boolean` | Whether next navigation is available. |
393
561
  | `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
394
562
  | `scrollProgress` | `() => number` | Current progress from `0` to `1`. |
395
563
  | `cellsInView` | `() => number[]` | Canonical cell indexes currently visible. |
564
+ | `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
565
+ | `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
566
+ | `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
567
+ | `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
568
+ | `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
569
+ | `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
570
+ | `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
396
571
  | `getInternals` | `() => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping }` | Low-level internals used by fullscreen and advanced sync code. |
397
572
 
398
573
  ### `createSliderIndexChannel`
@@ -473,7 +648,7 @@ export function SliderWithThumbnails() {
473
648
  }
474
649
  ```
475
650
 
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`.
651
+ The component forwards a ref to its outer thumbnail shell.
477
652
 
478
653
  ### ThumbnailSlider component props
479
654
 
@@ -532,7 +707,7 @@ The component forwards a ref to its outer thumbnail shell. The explicit `layout`
532
707
  | Option | Type | Default | Notes |
533
708
  | --- | --- | --- | --- |
534
709
  | `transitions.loading.enabled` | `boolean` | `true` | Enables the thumbnail loading layer. |
535
- | `transitions.loading.force` | `boolean` | `false` | Forces the loading layer to remain visible. |
710
+ | `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
711
  | `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive count for the built-in loading placeholders. |
537
712
  | `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
713
  | `transitions.loading.elements.container` | `ElementStyle` | `—` | Class and inline style for the built-in loading overlay container. |
@@ -588,44 +763,145 @@ export function BasicGrid() {
588
763
 
589
764
  | Option | Type | Default | Notes |
590
765
  | --- | --- | --- | --- |
591
- | `children` | `React.ReactNode` | `—` | Grid items rendered in order. |
766
+ | `children` | `React.ReactNode` | `—` | Grid items rendered in order. Wrap individual cards in `Grid.Item` when they need custom spans or wrapper props. |
592
767
  | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
593
768
  | `gridItemBaseClass` | `string` | `"rmg__grid-item"` | Internal item base class override. |
594
769
  | `renderMode` | `"wrap" \| "passthrough"` | `"wrap"` | `wrap` adds an item wrapper; `passthrough` keeps child structure closer to the source node. |
595
770
 
771
+ ### Grid.Item props
772
+
773
+ `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.
774
+
775
+ | Option | Type | Default | Notes |
776
+ | --- | --- | --- | --- |
777
+ | `children` | `React.ReactNode` | `—` | The grid card content. |
778
+ | `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`. |
779
+ | `className` | `string` | `—` | Extra class name merged onto the grid item wrapper. |
780
+ | `style` | `React.CSSProperties` | `—` | Inline styles merged onto the grid item wrapper. |
781
+
596
782
  ### Grid options
597
783
 
598
784
  | Option | Type | Default | Notes |
599
785
  | --- | --- | --- | --- |
600
786
  | `columns` | `number \| Record<string, number>` | `—` | Fixed responsive column count. When omitted, Grid auto-fits using `minColumnWidth`. |
787
+ | `templateColumns` | `string \| Record<string, string>` | `—` | Explicit `grid-template-columns` value. Takes precedence over `columns` and `minColumnWidth`. |
601
788
  | `minColumnWidth` | `number \| string` | `160` | Minimum width used by auto-fit mode. |
602
789
  | `gap` | `number \| Record<string, number>` | `8` | Responsive grid gap. |
603
790
  | `rootClassName` | `string` | `—` | Class name for the grid root. |
604
791
  | `itemClassName` | `string` | `—` | Class name added to each wrapped grid item. |
605
792
  | `fullscreenTrigger` | `"item" \| "media"` | `"media"` | Opens fullscreen from the clicked media node or the entire item shell. |
606
- | `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
607
- | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
608
- | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
609
- | `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. |
793
+ | `plugins` | `GridPlugin[]` | `[]` | Explicit first-party Grid features such as lazy-load. |
616
794
  | `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. |
795
+ | `intro.staggerMs` | `number` | `60` | Reveal stagger for the fade-in. |
796
+ | `intro.durationMs` | `number` | `600` | Intro fade duration. |
619
797
  | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
620
798
  | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
621
799
 
622
- When `lazyLoad.enabled` is true, Grid rewrites trackable image `src` values into `data-rmg-lazy-src`, reveals them on viewport intersection, then fades them in after decode and spinner exit.
800
+ ### Grid plugins
801
+
802
+ Import Grid plugins from their own subpaths and pass them to `plugins`.
803
+
804
+ ```typescript
805
+ import { Grid } from "react-motion-gallery/grid";
806
+ import { gridLazyLoad } from "react-motion-gallery/grid/lazy-load";
807
+
808
+ <Grid plugins={[gridLazyLoad({ spinner: true })]}>{items}</Grid>;
809
+ ```
810
+
811
+ | Import | Factory | Notes |
812
+ | --- | --- | --- |
813
+ | `react-motion-gallery/grid/lazy-load` | `gridLazyLoad(options)` | Rewrites trackable image `src` values into `data-rmg-lazy-src`, reveals them on viewport intersection, then fades them in after decode and spinner exit. |
814
+
815
+ `gridLazyLoad()` enables lazy loading by default. Pass `{ enabled: false }` to make the plugin inert.
623
816
 
624
817
  Grid fullscreen behavior is provided by `GalleryCore` and `useFullscreenController`; Grid itself does not expose a ref-based imperative API.
625
818
 
626
- Grid skeleton `text` nodes use the same wrapped-line treatment as slider skeletons, including responsive `lines` maps and the configurable trailing `lineWidth`.
819
+ Wrap a card in `Grid.Item` when it should span tracks or needs wrapper styling:
820
+
821
+ ```typescript
822
+ <Grid columns={{ 0: 1, 720: 6, 1100: 12 }} gap={{ 0: 12, 1100: 18 }}>
823
+ <Grid.Item span={{ 0: "full", 720: 3, 1100: 6 }} className="feature-card">
824
+ <FeatureCard />
825
+ </Grid.Item>
826
+ <Grid.Item span={{ 0: "full", 720: 3, 1100: 3 }}>
827
+ <ProductCard />
828
+ </Grid.Item>
829
+ <Grid.Item span="full">
830
+ <WideEditorialCard />
831
+ </Grid.Item>
832
+ </Grid>
833
+ ```
834
+
835
+ 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.
836
+
837
+ Use `templateColumns` when the tracks themselves need custom proportions:
627
838
 
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.
839
+ ```typescript
840
+ <Grid
841
+ templateColumns={{
842
+ 0: "1fr",
843
+ 900: "minmax(0, 1.4fr) minmax(0, 1fr)",
844
+ 1200: "minmax(0, 2fr) repeat(2, minmax(0, 1fr))",
845
+ }}
846
+ gap={{ 0: 12, 1200: 18 }}
847
+ >
848
+ <Grid.Item span={{ 0: "full", 900: 2 }}>
849
+ <FeatureCard />
850
+ </Grid.Item>
851
+ </Grid>
852
+ ```
853
+
854
+ Grid no longer owns loading UI. Use `useGridReady` and wrap Grid with `GridSkeleton`, the same composition pattern used by Slider and Masonry.
855
+
856
+ 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`.
857
+
858
+ 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`.
859
+
860
+ 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.
861
+
862
+ ```typescript
863
+ import { Grid, useGridReady } from "react-motion-gallery";
864
+ import { GridSkeleton, type GridSkeletonSpec } from "react-motion-gallery/skeleton/grid";
865
+
866
+ const gridSkeleton: GridSkeletonSpec = {
867
+ radius: 14,
868
+ layout: {
869
+ kind: "grid",
870
+ count: 6,
871
+ item: {
872
+ kind: "rect",
873
+ style: { aspectRatio: "4 / 5" },
874
+ },
875
+ },
876
+ };
877
+
878
+ function GridWithSkeleton({ images }: { images: { src: string; alt: string }[] }) {
879
+ const { ref: gridRef, ready: gridReady } = useGridReady();
880
+
881
+ return (
882
+ <GridSkeleton
883
+ layout={gridSkeleton}
884
+ ready={gridReady}
885
+ timing={{ minVisibleMs: 220, exitMs: 600 }}
886
+ grid={{
887
+ count: images.length,
888
+ columns: { 0: 1, 640: 2, 960: 3 },
889
+ gap: { 0: 12, 960: 20 },
890
+ }}
891
+ >
892
+ <Grid
893
+ ref={gridRef}
894
+ columns={{ 0: 1, 640: 2, 960: 3 }}
895
+ gap={{ 0: 12, 960: 20 }}
896
+ >
897
+ {images.map((image) => (
898
+ <img key={image.src} src={image.src} alt={image.alt} />
899
+ ))}
900
+ </Grid>
901
+ </GridSkeleton>
902
+ );
903
+ }
904
+ ```
629
905
 
630
906
  ## Masonry
631
907
 
@@ -654,144 +930,166 @@ export function BasicMasonry() {
654
930
 
655
931
  | Option | Type | Default | Notes |
656
932
  | --- | --- | --- | --- |
657
- | `children` | `React.ReactNode` | `—` | Masonry items rendered in order. |
933
+ | `children` | `React.ReactNode` | `—` | Masonry items rendered in order. Wrap individual cards in `Masonry.Item` when they need custom spans or wrapper props. |
658
934
  | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
659
935
 
936
+ ### Masonry.Item props
937
+
938
+ | Option | Type | Default | Notes |
939
+ | --- | --- | --- | --- |
940
+ | `children` | `React.ReactNode` | `—` | The masonry card content. |
941
+ | `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. |
942
+ | `className` | `string` | `—` | Extra class name merged onto the masonry item wrapper. |
943
+ | `style` | `React.CSSProperties` | `—` | Inline styles merged onto the masonry item wrapper. |
944
+
660
945
  ### Masonry options
661
946
 
662
947
  | Option | Type | Default | Notes |
663
948
  | --- | --- | --- | --- |
664
949
  | `columns` | `number \| Record<string, number>` | `—` | Responsive column count. |
665
950
  | `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. |
951
+ | `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. |
952
+ | `fullscreenTrigger` | `"item" \| "media"` | `"media"` | Opens fullscreen from the clicked media node or the entire masonry item shell. |
668
953
  | `itemWrapClassName` | `string` | `—` | Class name added to the masonry item wrapper. |
669
954
  | `itemWrapStyle` | `React.CSSProperties` | `—` | Inline styles applied to the masonry item wrapper. |
670
955
  | `as` | `React.ElementType` | `"div"` | Root HTML element or custom component. |
671
956
  | `rootRef` | `React.Ref<HTMLDivElement>` | `—` | Ref to the masonry root. |
672
957
  | `classNames.root` | `string` | `—` | Root class name. |
673
- | `classNames.column` | `string` | `—` | Column class name. |
958
+ | `classNames.column` | `string` | `—` | Retained for backwards compatibility with the legacy column-wrapper renderer. |
674
959
  | `classNames.item` | `string` | `—` | Item class name. |
675
- | `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
676
- | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
677
- | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
678
- | `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. |
960
+ | `plugins` | `MasonryPlugin[]` | `[]` | Explicit first-party Masonry features such as lazy-load. |
683
961
  | `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. |
962
+ | `intro.staggerMs` | `number` | `160` | Reveal stagger for the fade-in. |
963
+ | `intro.durationMs` | `number` | `600` | Intro fade duration. |
686
964
  | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
687
965
  | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
688
966
 
689
- 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.
967
+ ### Masonry plugins
968
+
969
+ Import Masonry plugins from their own subpaths and pass them to `plugins`.
970
+
971
+ ```typescript
972
+ import { Masonry } from "react-motion-gallery/masonry";
973
+ import { masonryLazyLoad } from "react-motion-gallery/masonry/lazy-load";
974
+
975
+ <Masonry plugins={[masonryLazyLoad({ spinner: true })]}>{items}</Masonry>;
976
+ ```
977
+
978
+ | Import | Factory | Notes |
979
+ | --- | --- | --- |
980
+ | `react-motion-gallery/masonry/lazy-load` | `masonryLazyLoad(options)` | Uses the same image shell behavior as Slider: trackable image `src` values move into `data-rmg-lazy-src`, real images load on intersection, and items fade in after decode and spinner exit. |
690
981
 
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.
982
+ `masonryLazyLoad()` enables lazy loading by default. Pass `{ enabled: false }` to make the plugin inert.
692
983
 
693
- Masonry skeletons can now use a structured `layout` spec with the same inner node vocabulary as Grid skeletons, including `text` nodes and `itemWrapStyle`.
984
+ Masonry already accepts arbitrary React children, including text-containing JSX. The wrapper props are only for styling the built-in masonry item shell.
694
985
 
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.
986
+ Wrap a card in `Masonry.Item` when it needs its own span, wrapper `className`, or wrapper `style`:
696
987
 
697
988
  ```typescript
698
989
  <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)",
990
+ columns={{ 0: 1, 760: 2, 1160: 4 }}
991
+ gap={{ 0: 12, 1160: 18 }}
992
+ placement="horizontalOrder"
993
+ >
994
+ <Masonry.Item span={{ 0: 1, 760: 2, 1160: 2 }}>
995
+ <FeatureCard />
996
+ </Masonry.Item>
997
+ <Masonry.Item span={1}>
998
+ <StandardCard />
999
+ </Masonry.Item>
1000
+ </Masonry>
1001
+ ```
1002
+
1003
+ Choose a placement based on what should feel stable:
1004
+
1005
+ - `balanced`: best when visual balance and the shortest overall columns matter most.
1006
+ - `roundRobin`: best when deterministic column assignment matters more than tight packing.
1007
+ - `horizontalOrder`: best when wider cards should still read in a mostly left-to-right order.
1008
+
1009
+ Masonry no longer owns loading UI. Use `useMasonryReady` and wrap Masonry with `MasonrySkeleton`, the same composition pattern used by Slider and Grid.
1010
+
1011
+ 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`.
1012
+
1013
+ 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.
1014
+
1015
+ `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.
1016
+
1017
+ ```typescript
1018
+ import { Masonry, useMasonryReady } from "react-motion-gallery";
1019
+ import {
1020
+ MasonrySkeleton,
1021
+ type MasonrySkeletonSpec,
1022
+ } from "react-motion-gallery/skeleton/masonry";
1023
+
1024
+ const masonrySkeleton: MasonrySkeletonSpec = {
1025
+ ratios: [118, 126, 102, 146],
1026
+ layout: {
1027
+ kind: "masonry",
1028
+ itemWrapStyle: {
1029
+ padding: 14,
1030
+ borderRadius: 20,
1031
+ boxShadow: "0 18px 36px rgba(15, 23, 42, 0.08)",
1032
+ },
1033
+ item: {
1034
+ kind: "col",
1035
+ style: { gap: 12 },
1036
+ children: [
1037
+ {
1038
+ kind: "rect",
1039
+ style: { width: "100%", height: 180, borderRadius: 16 },
1040
+ },
1041
+ {
1042
+ kind: "text",
1043
+ barHeight: 14,
1044
+ lineHeight: 1.55,
1045
+ lines: 3,
1046
+ lastBarWidth: "74%",
1047
+ style: { width: "100%" },
715
1048
  },
1049
+ ],
1050
+ },
1051
+ slots: [
1052
+ {
1053
+ ratio: 182,
1054
+ span: { 0: 1, 1100: 2 },
716
1055
  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
- ],
1056
+ kind: "rect",
1057
+ style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 },
749
1058
  },
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
1059
  },
790
- },
791
- }}
792
- >
793
- {items}
794
- </Masonry>
1060
+ ],
1061
+ },
1062
+ };
1063
+
1064
+ function MasonryWithSkeleton({ items }: { items: React.ReactNode[] }) {
1065
+ const { ref: masonryRef, ready: masonryReady } = useMasonryReady();
1066
+
1067
+ return (
1068
+ <MasonrySkeleton
1069
+ layout={masonrySkeleton}
1070
+ ready={masonryReady}
1071
+ timing={{ minVisibleMs: 220, exitMs: 600 }}
1072
+ masonry={{
1073
+ count: items.length,
1074
+ columns: { 0: 1, 700: 2, 1100: 3 },
1075
+ gap: { 0: 12, 1100: 20 },
1076
+ placement: "balanced",
1077
+ }}
1078
+ >
1079
+ <Masonry
1080
+ ref={masonryRef}
1081
+ columns={{ 0: 1, 700: 2, 1100: 3 }}
1082
+ gap={{ 0: 12, 1100: 20 }}
1083
+ itemWrapStyle={{
1084
+ padding: "6px",
1085
+ borderRadius: "28px",
1086
+ }}
1087
+ >
1088
+ {items}
1089
+ </Masonry>
1090
+ </MasonrySkeleton>
1091
+ );
1092
+ }
795
1093
  ```
796
1094
 
797
1095
  ## Entries
@@ -863,6 +1161,16 @@ export function EntryGallery() {
863
1161
  }
864
1162
  ```
865
1163
 
1164
+ ### Entry loading, decode, and reveal flow
1165
+
1166
+ 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.
1167
+
1168
+ 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.
1169
+
1170
+ 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.
1171
+
1172
+ 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.
1173
+
866
1174
  ### `Entries` component props
867
1175
 
868
1176
  | Option | Type | Default | Notes |
@@ -886,11 +1194,14 @@ export function EntryGallery() {
886
1194
  | `mediaLayout` | `"slider" \| "grid" \| "masonry"` | `"slider"` | Declares the intended media layout. |
887
1195
  | `render.card` | `({ entry, entryIndex, media }) => ReactNode` | `—` | Wraps the media container in custom card UI. |
888
1196
  | `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. |
1197
+ | `render.overlay` | `({ entry, entryIndex, media, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
890
1198
  | `render.skeleton` | `({ entry, entryIndex }) => ReactNode` | `—` | Declared in the type, but the current runtime uses `loading.skeleton` instead. |
891
1199
  | `overlay` | `ElementStyle` | `—` | Styles the fullscreen overlay container that wraps `render.overlay`. |
1200
+ | `overlay.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"overlay"` | Selects whether fullscreen entry changes fade only the rendered overlay content or the whole overlay layer. |
1201
+ | `overlay.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen entry overlay crossfades. |
1202
+ | `overlay.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen entry overlay crossfades. |
892
1203
  | `loading.enabled` | `boolean` | `—` | Enables entry loading and decode gating. |
893
- | `loading.force` | `boolean` | `—` | Forces entry skeletons to remain visible. |
1204
+ | `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
1205
  | `loading.skeleton` | `EntrySkeletonSpec \| ((args) => EntrySkeletonSpec \| null \| undefined)` | `—` | Built-in skeleton spec or resolver. |
895
1206
  | `loading.minHeight` | `number \| string` | `"260px"` | Minimum reserved height while loading. |
896
1207
  | `loading.nearMargin` | `string` | `"700px 0px"` | Preload margin used before entries enter view. |
@@ -907,7 +1218,7 @@ export function EntryGallery() {
907
1218
  | `entryList` | `ElementStyle` | `—` | Styles the entry list container. |
908
1219
  | `entryRow` | `ElementStyle` | `—` | Styles each entry row container. |
909
1220
 
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`.
1221
+ 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
1222
 
912
1223
  ### Entry-related callback and helper types
913
1224
 
@@ -941,6 +1252,7 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
941
1252
  | --- | --- | --- |
942
1253
  | `entry` | `EntryItem` | Entry owning the active fullscreen slide. |
943
1254
  | `entryIndex` | `number` | Entry index. |
1255
+ | `media` | `MediaItem \| null` | Media item for the active fullscreen slide, when available. |
944
1256
  | `mediaIndex` | `number \| null` | Media index inside the entry when available. |
945
1257
  | `link` | `MediaEntryLink \| null` | Flattened link back to the entry/media pair. |
946
1258
  | `opacity` | `number` | Overlay opacity supplied by the runtime. |
@@ -981,9 +1293,86 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
981
1293
 
982
1294
  Fullscreen is compositional. `GalleryCore` owns the normalized fullscreen item list, your layout opens slides through that core, and `useFullscreenController` renders the portal UI.
983
1295
 
1296
+ ### Standalone fullscreen
1297
+
1298
+ 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.
1299
+
1300
+ ```typescript
1301
+ import * as React from "react";
1302
+ import { GalleryCore, useGalleryCore } from "react-motion-gallery/core";
1303
+ import { useFullscreenController } from "react-motion-gallery/fullscreen";
1304
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
1305
+ import { toMediaItems } from "react-motion-gallery/media";
1306
+
1307
+ const images = [
1308
+ {
1309
+ src: "https://picsum.photos/id/1015/1600/900",
1310
+ alt: "Mountain lake",
1311
+ },
1312
+ {
1313
+ src: "https://picsum.photos/id/1018/1600/900",
1314
+ alt: "Forest path",
1315
+ },
1316
+ ];
1317
+
1318
+ const fullscreenItems = toMediaItems(images);
1319
+
1320
+ function FullscreenPortal() {
1321
+ const { fullscreenNode } = useFullscreenController({
1322
+ plugins: [fullscreenSlider()],
1323
+ fullscreen: { enabled: true },
1324
+ });
1325
+
1326
+ return <>{fullscreenNode}</>;
1327
+ }
1328
+
1329
+ function ImageButton(props: {
1330
+ image: (typeof images)[number];
1331
+ index: number;
1332
+ }) {
1333
+ const gallery = useGalleryCore();
1334
+
1335
+ const open = (event: React.MouseEvent<HTMLButtonElement>) => {
1336
+ gallery.openFullscreenAt({
1337
+ index: props.index,
1338
+ event: event.nativeEvent,
1339
+ });
1340
+ };
1341
+
1342
+ return (
1343
+ <button type="button" onClick={open}>
1344
+ <img
1345
+ src={props.image.src}
1346
+ alt={props.image.alt}
1347
+ style={{
1348
+ display: "block",
1349
+ width: 180,
1350
+ aspectRatio: "16 / 9",
1351
+ objectFit: "cover",
1352
+ }}
1353
+ />
1354
+ </button>
1355
+ );
1356
+ }
1357
+
1358
+ export function StandaloneFullscreen() {
1359
+ return (
1360
+ <GalleryCore fullscreenItems={fullscreenItems}>
1361
+ {images.map((image, index) => (
1362
+ <ImageButton key={image.src} image={image} index={index} />
1363
+ ))}
1364
+ <FullscreenPortal />
1365
+ </GalleryCore>
1366
+ );
1367
+ }
1368
+ ```
1369
+
1370
+ ### Slider fullscreen
1371
+
984
1372
  ```typescript
985
1373
  import * as React from "react";
986
1374
  import { GalleryCore, Slider, useFullscreenController } from "react-motion-gallery";
1375
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
987
1376
 
988
1377
  const slides = [
989
1378
  "https://picsum.photos/id/1015/1600/900",
@@ -993,6 +1382,7 @@ const slides = [
993
1382
 
994
1383
  function FullscreenAddon() {
995
1384
  const { fullscreenNode } = useFullscreenController({
1385
+ plugins: [fullscreenSlider()],
996
1386
  fullscreen: { enabled: true },
997
1387
  });
998
1388
 
@@ -1013,13 +1403,28 @@ export function SliderWithFullscreen() {
1013
1403
  }
1014
1404
  ```
1015
1405
 
1406
+ ### Fullscreen lazy-load handshake
1407
+
1408
+ 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.
1409
+
1410
+ 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.
1411
+
1412
+ 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.
1413
+
1414
+ 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.
1415
+
1416
+ 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.
1417
+
1016
1418
  Add fullscreen thumbnails by rendering `FullscreenThumbnailSlider` with the bridge returned from `useFullscreenController`.
1017
1419
 
1018
1420
  ```typescript
1019
1421
  import { FullscreenThumbnailSlider, useFullscreenController } from "react-motion-gallery";
1422
+ import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
1423
+ import { fullscreenThumbnails } from "react-motion-gallery/fullscreen/thumbnails";
1020
1424
 
1021
1425
  function FullscreenWithThumbs({ thumbs }: { thumbs: string[] }) {
1022
1426
  const { fullscreenNode, fullscreenThumbnailBridge } = useFullscreenController({
1427
+ plugins: [fullscreenSlider(), fullscreenThumbnails()],
1023
1428
  fullscreen: {
1024
1429
  enabled: true,
1025
1430
  slider: {
@@ -1047,6 +1452,7 @@ Set `fullscreen.slider.direction` when fullscreen should mirror RTL interaction:
1047
1452
 
1048
1453
  ```typescript
1049
1454
  useFullscreenController({
1455
+ plugins: [fullscreenSlider()],
1050
1456
  fullscreen: {
1051
1457
  enabled: true,
1052
1458
  slider: {
@@ -1056,22 +1462,52 @@ useFullscreenController({
1056
1462
  });
1057
1463
  ```
1058
1464
 
1059
- ### `GalleryCore` props
1465
+ 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
1466
 
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. |
1467
+ ```typescript
1468
+ useFullscreenController({
1469
+ plugins: [fullscreenSlider()],
1470
+ fullscreen: {
1471
+ enabled: true,
1472
+ slider: {
1473
+ gap: { 0: 12, md: 20, 1200: 28 },
1474
+ },
1475
+ },
1476
+ });
1477
+ ```
1478
+
1479
+ 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:
1480
+
1481
+ ```typescript
1482
+ useFullscreenController({
1483
+ plugins: [fullscreenSlider(), fullscreenVideo()],
1484
+ fullscreen: {
1485
+ enabled: true,
1486
+ video: {
1487
+ playOnOpen: true,
1488
+ },
1489
+ },
1490
+ });
1491
+ ```
1068
1492
 
1069
1493
  ### `useFullscreenController` args
1070
1494
 
1071
1495
  | Option | Type | Default | Notes |
1072
1496
  | --- | --- | --- | --- |
1497
+ | `plugins` | `FullscreenPlugin[]` | `[]` | Explicit first-party fullscreen features. At minimum, import `fullscreenSlider()` to mount the fullscreen runtime. |
1073
1498
  | `fullscreen` | `FullscreenOptions` | `—` | Fullscreen behavior and rendering options. |
1074
1499
 
1500
+ | Import | Factory | Notes |
1501
+ | --- | --- | --- |
1502
+ | `react-motion-gallery/fullscreen/slider` | `fullscreenSlider(options)` | Mounts the fullscreen slider runtime and accepts `fullscreen.slider` options. |
1503
+ | `react-motion-gallery/fullscreen/controls` | `fullscreenControls(options)` | Option plugin for close, arrows, and counter options. Use with `fullscreenSlider()`. |
1504
+ | `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions(options)` | Adds caption rendering, placement, and caption motion runtime. Use with `fullscreenSlider()`. |
1505
+ | `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan(options)` | Adds fullscreen click zoom, pan, and pinch runtime. Use with `fullscreenSlider()`. |
1506
+ | `react-motion-gallery/fullscreen/video` | `fullscreenVideo(options)` | Adds fullscreen Plyr rendering, source/options, and `playOnOpen` runtime. Use with `fullscreenSlider()`. |
1507
+ | `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad(options)` | Adds fullscreen image and video lazy-load gates. Use with `fullscreenSlider()`. |
1508
+ | `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade(options)` | Option plugin for fullscreen crossfade controls, drag, and wheel behavior. Use with `fullscreenSlider()`. |
1509
+ | `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails()` | Option-only plugin for fullscreen thumbnail bridge behavior. Use with `fullscreenSlider()`. |
1510
+
1075
1511
  ### Recommended `useFullscreenController` return values
1076
1512
 
1077
1513
  | Field | Type | Notes |
@@ -1092,9 +1528,10 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1092
1528
  | --- | --- | --- | --- |
1093
1529
  | `enabled` | `boolean` | `false` | Master switch for fullscreen UI. |
1094
1530
  | `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>`. |
1531
+ | `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
1532
  | `video.source` | `(item: MediaItem, index: number) => Plyr.SourceInfo` | `—` | Builds fullscreen Plyr sources for video items. |
1097
1533
  | `video.options` | `Plyr.Options \| ((item: MediaItem, index: number) => Plyr.Options)` | `—` | Builds fullscreen Plyr options. |
1534
+ | `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
1535
  | `video.style` | `React.CSSProperties` | `—` | Fullscreen player inline style. |
1099
1536
  | `video.className` | `string` | `—` | Fullscreen player class. |
1100
1537
  | `controls.close.enabled` | `boolean` | `true` | Toggles the close button. |
@@ -1120,6 +1557,9 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1120
1557
  | `caption.breakpoint` | `number` | `—` | Viewport cutoff for switching placement logic. |
1121
1558
  | `caption.render` | `({ item, index, isZoomed }) => ReactNode` | `—` | Custom caption renderer. |
1122
1559
  | `caption.layout` | `"overlay" \| "slide"` | `—` | Chooses whether the caption overlays the media or lives in the slide layout. |
1560
+ | `caption.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"content"` | Selects whether overlay caption changes fade only the rendered caption content or the whole overlay layer. |
1561
+ | `caption.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen overlay caption crossfades. |
1562
+ | `caption.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen overlay caption crossfades. |
1123
1563
  | `caption.zoomFade` | `boolean` | `true` | Fades captions out on fullscreen zoom-in and back in on zoom-out. |
1124
1564
  | `caption.zoomFadeDurationMs` | `number` | `300` | Duration for fullscreen caption zoom fades. |
1125
1565
  | `caption.zoomFadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen caption zoom fades. |
@@ -1128,6 +1568,7 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1128
1568
  | `slider.duration` | `number` | `25` | Fullscreen slider motion duration. |
1129
1569
  | `slider.friction` | `number` | `0.68` | Fullscreen slider friction. |
1130
1570
  | `slider.direction` | `"ltr" \| "rtl"` | `"ltr"` | Fullscreen slider interaction direction. |
1571
+ | `slider.gap` | `number \| Record<string, number>` | `0` | Responsive pixel gap between fullscreen slides. Named keys resolve from `GalleryCore.breakpoints`. |
1131
1572
  | `zoom.clickZoomLevel` | `number` | `2.5` | Zoom level used for click-to-zoom. |
1132
1573
  | `zoom.maxZoomLevel` | `number` | `3` | Maximum allowed zoom level. |
1133
1574
  | `zoom.panDuration` | `number` | `43` | Pan settling duration. |
@@ -1135,18 +1576,27 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1135
1576
  | `effects.introDuration` | `number` | `300` | Open animation duration. |
1136
1577
  | `effects.introEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Open animation easing. |
1137
1578
  | `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. |
1579
+ | `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. |
1580
+ | `effects.crossfade.drag` | `boolean` | `false` | Scrubs adjacent fullscreen slides with crossfade during drag instead of moving the track. |
1581
+ | `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. |
1582
+ | `effects.crossfade.wheel.enabled` | `boolean` | `true` when object form is used | Enables or disables fullscreen wheel crossfade when using the object form. |
1583
+ | `effects.crossfade.wheel.sensitivity` | `number` | `5` | Multiplies wheel delta into virtual drag progress. Higher values reach the commit threshold sooner. |
1584
+ | `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`. |
1585
+ | `effects.crossfade.wheel.durationMs` | `number` | `effects.crossfade.durationMs` | Fade duration after fullscreen wheel crossfade commits. |
1586
+ | `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. |
1587
+ | `effects.crossfade.durationMs` | `number` | `120` | Shared fullscreen crossfade duration for controls, drag release, and wheel commit unless wheel overrides it. |
1588
+ | `effects.crossfade.easing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Shared fullscreen crossfade easing. |
1589
+ | `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
1590
  | `lazyLoad.images.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen images. |
1143
1591
  | `lazyLoad.images.spinnerClassName` | `string` | `—` | Spinner class for image slides. |
1144
1592
  | `lazyLoad.images.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for image slides. |
1145
- | `lazyLoad.videos.enabled` | `boolean` | `—` | Enables fullscreen video lazy loading. |
1593
+ | `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
1594
  | `lazyLoad.videos.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen videos. |
1147
1595
  | `lazyLoad.videos.spinnerClassName` | `string` | `—` | Spinner class for video slides. |
1148
1596
  | `lazyLoad.videos.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for video slides. |
1149
1597
 
1598
+ 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`.
1599
+
1150
1600
  ### Fullscreen callback and helper types
1151
1601
 
1152
1602
  #### `FsCounterArgs`
@@ -1255,37 +1705,6 @@ The hook returns additional refs and setters for the internal fullscreen runtime
1255
1705
  | `fadeDurationMs` | `number \| undefined` | Slot fade duration. |
1256
1706
  | `fadeEasing` | `string \| undefined` | Slot fade easing. |
1257
1707
 
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
1708
  ## Video
1290
1709
 
1291
1710
  `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.