react-motion-gallery 2.0.6 → 2.0.8

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 (63) hide show
  1. package/README.md +1029 -29
  2. package/dist/chunk-2AHLR3V4.mjs +1 -0
  3. package/dist/chunk-2UHS4WYL.mjs +5 -0
  4. package/dist/chunk-3HPXMD5O.mjs +10 -0
  5. package/dist/chunk-4JEELX5B.mjs +2 -0
  6. package/dist/chunk-A6MPGIEJ.mjs +1 -0
  7. package/dist/chunk-ESF6XBYF.mjs +1 -0
  8. package/dist/chunk-EV6ZK4QI.mjs +1 -0
  9. package/dist/chunk-H4BEIJAD.mjs +5 -0
  10. package/dist/chunk-HL75KJY3.mjs +2 -0
  11. package/dist/chunk-JYKEA7LO.mjs +1 -0
  12. package/dist/chunk-KA2TOUNO.mjs +59 -0
  13. package/dist/chunk-LPIHQAND.mjs +1 -0
  14. package/dist/chunk-LVYED5ZM.mjs +1 -0
  15. package/dist/chunk-NKBEYOBF.mjs +1 -0
  16. package/dist/chunk-P2GQPFSL.mjs +1 -0
  17. package/dist/chunk-TZGAHWM7.mjs +1 -0
  18. package/dist/chunk-UYTKRIYQ.mjs +1 -0
  19. package/dist/chunk-V35ILQJ4.mjs +4 -0
  20. package/dist/chunk-X4HEGEZV.mjs +1 -0
  21. package/dist/chunk-XO5LPPN6.mjs +2 -0
  22. package/dist/core.d.mts +86 -0
  23. package/dist/core.mjs +1 -0
  24. package/dist/elements-Bd1vm4Uk.d.mts +37 -0
  25. package/dist/entries.css +1 -0
  26. package/dist/entries.d.mts +77 -0
  27. package/dist/entries.mjs +1 -0
  28. package/dist/fullscreen.css +1 -0
  29. package/dist/fullscreen.d.mts +281 -0
  30. package/dist/fullscreen.mjs +1 -0
  31. package/dist/fullscreenThumbnails.css +1 -0
  32. package/dist/fullscreenThumbnails.d.mts +10 -0
  33. package/dist/fullscreenThumbnails.mjs +1 -0
  34. package/dist/grid.css +1 -0
  35. package/dist/grid.d.mts +126 -0
  36. package/dist/grid.mjs +1 -0
  37. package/dist/index-Bpj0ZM8C.d.mts +38 -0
  38. package/dist/index.css +1 -1
  39. package/dist/index.d.mts +23 -1164
  40. package/dist/index.mjs +1 -65
  41. package/dist/lazy-dGoYpcRa.d.mts +14 -0
  42. package/dist/masonry.css +1 -0
  43. package/dist/masonry.d.mts +73 -0
  44. package/dist/masonry.mjs +1 -0
  45. package/dist/media-moIXOhT1.d.mts +36 -0
  46. package/dist/metafile-esm.json +1 -1
  47. package/dist/plyrTypes-CmP9NWvX.d.mts +8 -0
  48. package/dist/responsive-CvE5dTnP.d.mts +5 -0
  49. package/dist/slider.css +1 -0
  50. package/dist/slider.d.mts +80 -0
  51. package/dist/slider.mjs +1 -0
  52. package/dist/sliderSub-DNikv2lm.d.mts +76 -0
  53. package/dist/thumbnails.css +1 -0
  54. package/dist/thumbnails.d.mts +68 -0
  55. package/dist/thumbnails.mjs +1 -0
  56. package/dist/types-Bi2iBbyG.d.mts +85 -0
  57. package/dist/types-CQ6I3EfZ.d.mts +109 -0
  58. package/dist/types-ChjyCquV.d.mts +158 -0
  59. package/dist/types-Dqm2ynv2.d.mts +262 -0
  60. package/dist/video.css +1 -0
  61. package/dist/video.d.mts +38 -0
  62. package/dist/video.mjs +1 -0
  63. package/package.json +34 -14
package/README.md CHANGED
@@ -1,46 +1,1046 @@
1
- # React Motion Gallery (RMG)
1
+ # react-motion-gallery
2
2
 
3
- A flexible React gallery component with four layouts:
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.
4
4
 
5
- - **Slider** (default)
6
- - **Grid**
7
- - **Masonry**
8
- - **Entries** (data-driven cards with media + overlays)
5
+ ## Overview
9
6
 
10
- Includes optional **Fullscreen mode**, **Thumbnails**, customizable **controls**, and an imperative **GalleryApi** for programmatic control.
7
+ Install the package, then add the optional video peers only if you use `Video`.
11
8
 
12
- ---
9
+ ```bash
10
+ npm install react-motion-gallery
11
+ npm install plyr plyr-react
12
+ ```
13
13
 
14
- ## Installation
14
+ Import the package stylesheet once. The published bundle ships CSS separately and does not side-effect import it for you.
15
15
 
16
- ```bash
17
- npm i react-motion-gallery
18
- # or
19
- yarn add react-motion-gallery
20
- # or
21
- pnpm add react-motion-gallery
16
+ ```tsx
17
+ import "react-motion-gallery/dist/index.css";
18
+ ```
19
+
20
+ Mental model:
21
+
22
+ - `Slider`, `Grid`, and `Masonry` render React children directly.
23
+ - `Entries` renders structured entry data with a custom media container.
24
+ - `GalleryCore` and `useFullscreenController` power fullscreen behavior.
25
+ - `Video` is the gallery-ready video primitive.
26
+
27
+ `MediaItem` accepts three shapes:
28
+
29
+ - image: `{ kind: "image", src, alt?, caption?, srcSet?, sizes?, width?, height? }`
30
+ - video: `{ kind: "video", src, poster?, alt?, caption? }`
31
+ - node: `{ kind: "node", node }`
32
+
33
+ `toMediaItems()` accepts string URLs, image/video objects, and node objects, then normalizes them into `MediaItem[]`. String URLs infer `kind` from the file extension.
34
+
35
+ ```tsx
36
+ import "react-motion-gallery/dist/index.css";
37
+ import { Slider, toMediaItems, type MediaItem } from "react-motion-gallery";
38
+
39
+ const items: MediaItem[] = toMediaItems([
40
+ "https://picsum.photos/id/1015/1600/900",
41
+ { src: "https://picsum.photos/id/1018/1600/900", alt: "Mountains" },
42
+ { kind: "node", node: <div>Custom slide</div> },
43
+ ]);
44
+
45
+ export function QuickStart() {
46
+ return (
47
+ <Slider>
48
+ {items.map((item, index) =>
49
+ item.kind === "image" ? (
50
+ <img
51
+ key={item.src}
52
+ src={item.src}
53
+ alt={item.alt ?? `Slide ${index + 1}`}
54
+ style={{ width: "100%", aspectRatio: "16 / 9", objectFit: "cover" }}
55
+ />
56
+ ) : item.kind === "node" ? (
57
+ <div key={index}>{item.node}</div>
58
+ ) : null
59
+ )}
60
+ </Slider>
61
+ );
62
+ }
63
+ ```
64
+
65
+ 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`.
66
+
67
+ 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`, and `react-motion-gallery/video`.
68
+
69
+ ## Slider
70
+
71
+ ```tsx
72
+ import { Slider } from "react-motion-gallery";
73
+
74
+ const slides = [
75
+ "https://picsum.photos/id/1015/1600/900",
76
+ "https://picsum.photos/id/1018/1600/900",
77
+ "https://picsum.photos/id/1024/1600/900",
78
+ ];
79
+
80
+ export function BasicSlider() {
81
+ return (
82
+ <Slider>
83
+ {slides.map((src, index) => (
84
+ <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
85
+ ))}
86
+ </Slider>
87
+ );
88
+ }
89
+ ```
90
+
91
+ ### Slider component props
92
+
93
+ | Option | Type | Default | Notes |
94
+ | --- | --- | --- | --- |
95
+ | `children` | `React.ReactNode` | `—` | Slide content rendered in order. |
96
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Merged with the internal breakpoint map for responsive values. |
97
+ | `expandableImageRefs` | `React.RefObject<(HTMLImageElement | null)[]>` | internal ref | Supplies origin images for fullscreen scale transitions. |
98
+ | `indexChannel` | `SliderIndexChannel` | internal channel | Share index state with thumbnails or sibling sliders. |
99
+
100
+ ### Slider layout and scroll options
101
+
102
+ | Option | Type | Default | Notes |
103
+ | --- | --- | --- | --- |
104
+ | `layout.gap` | `number` | `20` | Gap between cells. |
105
+ | `layout.cellsPerSlide` | `number \| Record<string, number>` | `—` | Groups multiple cells into a slide page. |
106
+ | `direction.dir` | `"ltr" \| "rtl"` | `"ltr"` | Text direction and arrow direction. |
107
+ | `direction.axis` | `"x" \| "y"` | `"x"` | Horizontal or vertical slider axis. |
108
+ | `align` | `"start" \| "center"` | `"start"` | Slide alignment inside the viewport. |
109
+ | `scroll.groupCells` | `boolean` | `false` | Scrolls by grouped cells instead of every cell. |
110
+ | `scroll.skipSnaps` | `boolean` | `false` | Allows momentum to skip snap points. |
111
+ | `scroll.freeScroll` | `boolean` | `false` | Enables free dragging instead of strict snapping. |
112
+ | `scroll.loop` | `boolean` | `false` | Wraps around at the ends. |
113
+
114
+ ### Slider element and lazy-load options
115
+
116
+ | Option | Type | Default | Notes |
117
+ | --- | --- | --- | --- |
118
+ | `elements.viewport` | `ElementStyle` | `—` | Class and inline style for the viewport element. |
119
+ | `elements.container` | `ElementStyle` | `—` | Class and inline style for the moving slider container. |
120
+ | `lazyLoad.enabled` | `boolean` | `false` | Enables slide-level lazy image and video loading. |
121
+ | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `true` | `false` disables the built-in spinner. |
122
+ | `lazyLoad.spinnerClassName` | `string` | `""` | Applied to the spinner wrapper. |
123
+ | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `{}` | Inline styles for the spinner wrapper. |
124
+
125
+ ### Slider control options
126
+
127
+ | Option | Type | Default | Notes |
128
+ | --- | --- | --- | --- |
129
+ | `controls.arrows.enabled` | `boolean` | `true` | Toggles previous and next arrows. |
130
+ | `controls.arrows.arrow` | `ElementStyle` | `{}` | Shared arrow class and style. |
131
+ | `controls.arrows.prev` | `ElementStyle` | `{}` | Previous-arrow override. |
132
+ | `controls.arrows.next` | `ElementStyle` | `{}` | Next-arrow override. |
133
+ | `controls.arrows.render` | `(args) => ReactNode` | `—` | Custom renderer for both arrows. |
134
+ | `controls.arrows.renderPrev` | `(args) => ReactNode` | `—` | Custom previous arrow. |
135
+ | `controls.arrows.renderNext` | `(args) => ReactNode` | `—` | Custom next arrow. |
136
+ | `controls.dots.enabled` | `boolean` | `true` | Toggles pagination dots. |
137
+ | `controls.dots.root` | `ElementStyle` | `{}` | Dot container class and style. |
138
+ | `controls.dots.dot` | `ElementStyle` | `{}` | Individual dot class and style. |
139
+ | `controls.dots.render` | `(args) => ReactNode` | `—` | Full custom dots UI. |
140
+ | `controls.progress.enabled` | `boolean` | `false` | Toggles the progress bar. |
141
+ | `controls.progress.root` | `ElementStyle` | `{}` | Progress track class and style. |
142
+ | `controls.progress.bar` | `ElementStyle` | `{}` | Progress fill class and style. |
143
+ | `controls.progress.render` | `(args) => ReactNode` | `—` | Full custom progress UI. |
144
+ | `controls.ripple.enabled` | `boolean` | `true` | Toggles control ripple feedback. |
145
+ | `controls.ripple.className` | `string` | `""` | Custom ripple class. |
146
+
147
+ ### Slider auto and transition options
148
+
149
+ | Option | Type | Default | Notes |
150
+ | --- | --- | --- | --- |
151
+ | `auto.play.enabled` | `boolean` | `false` | Timed slide changes. |
152
+ | `auto.play.speedMs` | `number` | `3000` | Delay between autoplay advances. |
153
+ | `auto.play.pauseMs` | `number` | `1000` | Delay after interaction before autoplay resumes. |
154
+ | `auto.play.pauseOnHover` | `boolean` | `true` | Pauses autoplay while hovering. |
155
+ | `auto.scroll.enabled` | `boolean` | `false` | Continuous timed scrolling. |
156
+ | `auto.scroll.speedMs` | `number` | `3000` | Interval for auto-scroll movement. |
157
+ | `auto.scroll.pauseMs` | `number` | `1000` | Delay after interaction before auto-scroll resumes. |
158
+ | `auto.scroll.pauseOnHover` | `boolean` | `true` | Pauses while hovering. |
159
+ | `transitions.loading.enabled` | `boolean` | `—` | Enables the loading skeleton layer. |
160
+ | `transitions.loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
161
+ | `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive skeleton slot count. |
162
+ | `transitions.loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
163
+ | `transitions.loading.skeleton` | `SliderSkeletonSpec` | `—` | Built-in skeleton spec. |
164
+ | `transitions.intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
165
+ | `transitions.intro.staggerMs` | `number` | `—` | Delay between item reveals. |
166
+ | `transitions.intro.transform` | `number \| string` | `—` | Initial intro transform. |
167
+ | `transitions.intro.durationMs` | `number` | `—` | Intro duration. |
168
+ | `transitions.intro.easing` | `string` | `—` | Intro easing. |
169
+
170
+ ### Slider motion and effect options
171
+
172
+ | Option | Type | Default | Notes |
173
+ | --- | --- | --- | --- |
174
+ | `motion.selectDuration` | `number` | `25` | Duration for snapped selection motion. |
175
+ | `motion.freeScrollDuration` | `number` | `43` | Duration for free-scroll settling. |
176
+ | `motion.friction` | `number` | `0.68` | Drag and settling friction. |
177
+ | `effects.parallax.enabled` | `boolean` | `—` | Enables the parallax slide treatment. |
178
+ | `effects.parallax.bleedPct` | `string` | `—` | Extra image bleed around the viewport. |
179
+ | `effects.parallax.borderRadius` | `string` | `—` | Radius for the parallax frame. |
180
+ | `effects.parallax.sideWidth` | `string` | `—` | Side crop width used by the effect. |
181
+ | `effects.scale.enabled` | `boolean` | `—` | Scales neighboring slides. |
182
+ | `effects.scale.amount` | `number` | `—` | Scale multiplier for the scale effect. |
183
+ | `effects.fade.enabled` | `boolean` | `—` | Fades slides based on position. |
184
+
185
+ ### Slider render callback args
186
+
187
+ #### `ArrowRenderArgs`
188
+
189
+ | Field | Type | Notes |
190
+ | --- | --- | --- |
191
+ | `ref` | `React.RefObject<HTMLDivElement \| null>` | Attach to the arrow root. |
192
+ | `onClick` | `() => void` | Calls the built-in previous or next action. |
193
+ | `hidden` | `boolean` | `true` when the arrow should not render visually. |
194
+ | `disabled` | `boolean` | `true` when navigation is unavailable. |
195
+ | `createRipple` | `(el: HTMLElement) => void` | Triggers the built-in ripple effect manually. |
196
+ | `className` | `string \| undefined` | Resolved class name for the arrow root. |
197
+
198
+ #### `DotsRenderArgs`
199
+
200
+ | Field | Type | Notes |
201
+ | --- | --- | --- |
202
+ | `ref` | `React.RefObject<HTMLDivElement \| null>` | Attach to the dots root. |
203
+ | `count` | `number` | Dot count. |
204
+ | `activeIndex` | `number` | Current selected slide index. |
205
+ | `hidden` | `boolean` | `true` when dots should be hidden. |
206
+ | `goTo` | `(index: number) => void` | Navigate to a slide. |
207
+ | `getDotRef` | `(index: number) => (el: HTMLDivElement \| null) => void` | Ref factory for each dot. |
208
+ | `createRipple` | `(el: HTMLElement) => void` | Manual ripple trigger. |
209
+ | `classNameContainer` | `string \| undefined` | Resolved root class name. |
210
+ | `classNameDot` | `string \| undefined` | Resolved dot class name. |
211
+
212
+ #### `ProgressRenderArgs`
213
+
214
+ | Field | Type | Notes |
215
+ | --- | --- | --- |
216
+ | `ref` | `React.Ref<HTMLDivElement>` | Attach to the progress root. |
217
+ | `innerRef` | `React.Ref<HTMLDivElement> \| undefined` | Attach to the fill element. |
218
+ | `hidden` | `boolean` | `true` when the progress bar should be hidden. |
219
+ | `progress` | `number` | Progress value from `0` to `1`. |
220
+ | `axis` | `"x" \| "y"` | Fill direction. |
221
+ | `className` | `string \| undefined` | Root class name. |
222
+ | `style` | `React.CSSProperties \| undefined` | Root inline style. |
223
+ | `innerClassName` | `string \| undefined` | Fill class name. |
224
+ | `innerStyle` | `React.CSSProperties \| undefined` | Fill inline style. |
225
+
226
+ ### `SliderHandle` methods
227
+
228
+ | Method | Signature | Notes |
229
+ | --- | --- | --- |
230
+ | `centerSlider` | `() => void` | Re-centers the slider after layout changes. |
231
+ | `getIndex` | `() => number` | Current active slide index. |
232
+ | `setIndex` | `(i: number, mode?: IndexMode) => void` | Jumps or animates to a slide. |
233
+ | `subscribeIndex` | `(fn: () => void) => () => void` | Subscribes to index changes. |
234
+ | `slideIndexForCell` | `(cellIndex: number) => number` | Maps a cell index to its slide index when using grouped cells. |
235
+ | `getRootNode` | `() => HTMLElement \| null` | Outer slider root. |
236
+ | `getContainerNode` | `() => HTMLElement \| null` | Moving slide container. |
237
+ | `getSlideNodes` | `() => HTMLElement[]` | Current slide elements. |
238
+ | `getViewportNode` | `() => HTMLDivElement \| null` | Scroll viewport. |
239
+ | `onSlidesBuilt` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when slide nodes are ready. |
240
+ | `whenSlidesBuilt` | `() => Promise<HTMLElement[]>` | Promise form of `onSlidesBuilt`. |
241
+ | `isSlidesBuilt` | `() => boolean` | `true` once the slide list is ready. |
242
+ | `scrollNext` | `(mode?: IndexMode) => void` | Advances one step. |
243
+ | `scrollPrev` | `(mode?: IndexMode) => void` | Moves backward one step. |
244
+ | `canScrollNext` | `() => boolean` | Whether next navigation is available. |
245
+ | `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
246
+ | `scrollProgress` | `() => number` | Current progress from `0` to `1`. |
247
+ | `cellsInView` | `() => number[]` | Canonical cell indexes currently visible. |
248
+ | `getInternals` | `() => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping }` | Low-level internals used by fullscreen and advanced sync code. |
249
+
250
+ ### `createSliderIndexChannel`
251
+
252
+ ```tsx
253
+ import { Slider, createSliderIndexChannel } from "react-motion-gallery";
254
+
255
+ const channel = createSliderIndexChannel();
256
+
257
+ export function SharedIndexSlider() {
258
+ return (
259
+ <Slider indexChannel={channel}>
260
+ <div>One</div>
261
+ <div>Two</div>
262
+ <div>Three</div>
263
+ </Slider>
264
+ );
265
+ }
266
+ ```
267
+
268
+ | Method | Signature | Notes |
269
+ | --- | --- | --- |
270
+ | `createSliderIndexChannel` | `(initialIndex = 0, initialMode = "animated") => SliderIndexChannel` | Creates a shared index event bus. |
271
+ | `get` | `() => { index: number; mode: IndexMode }` | Reads the stored index and mode. |
272
+ | `set` | `(next: number, mode?: IndexMode, opts?: { silent?: boolean }) => void` | Sets the current index and emits a `"set"` event unless silenced. |
273
+ | `bump` | `(delta: number, mode?: IndexMode, opts?: { silent?: boolean }) => void` | Emits a relative index change event. |
274
+ | `subscribe` | `(fn: () => void) => () => void` | Subscribes to channel updates. |
275
+ | `onEvent` | `(fn: (ev: IndexEvent) => void) => () => void` | Receives the last `"set"` or `"bump"` event payload. |
276
+ | `onBasePointerDown` | `(fn: () => void) => () => void` | Subscribes to base slider pointer-down events. |
277
+ | `emitBasePointerDown` | `() => void` | Broadcasts a pointer-down event to subscribers. |
278
+
279
+ ### ThumbnailSlider
280
+
281
+ Use `ThumbnailSlider` when you want a synced thumbnail rail for a base `Slider`. In the common case, share one `createSliderIndexChannel()` instance and pass it to both components.
282
+
283
+ ```tsx
284
+ import {
285
+ Slider,
286
+ ThumbnailSlider,
287
+ createSliderIndexChannel,
288
+ } from "react-motion-gallery";
289
+
290
+ const slides = [
291
+ "https://picsum.photos/id/1015/1600/900",
292
+ "https://picsum.photos/id/1018/1600/900",
293
+ "https://picsum.photos/id/1024/1600/900",
294
+ ];
295
+
296
+ const channel = createSliderIndexChannel();
297
+
298
+ export function SliderWithThumbnails() {
299
+ return (
300
+ <>
301
+ <Slider indexChannel={channel}>
302
+ {slides.map((src, index) => (
303
+ <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
304
+ ))}
305
+ </Slider>
306
+ <ThumbnailSlider
307
+ indexChannel={channel}
308
+ options={{
309
+ layout: { position: "bottom", gap: 8, thumbnail: { width: 88, height: 56 } },
310
+ scroll: { centerActiveThumb: true },
311
+ controls: { enabled: true },
312
+ }}
313
+ >
314
+ {slides.map((src, index) => (
315
+ <img
316
+ key={`thumb-${src}`}
317
+ src={src}
318
+ alt={`Thumbnail ${index + 1}`}
319
+ style={{ width: "100%", height: "100%", objectFit: "cover" }}
320
+ />
321
+ ))}
322
+ </ThumbnailSlider>
323
+ </>
324
+ );
325
+ }
326
+ ```
327
+
328
+ The component forwards a ref to its outer thumbnail shell. The explicit `layout`, `scroll`, and `motion` defaults below are also exported as `DEFAULT_THUMBNAILS`.
329
+
330
+ ### ThumbnailSlider component props
331
+
332
+ | Option | Type | Default | Notes |
333
+ | --- | --- | --- | --- |
334
+ | `children` | `React.ReactNode` | `—` | Thumbnail nodes rendered in order. Overrides `options.children` when both are provided. |
335
+ | `options` | `ThumbnailsOptions` | `—` | Base thumbnail configuration object. |
336
+ | `indexChannel` | `SliderIndexChannel` | internal channel | Share the same channel as a base `Slider` to keep selection in sync. |
337
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve `layout.position` and responsive loading counts. |
338
+ | `onThumbnailClick` | `(index: number) => void` | `—` | Fired when a thumbnail click publishes a selection to the shared channel. |
339
+ | `onReadyChange` | `(ready: boolean) => void` | `—` | Fired when the thumbnail rail finishes or re-enters its loading/layout cycle. |
340
+ | `direction` | `"ltr" \| "rtl"` | `"ltr"` | Affects horizontal arrow direction and RTL scroll behavior. |
341
+
342
+ ### Thumbnail layout and scroll options
343
+
344
+ | Option | Type | Default | Notes |
345
+ | --- | --- | --- | --- |
346
+ | `children` | `React.ReactNode` | `—` | Fallback thumbnail content when component children are omitted. |
347
+ | `layout.position` | `ResponsivePosition` | `"bottom"` | Thumbnail rail position: `"top"`, `"right"`, `"bottom"`, or `"left"`. |
348
+ | `layout.gap` | `number` | `8` | Gap between thumbnails. |
349
+ | `layout.center` | `boolean` | `false` | Centers the overall rail content within its container when possible. |
350
+ | `layout.thumbnail.width` | `number \| string` | `—` | Width for each thumbnail item. |
351
+ | `layout.thumbnail.height` | `number \| string` | `—` | Height for each thumbnail item. |
352
+ | `layout.container.width` | `number \| string` | `—` | Width for the outer thumbnail container. |
353
+ | `layout.container.height` | `number \| string` | `—` | Height for the outer thumbnail container. |
354
+ | `scroll.freeScroll` | `boolean` | `true` | Enables drag or wheel movement without strict snapping. |
355
+ | `scroll.groupCells` | `boolean` | `false` | Pages the rail by grouped thumbnail cells. |
356
+ | `scroll.loop` | `boolean` | `false` | Wraps thumbnails at the ends. |
357
+ | `scroll.skipSnaps` | `boolean` | `false` | Allows momentum to skip snap points. |
358
+ | `scroll.centerActiveThumb` | `boolean` | `false` | Repositions the rail to keep the active thumbnail centered. |
359
+
360
+ `ResponsivePosition` accepts a single side, an array, or a breakpoint map. For arrays, the first entry is used.
361
+
362
+ ### Thumbnail element, control, and motion options
363
+
364
+ | Option | Type | Default | Notes |
365
+ | --- | --- | --- | --- |
366
+ | `elements.container` | `ElementStyle` | `—` | Class and inline style for the outer thumbnail container. |
367
+ | `elements.thumbnail` | `ElementStyle` | `—` | Class and inline style for each thumbnail item shell. |
368
+ | `controls.enabled` | `boolean` | `false` | Shows previous and next arrows when the rail overflows. |
369
+ | `controls.arrow` | `ElementStyle` | `—` | Shared arrow class and style. |
370
+ | `controls.prev` | `ElementStyle` | `—` | Previous-arrow override. |
371
+ | `controls.next` | `ElementStyle` | `—` | Next-arrow override. |
372
+ | `controls.render` | `(args: ArrowRenderArgs & { dir: "prev" \| "next" }) => ReactNode` | `—` | Custom renderer for both thumbnail arrows. |
373
+ | `controls.renderPrev` | `(args: ArrowRenderArgs) => ReactNode` | `—` | Custom previous arrow. |
374
+ | `controls.renderNext` | `(args: ArrowRenderArgs) => ReactNode` | `—` | Custom next arrow. |
375
+ | `controls.ripple.enabled` | `boolean` | `true` | Enables ripple feedback for thumbnail arrows. |
376
+ | `controls.ripple.className` | `string` | `—` | Custom ripple class for the arrow feedback element. |
377
+ | `motion.selectDuration` | `number` | `25` | Duration for snapped thumbnail selection motion. |
378
+ | `motion.freeScrollDuration` | `number` | `43` | Duration for free-scroll settling. |
379
+ | `motion.friction` | `number` | `0.68` | Drag and settling friction. |
380
+ | `breakpointMap` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Override map used for responsive thumbnail positions and loading counts. |
381
+
382
+ ### Thumbnail transition options
383
+
384
+ | Option | Type | Default | Notes |
385
+ | --- | --- | --- | --- |
386
+ | `transitions.loading.enabled` | `boolean` | `true` | Enables the thumbnail loading layer. |
387
+ | `transitions.loading.force` | `boolean` | `false` | Forces the loading layer to remain visible. |
388
+ | `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive count for the built-in loading placeholders. |
389
+ | `transitions.loading.renderLoading` | `() => ReactNode` | `—` | Replaces the built-in thumbnail loading skeleton. |
390
+ | `transitions.intro.renderIntro` | `({ active, containerProps }, inner) => ReactNode` | `—` | Custom intro wrapper for the thumbnail rail. |
391
+ | `transitions.intro.staggerMs` | `number` | `40` | Delay between thumbnail reveals. |
392
+ | `transitions.intro.transform` | `string` | `"10px"` | Starting translate offset used by the default intro. |
393
+ | `transitions.intro.durationMs` | `number` | `300` | Intro duration. |
394
+ | `transitions.intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro easing. |
395
+
396
+ ### `createThumbnailSyncBridge`
397
+
398
+ `ThumbnailSlider` creates and starts this bridge for you internally when you pass `indexChannel`. Reach for `createThumbnailSyncBridge()` only when you need to wire a local thumbnail rail to an external slider channel manually.
399
+
400
+ | Method | Signature | Notes |
401
+ | --- | --- | --- |
402
+ | `createThumbnailSyncBridge` | `(args: { localChannel, externalChannel?, clampIndex? }) => ThumbnailSyncBridge` | Creates a bridge between local thumbnail state and an optional external slider channel. |
403
+ | `start` | `() => () => void` | Starts syncing and returns a cleanup function. |
404
+ | `stop` | `() => void` | Stops syncing without disposing the channels. |
405
+ | `publishThumbnailClick` | `(index: number, mode?: IndexMode) => void` | Publishes a thumbnail click to the external slider channel. |
406
+
407
+ ## Grid
408
+
409
+ ```tsx
410
+ import { Grid } from "react-motion-gallery";
411
+
412
+ const images = Array.from({ length: 6 }, (_, index) => ({
413
+ src: `https://picsum.photos/seed/grid-${index}/1200/1200`,
414
+ alt: `Grid item ${index + 1}`,
415
+ }));
416
+
417
+ export function BasicGrid() {
418
+ return (
419
+ <Grid columns={{ 0: 1, 640: 2, 960: 3 }} gap={{ 0: 12, 960: 20 }}>
420
+ {images.map((image) => (
421
+ <img key={image.src} src={image.src} alt={image.alt} style={{ width: "100%" }} />
422
+ ))}
423
+ </Grid>
424
+ );
425
+ }
426
+ ```
427
+
428
+ ### Grid component props
429
+
430
+ | Option | Type | Default | Notes |
431
+ | --- | --- | --- | --- |
432
+ | `children` | `React.ReactNode` | `—` | Grid items rendered in order. |
433
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
434
+ | `gridItemBaseClass` | `string` | `"rmg__grid-item"` | Internal item base class override. |
435
+ | `renderMode` | `"wrap" \| "passthrough"` | `"wrap"` | `wrap` adds an item wrapper; `passthrough` keeps child structure closer to the source node. |
436
+
437
+ ### Grid options
438
+
439
+ | Option | Type | Default | Notes |
440
+ | --- | --- | --- | --- |
441
+ | `columns` | `number \| Record<string, number>` | `—` | Fixed responsive column count. When omitted, Grid auto-fits using `minColumnWidth`. |
442
+ | `minColumnWidth` | `number \| string` | `160` | Minimum width used by auto-fit mode. |
443
+ | `gap` | `number \| Record<string, number>` | `8` | Responsive grid gap. |
444
+ | `rootClassName` | `string` | `—` | Class name for the grid root. |
445
+ | `itemClassName` | `string` | `—` | Class name added to each wrapped grid item. |
446
+ | `fullscreenTrigger` | `"item" \| "media"` | `"media"` | Opens fullscreen from the clicked media node or the entire item shell. |
447
+ | `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
448
+ | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
449
+ | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
450
+ | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
451
+ | `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
452
+ | `loading.force` | `boolean` | `—` | Keeps the loading layer visible even when media is ready. |
453
+ | `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
454
+ | `loading.skeleton` | `GridSkeletonSpec` | `—` | Built-in grid skeleton spec. |
455
+ | `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
456
+ | `intro.staggerMs` | `number` | `40` | Reveal stagger. |
457
+ | `intro.transform` | `string` | `"translateY(10px) scale(0.99)"` | Starting transform. |
458
+ | `intro.durationMs` | `number` | `300` | Intro duration. |
459
+ | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro easing. |
460
+ | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
461
+
462
+ Grid fullscreen behavior is provided by `GalleryCore` and `useFullscreenController`; Grid itself does not expose a ref-based imperative API.
463
+
464
+ ## Masonry
465
+
466
+ ```tsx
467
+ import { Masonry } from "react-motion-gallery";
468
+
469
+ const cards = [280, 360, 220, 420, 300, 340];
470
+
471
+ export function BasicMasonry() {
472
+ return (
473
+ <Masonry columns={{ 0: 1, 700: 2, 1100: 3 }} gap={{ 0: 12, 1100: 20 }}>
474
+ {cards.map((height, index) => (
475
+ <img
476
+ key={index}
477
+ src={`https://picsum.photos/seed/masonry-${index}/1000/${height * 3}`}
478
+ alt={`Masonry item ${index + 1}`}
479
+ style={{ width: "100%", height, objectFit: "cover", borderRadius: 12 }}
480
+ />
481
+ ))}
482
+ </Masonry>
483
+ );
484
+ }
22
485
  ```
23
486
 
24
- ## Core rule: wrap every item in a div
487
+ ### Masonry component props
488
+
489
+ | Option | Type | Default | Notes |
490
+ | --- | --- | --- | --- |
491
+ | `children` | `React.ReactNode` | `—` | Masonry items rendered in order. |
492
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
493
+
494
+ ### Masonry options
25
495
 
26
- RMG treats each direct child wrapper as an item.
27
- That means every image/video must be inside a div:
496
+ | Option | Type | Default | Notes |
497
+ | --- | --- | --- | --- |
498
+ | `columns` | `number \| Record<string, number>` | `—` | Responsive column count. |
499
+ | `gap` | `number \| Record<string, number>` | `—` | Responsive gap between columns and items. |
500
+ | `placement` | `"balanced" \| "roundRobin"` | `"balanced"` | `balanced` aims for even column heights. |
501
+ | `estimatedItemHeight` | `number` | `—` | Hint used before measurements settle. |
502
+ | `as` | `React.ElementType` | `"div"` | Root HTML element or custom component. |
503
+ | `rootRef` | `React.Ref<HTMLDivElement>` | `—` | Ref to the masonry root. |
504
+ | `classNames.root` | `string` | `—` | Root class name. |
505
+ | `classNames.column` | `string` | `—` | Column class name. |
506
+ | `classNames.item` | `string` | `—` | Item class name. |
507
+ | `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
508
+ | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
509
+ | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
510
+ | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
511
+ | `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
512
+ | `loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
513
+ | `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
514
+ | `loading.skeleton` | `MasonrySkeletonSpec` | `—` | Built-in masonry skeleton spec. |
515
+ | `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
516
+ | `intro.staggerMs` | `number` | `40` | Reveal stagger. |
517
+ | `intro.transform` | `string` | `"translateY(10px) scale(0.99)"` | Starting transform. |
518
+ | `intro.durationMs` | `number` | `300` | Intro duration. |
519
+ | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro easing. |
520
+ | `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
521
+
522
+ ## Entries
523
+
524
+ `Entries` is the structured-data surface. You pass entry objects, render each media item however you want, and provide a `renderMediaContainer` function that decides whether an entry’s media should be laid out as a slider, grid, or masonry block.
28
525
 
29
526
  ```tsx
30
527
  import * as React from "react";
31
- import { Gallery } from "react-motion-gallery";
528
+ import {
529
+ Entries,
530
+ GalleryCore,
531
+ Slider,
532
+ flattenEntries,
533
+ type SliderHandle,
534
+ } from "react-motion-gallery";
535
+
536
+ const entries = [
537
+ {
538
+ id: "a",
539
+ title: "Entry A",
540
+ media: [
541
+ { kind: "image", src: "https://picsum.photos/seed/a1/1400/900", alt: "A1" },
542
+ { kind: "image", src: "https://picsum.photos/seed/a2/1400/900", alt: "A2" },
543
+ ],
544
+ },
545
+ {
546
+ id: "b",
547
+ title: "Entry B",
548
+ media: [{ kind: "image", src: "https://picsum.photos/seed/b1/1400/900", alt: "B1" }],
549
+ },
550
+ ] as const;
551
+
552
+ export function EntryGallery() {
553
+ const flat = React.useMemo(() => flattenEntries(entries as any), []);
554
+ const fullscreenItems = flat.flattenedMedia;
32
555
 
33
- export function WrapperRuleExample() {
34
556
  return (
35
- <Gallery>
36
- <div>
37
- <img src="https://picsum.photos/seed/1/900/500" alt="" />
38
- </div>
557
+ <GalleryCore layout="entries" fullscreenItems={fullscreenItems}>
558
+ <Entries
559
+ entries={{
560
+ items: entries as any,
561
+ mediaLayout: "slider",
562
+ render: {
563
+ card: ({ entry, media }) => (
564
+ <article style={{ display: "grid", gap: 12 }}>
565
+ <h3>{entry.title}</h3>
566
+ {media}
567
+ </article>
568
+ ),
569
+ media: ({ media, mediaIndex }) =>
570
+ media.kind === "image" ? (
571
+ <img key={mediaIndex} src={media.src} alt={media.alt ?? ""} style={{ width: "100%" }} />
572
+ ) : null,
573
+ },
574
+ }}
575
+ fullscreen={{ enabled: true }}
576
+ renderMediaContainer={({ entryIndex, mediaNodes, entrySliderRefs }) => (
577
+ <Slider
578
+ ref={(node: SliderHandle | null) => {
579
+ if (entrySliderRefs?.current) entrySliderRefs.current[entryIndex] = node;
580
+ }}
581
+ >
582
+ {mediaNodes}
583
+ </Slider>
584
+ )}
585
+ />
586
+ </GalleryCore>
587
+ );
588
+ }
589
+ ```
590
+
591
+ ### `Entries` component props
592
+
593
+ | Option | Type | Default | Notes |
594
+ | --- | --- | --- | --- |
595
+ | `enabled` | `boolean` | `true` | Master switch for rendering entry content and transitions. |
596
+ | `entries` | `EntriesOptions` | `—` | Structured entry configuration. |
597
+ | `fullscreen.enabled` | `boolean` | `true` | Enables fullscreen opening for entry media. |
598
+ | `fullscreen.items` | `MediaItem[] \| string[]` | flattened entry media | Optional fullscreen media override. |
599
+ | `renderMediaContainer` | `({ entryIndex, mediaNodes, entrySliderRefs }) => ReactNode` | `—` | Chooses how each entry’s media nodes are laid out. |
600
+ | `nodeFromMedia` | `(media: MediaItem) => ReactNode` | built-in image/video renderer | Fallback renderer when `entries.render.media` is omitted. |
601
+ | `entryFlatIndexRef` | `React.RefObject<number[][] \| null>` | internal ref | Receives per-entry local-to-global media index maps. |
602
+ | `entryMapRef` | `React.RefObject<MediaEntryLink[] \| null>` | internal ref | Receives the flattened media-to-entry map. |
603
+ | `fsOwnersRef` | `React.RefObject<SlideOwner[]>` | internal ref | Receives the fullscreen slide owner list. |
604
+ | `entrySliderRefs` | `React.RefObject<(SliderHandle \| null)[]>` | internal ref | Lets `renderMediaContainer` wire fullscreen back to per-entry sliders. |
605
+
606
+ ### `EntriesOptions`
607
+
608
+ | Option | Type | Default | Notes |
609
+ | --- | --- | --- | --- |
610
+ | `items` | `EntryItem[]` | `—` | Entry records. Each item can hold arbitrary fields plus `media`. |
611
+ | `mediaLayout` | `"slider" \| "grid" \| "masonry"` | `"slider"` | Declares the intended media layout. |
612
+ | `render.card` | `({ entry, entryIndex, media }) => ReactNode` | `—` | Wraps the media container in custom card UI. |
613
+ | `render.media` | `({ entry, entryIndex, media, mediaIndex }) => ReactNode` | `—` | Custom media renderer per media item. |
614
+ | `render.overlay` | `({ entry, entryIndex, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
615
+ | `render.skeleton` | `({ entry, entryIndex }) => ReactNode` | `—` | Declared in the type, but the current runtime uses `loading.skeleton` instead. |
616
+ | `overlay` | `ElementStyle` | `—` | Styles the fullscreen overlay container that wraps `render.overlay`. |
617
+ | `loading.enabled` | `boolean` | `—` | Enables entry loading and decode gating. |
618
+ | `loading.force` | `boolean` | `—` | Forces entry skeletons to remain visible. |
619
+ | `loading.skeleton` | `EntrySkeletonSpec \| ((args) => EntrySkeletonSpec \| null \| undefined)` | `—` | Built-in skeleton spec or resolver. |
620
+ | `loading.minHeight` | `number \| string` | `"260px"` | Minimum reserved height while loading. |
621
+ | `loading.nearMargin` | `string` | `"700px 0px"` | Preload margin used before entries enter view. |
622
+ | `loading.viewMargin` | `string` | `"0px 0px"` | Margin used for the actual in-view gate. |
623
+ | `loading.threshold` | `number` | `0.01` | Intersection threshold for view detection. |
624
+ | `loading.waitForDecode` | `boolean` | `true` | Waits for image decode before revealing an entry. |
625
+ | `loading.decodeTimeoutMs` | `number` | `8000` | Decode timeout fallback. |
626
+ | `loading.skeletonWrap` | `ElementStyle` | `—` | Styles the skeleton wrapper. |
627
+ | `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
628
+ | `intro.staggerMs` | `number` | `200` | Delay between entry reveals. |
629
+ | `intro.durationMs` | `number` | `700` | Entry intro duration. |
630
+ | `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Entry intro easing. |
631
+ | `intro.staggerLimit` | `number` | `6` | Maximum number of entries that receive staggered delays. |
632
+ | `entryList` | `ElementStyle` | `—` | Styles the entry list container. |
633
+ | `entryRow` | `ElementStyle` | `—` | Styles each entry row container. |
634
+
635
+ ### Entry-related callback and helper types
636
+
637
+ #### `EntryItem`
638
+
639
+ | Field | Type | Notes |
640
+ | --- | --- | --- |
641
+ | `media` | `MediaItem[] \| undefined` | Optional list of media items for the entry. |
642
+ | `[key: string]` | `any` | Additional entry fields are allowed. |
643
+
644
+ #### `EntryMediaRenderArgs`
645
+
646
+ | Field | Type | Notes |
647
+ | --- | --- | --- |
648
+ | `entry` | `EntryItem` | Current entry object. |
649
+ | `entryIndex` | `number` | Entry index. |
650
+ | `media` | `MediaItem` | Current media item. |
651
+ | `mediaIndex` | `number` | Media index within the entry. |
652
+
653
+ #### `EntryCardRenderArgs`
654
+
655
+ | Field | Type | Notes |
656
+ | --- | --- | --- |
657
+ | `entry` | `EntryItem` | Current entry object. |
658
+ | `entryIndex` | `number` | Entry index. |
659
+ | `media` | `ReactNode` | The rendered media container returned by `renderMediaContainer`. |
660
+
661
+ #### `EntryOverlayRenderArgs`
39
662
 
40
- <div>
41
- <img src="https://picsum.photos/seed/2/900/500" alt="" />
42
- </div>
43
- </Gallery>
663
+ | Field | Type | Notes |
664
+ | --- | --- | --- |
665
+ | `entry` | `EntryItem` | Entry owning the active fullscreen slide. |
666
+ | `entryIndex` | `number` | Entry index. |
667
+ | `mediaIndex` | `number \| null` | Media index inside the entry when available. |
668
+ | `link` | `MediaEntryLink \| null` | Flattened link back to the entry/media pair. |
669
+ | `opacity` | `number` | Overlay opacity supplied by the runtime. |
670
+ | `fsIndex` | `number` | Current fullscreen slide index. |
671
+ | `style` | `React.CSSProperties` | Overlay positioning and animation style. |
672
+ | `containerProps` | `React.HTMLAttributes<HTMLDivElement>` | Props to spread onto the overlay root. |
673
+
674
+ #### `EntrySkeletonRenderArgs`
675
+
676
+ | Field | Type | Notes |
677
+ | --- | --- | --- |
678
+ | `entry` | `EntryItem` | Current entry object. |
679
+ | `entryIndex` | `number` | Entry index. |
680
+
681
+ #### `MediaEntryLink`
682
+
683
+ | Field | Type | Notes |
684
+ | --- | --- | --- |
685
+ | `entryIndex` | `number` | Entry index. |
686
+ | `mediaIndex` | `number` | Media index inside the entry. |
687
+
688
+ #### `SlideOwner`
689
+
690
+ | Field | Type | Notes |
691
+ | --- | --- | --- |
692
+ | `entryIndex` | `number` | Entry that owns a fullscreen slide. |
693
+
694
+ ### `flattenEntries`
695
+
696
+ | Field | Type | Notes |
697
+ | --- | --- | --- |
698
+ | `flattenedMedia` | `MediaItem[]` | One flat media array, in fullscreen order. |
699
+ | `flattenedMap` | `MediaEntryLink[]` | Global slide index back to `entryIndex` and `mediaIndex`. |
700
+ | `entryFlatIndex` | `number[][] \| null` | Per-entry lookup from local media index to global slide index. |
701
+ | `owners` | `SlideOwner[]` | Owner metadata for each flattened slide. |
702
+
703
+ ## Fullscreen
704
+
705
+ Fullscreen is compositional. `GalleryCore` owns the normalized fullscreen item list, your layout opens slides through that core, and `useFullscreenController` renders the portal UI.
706
+
707
+ ```tsx
708
+ import * as React from "react";
709
+ import { GalleryCore, Slider, useFullscreenController } from "react-motion-gallery";
710
+
711
+ const slides = [
712
+ "https://picsum.photos/id/1015/1600/900",
713
+ "https://picsum.photos/id/1018/1600/900",
714
+ "https://picsum.photos/id/1024/1600/900",
715
+ ];
716
+
717
+ function FullscreenAddon({ count }: { count: number }) {
718
+ const { fullscreenNode } = useFullscreenController({
719
+ fullscreen: { enabled: true },
720
+ sliderObject: { align: "start", direction: { dir: "ltr" } },
721
+ cellsStateLength: count,
722
+ });
723
+
724
+ return <>{fullscreenNode}</>;
725
+ }
726
+
727
+ export function SliderWithFullscreen() {
728
+ return (
729
+ <GalleryCore layout="slider" fullscreenItems={slides}>
730
+ <Slider>
731
+ {slides.map((src, index) => (
732
+ <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
733
+ ))}
734
+ </Slider>
735
+ <FullscreenAddon count={slides.length} />
736
+ </GalleryCore>
44
737
  );
45
738
  }
46
- ```
739
+ ```
740
+
741
+ Add fullscreen thumbnails by rendering `FullscreenThumbnailSlider` with the bridge returned from `useFullscreenController`.
742
+
743
+ ```tsx
744
+ import { FullscreenThumbnailSlider, useFullscreenController } from "react-motion-gallery";
745
+
746
+ function FullscreenWithThumbs({ thumbs }: { thumbs: string[] }) {
747
+ const { fullscreenNode, fullscreenThumbnailBridge } = useFullscreenController({
748
+ fullscreen: { enabled: true },
749
+ sliderObject: { align: "start", direction: { dir: "ltr" } },
750
+ cellsStateLength: thumbs.length,
751
+ });
752
+
753
+ return (
754
+ <>
755
+ {fullscreenNode}
756
+ <FullscreenThumbnailSlider
757
+ bridge={fullscreenThumbnailBridge}
758
+ items={thumbs.map((thumbSrc, index) => ({ thumbSrc, alt: `Thumb ${index + 1}` }))}
759
+ position="bottom"
760
+ thumbnailHeight={60}
761
+ gap={10}
762
+ />
763
+ </>
764
+ );
765
+ }
766
+ ```
767
+
768
+ ### `GalleryCore` props
769
+
770
+ | Option | Type | Default | Notes |
771
+ | --- | --- | --- | --- |
772
+ | `children` | `React.ReactNode` | `—` | The gallery tree using the shared core. |
773
+ | `layout` | `"slider" \| "grid" \| "masonry" \| "entries"` | `—` | Declares the owning base layout. |
774
+ | `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Breakpoint map shared with descendants. |
775
+ | `fullscreenItems` | `MediaItem[] \| string[]` | `[]` | Normalized fullscreen media list. |
776
+ | `nodes` | `ReactNode \| ReactNode[]` | `—` | Advanced initial node list for imperative gallery state. |
777
+
778
+ ### `useFullscreenController` args
779
+
780
+ | Option | Type | Default | Notes |
781
+ | --- | --- | --- | --- |
782
+ | `fullscreen` | `FullscreenOptions` | `—` | Fullscreen behavior and rendering options. |
783
+ | `sliderObject` | `any` | `—` | The controller reads `align` and `direction.dir` to mirror the base slider. |
784
+ | `cellsStateLength` | `number` | `—` | Number of base cells used by the fullscreen runtime. |
785
+ | `slider` | `SliderOptions` | `—` | Present in the hook args type, but currently unused by the implementation. |
786
+
787
+ ### Recommended `useFullscreenController` return values
788
+
789
+ | Field | Type | Notes |
790
+ | --- | --- | --- |
791
+ | `fullscreenNode` | `ReactNode` | The fullscreen portal UI. Render this once inside the `GalleryCore` tree. |
792
+ | `fullscreenThumbnailBridge` | `FullscreenThumbnailBridge` | Bridge consumed by `FullscreenThumbnailSlider`. |
793
+ | `openFullscreenAt` | `(source, index, originEl?, requestedMethod?) => void` | Programmatic fullscreen open helper returned by the controller. |
794
+ | `showFullscreenModal` | `boolean` | `true` while the fullscreen modal is mounted and open. |
795
+ | `showFullscreenSlider` | `boolean` | `true` once the slider portion is visible. |
796
+ | `fsFadeOpening` | `boolean` | `true` while a fade-based open animation is running. |
797
+ | `closingModal` | `boolean` | `true` while the close animation is running. |
798
+
799
+ The hook returns additional refs and setters for the internal fullscreen runtime. Those values are implementation plumbing and are not the recommended consumer-facing surface for app code.
800
+
801
+ ### `FullscreenOptions`
802
+
803
+ | Option | Type | Default | Notes |
804
+ | --- | --- | --- | --- |
805
+ | `enabled` | `boolean` | `false` | Master switch for fullscreen UI. |
806
+ | `items` | `MediaItem[] \| string[]` | `—` | Declared in the type, but current fullscreen media resolution comes from `GalleryCore.fullscreenItems`. |
807
+ | `renderImage` | `({ item, index, isZoomed, className, baseStyle }) => ReactNode` | `—` | Custom fullscreen image renderer. Must render a real descendant `<img>`. |
808
+ | `video.source` | `(item: MediaItem, index: number) => Plyr.SourceInfo` | `—` | Builds fullscreen Plyr sources for video items. |
809
+ | `video.options` | `Plyr.Options \| ((item: MediaItem, index: number) => Plyr.Options)` | `—` | Builds fullscreen Plyr options. |
810
+ | `video.style` | `React.CSSProperties` | `—` | Fullscreen player inline style. |
811
+ | `video.className` | `string` | `—` | Fullscreen player class. |
812
+ | `controls.close.enabled` | `boolean` | `true` | Toggles the close button. |
813
+ | `controls.close.style` | `React.CSSProperties` | `{}` | Close button inline style. |
814
+ | `controls.close.className` | `string` | `""` | Close button class. |
815
+ | `controls.close.render` | `() => ReactNode` | `—` | Custom close button renderer. |
816
+ | `controls.arrows.enabled` | `boolean` | `true` | Toggles fullscreen arrows. |
817
+ | `controls.arrows.arrow` | `ElementStyle` | `{}` | Shared arrow style. |
818
+ | `controls.arrows.prev` | `ElementStyle` | `{}` | Previous-arrow override. |
819
+ | `controls.arrows.next` | `ElementStyle` | `{}` | Next-arrow override. |
820
+ | `controls.arrows.render` | `({ dir }) => ReactNode` | `—` | Custom renderer for both arrows. |
821
+ | `controls.arrows.renderPrev` | `() => ReactNode` | `—` | Custom previous arrow. |
822
+ | `controls.arrows.renderNext` | `() => ReactNode` | `—` | Custom next arrow. |
823
+ | `controls.counter.enabled` | `boolean` | `true` | Toggles the index counter. |
824
+ | `controls.counter.style` | `React.CSSProperties` | `{}` | Counter inline style. |
825
+ | `controls.counter.className` | `string` | `""` | Counter class. |
826
+ | `controls.counter.render` | `({ index, count }) => ReactNode` | `—` | Custom counter renderer. |
827
+ | `caption.className` | `string` | `—` | Caption root class. |
828
+ | `caption.style` | `React.CSSProperties` | `—` | Caption root style. |
829
+ | `caption.placement` | `"top" \| "right" \| "bottom" \| "left"` | `—` | Preferred caption placement. |
830
+ | `caption.width` | `number` | `—` | Caption area width. |
831
+ | `caption.height` | `number` | `—` | Caption area height. |
832
+ | `caption.breakpoint` | `number` | `—` | Viewport cutoff for switching placement logic. |
833
+ | `caption.render` | `({ item, index, isZoomed }) => ReactNode` | `—` | Custom caption renderer. |
834
+ | `slider.duration` | `number` | `25` | Fullscreen slider motion duration. |
835
+ | `slider.friction` | `number` | `0.68` | Fullscreen slider friction. |
836
+ | `zoom.clickZoomLevel` | `number` | `2.5` | Zoom level used for click-to-zoom. |
837
+ | `zoom.maxZoomLevel` | `number` | `3` | Maximum allowed zoom level. |
838
+ | `zoom.panDuration` | `number` | `43` | Pan settling duration. |
839
+ | `zoom.panFriction` | `number` | `0.68` | Pan friction. |
840
+ | `effects.introDuration` | `number` | `300` | Open animation duration. |
841
+ | `effects.introEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Open animation easing. |
842
+ | `effects.introFade` | `boolean` | `false` | Forces fade intro behavior. |
843
+ | `effects.slideFade` | `boolean` | `false` | Fades between fullscreen slides. |
844
+ | `effects.slideFadeDuration` | `number` | `120` | Slide-fade duration. |
845
+ | `effects.slideFadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Slide-fade easing. |
846
+ | `lazyLoad.images.enabled` | `boolean` | `—` | Enables fullscreen image lazy loading. |
847
+ | `lazyLoad.images.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen images. |
848
+ | `lazyLoad.images.spinnerClassName` | `string` | `—` | Spinner class for image slides. |
849
+ | `lazyLoad.images.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for image slides. |
850
+ | `lazyLoad.videos.enabled` | `boolean` | `—` | Enables fullscreen video lazy loading. |
851
+ | `lazyLoad.videos.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen videos. |
852
+ | `lazyLoad.videos.spinnerClassName` | `string` | `—` | Spinner class for video slides. |
853
+ | `lazyLoad.videos.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for video slides. |
854
+
855
+ ### Fullscreen callback and helper types
856
+
857
+ #### `FsCounterArgs`
858
+
859
+ | Field | Type | Notes |
860
+ | --- | --- | --- |
861
+ | `index` | `number` | Current fullscreen index. |
862
+ | `count` | `number` | Total slide count. |
863
+
864
+ #### `FsCaptionRenderArgs`
865
+
866
+ | Field | Type | Notes |
867
+ | --- | --- | --- |
868
+ | `item` | `MediaItem` | Active fullscreen item. |
869
+ | `index` | `number` | Active fullscreen index. |
870
+ | `isZoomed` | `boolean` | `true` when the active slide is zoomed. |
871
+
872
+ #### `FsCaptionPlacement`
873
+
874
+ | Value | Notes |
875
+ | --- | --- |
876
+ | `"top"` | Places the caption above the media. |
877
+ | `"right"` | Places the caption to the right of the media. |
878
+ | `"bottom"` | Places the caption below the media. |
879
+ | `"left"` | Places the caption to the left of the media. |
880
+
881
+ #### `FsIntroRequest`
882
+
883
+ | Field | Type | Notes |
884
+ | --- | --- | --- |
885
+ | `originalImage` | `HTMLImageElement \| null` | Origin image used for scale transitions. |
886
+ | `index` | `number` | Target fullscreen index. |
887
+ | `method` | `"fade" \| "scale"` | Requested intro method. |
888
+ | `closestSelector` | `string \| undefined` | Selector used to resolve the source slide element. |
889
+
890
+ #### `FullscreenLazyLoadArgs`
891
+
892
+ | Field | Type | Notes |
893
+ | --- | --- | --- |
894
+ | `kind` | `"image" \| "video"` | Media kind currently loading. |
895
+ | `isClone` | `boolean \| undefined` | `true` for cloned looped slides when relevant. |
896
+
897
+ #### `FullscreenThumbnailSlider` props
898
+
899
+ `FullscreenThumbnailSliderProps` is exported from both the package root and `react-motion-gallery/fullscreenThumbnails`. The table below summarizes the prop surface.
900
+
901
+ | Option | Type | Default | Notes |
902
+ | --- | --- | --- | --- |
903
+ | `bridge` | `FullscreenThumbnailBridge` | `—` | Bridge returned from `useFullscreenController`. |
904
+ | `items` | `{ thumbSrc: string; alt?: string }[]` | `—` | Thumbnail list. |
905
+ | `position` | `"top" \| "right" \| "bottom" \| "left"` | `—` | Thumbnail rail position. |
906
+ | `containerClassName` | `string` | `—` | Thumbnail container class. |
907
+ | `containerStyle` | `React.CSSProperties` | `—` | Thumbnail container style. |
908
+ | `thumbnailWidth` | `number \| string` | `—` | Individual thumbnail width. |
909
+ | `thumbnailHeight` | `number \| string` | `—` | Individual thumbnail height. |
910
+ | `thumbnailsCenter` | `boolean` | `—` | Centers the thumbnail strip within its container. |
911
+ | `thumbnailsContainerWidth` | `number \| string` | `—` | Explicit strip width. |
912
+ | `thumbnailsContainerHeight` | `number \| string` | `—` | Explicit strip height. |
913
+ | `fadeDurationMs` | `number` | `300` | Mount and unmount fade duration. |
914
+ | `fadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Fade easing. |
915
+ | `thumbnailItemClassName` | `string` | `—` | Thumbnail item class. |
916
+ | `thumbnailItemStyle` | `React.CSSProperties` | `—` | Thumbnail item style. |
917
+ | `gap` | `number` | `—` | Gap between thumbnails. |
918
+ | `freeScroll` | `boolean` | `—` | Enables free thumbnail dragging. |
919
+ | `groupCells` | `boolean` | `—` | Groups thumbnail cells into snaps. |
920
+ | `loop` | `boolean` | `—` | Loops the thumbnail slider. |
921
+ | `axis` | `"x" \| "y"` | `—` | Declared in the prop type, but the current implementation does not wire it through. |
922
+ | `skipSnaps` | `boolean` | `—` | Allows momentum to skip snaps. |
923
+ | `centerActiveThumb` | `boolean` | `—` | Keeps the active thumbnail centered. |
924
+ | `selectDuration` | `number` | `—` | Selection motion duration. |
925
+ | `freeScrollDuration` | `number` | `—` | Free-scroll settling duration. |
926
+ | `sliderFriction` | `number` | `—` | Thumbnail slider friction. |
927
+ | `breakpointMap` | `Record<string, number>` | `{ xs: 0, sm: 640, md: 768, lg: 1024, xl: 1280 }` | Breakpoints used by the thumbnail strip. |
928
+ | `rippleEnabled` | `boolean` | `—` | Enables thumbnail arrow ripples. |
929
+ | `rippleClassName` | `string` | `—` | Ripple class name. |
930
+ | `showArrows` | `boolean` | `false` | Toggles thumbnail arrows. |
931
+ | `arrowStyles` | `React.CSSProperties` | `—` | Shared arrow styles. |
932
+ | `arrowClassName` | `string` | `—` | Shared arrow class. |
933
+ | `prevArrowStyles` | `React.CSSProperties` | `—` | Previous-arrow styles. |
934
+ | `prevArrowClassName` | `string` | `—` | Previous-arrow class. |
935
+ | `nextArrowStyles` | `React.CSSProperties` | `—` | Next-arrow styles. |
936
+ | `nextArrowClassName` | `string` | `—` | Next-arrow class. |
937
+ | `renderArrows` | `(args) => ReactNode` | `—` | Custom renderer for both arrows. |
938
+ | `renderPrevArrow` | `(args) => ReactNode` | `—` | Custom previous arrow. |
939
+ | `renderNextArrow` | `(args) => ReactNode` | `—` | Custom next arrow. |
940
+
941
+ #### `FullscreenThumbnailBridge`
942
+
943
+ | Field | Type | Notes |
944
+ | --- | --- | --- |
945
+ | `mountEl` | `HTMLDivElement \| null` | Portal mount node for the thumbnail strip. |
946
+ | `fsSub` | `FullscreenSliderSub` | Fullscreen slider index channel used internally. |
947
+ | `visible` | `boolean` | `true` when the strip should be visible. |
948
+ | `invisible` | `boolean` | `true` during hidden transitional states. |
949
+ | `direction` | `"ltr" \| "rtl"` | Fullscreen direction. |
950
+ | `registerLayout` | `(layout: FullscreenThumbnailSlotLayout) => void` | Registers the slot layout metadata. |
951
+ | `clearLayout` | `() => void` | Clears the current slot layout. |
952
+
953
+ #### `FullscreenThumbnailSlotLayout`
954
+
955
+ | Field | Type | Notes |
956
+ | --- | --- | --- |
957
+ | `position` | `"top" \| "right" \| "bottom" \| "left"` | Thumbnail rail position. |
958
+ | `className` | `string \| undefined` | Slot container class. |
959
+ | `style` | `React.CSSProperties \| undefined` | Slot container style. |
960
+ | `fadeDurationMs` | `number \| undefined` | Slot fade duration. |
961
+ | `fadeEasing` | `string \| undefined` | Slot fade easing. |
962
+
963
+ ### `GalleryApi`
964
+
965
+ `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.
966
+
967
+ | Method | Signature | Notes |
968
+ | --- | --- | --- |
969
+ | `rootNode` | `() => HTMLElement \| null` | Gallery root node. |
970
+ | `containerNode` | `() => HTMLElement \| null` | Moving or content container node. |
971
+ | `getViewportNode` | `() => HTMLDivElement \| null` | Viewport node. |
972
+ | `slideNodes` | `() => HTMLElement[]` | Current slide elements. |
973
+ | `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Subscribes to readiness. |
974
+ | `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of readiness. |
975
+ | `isReady` | `() => boolean` | `true` after readiness resolves. |
976
+ | `scrollTo` | `(index: number, jump?: boolean) => void` | Navigates to a slide. |
977
+ | `scrollNext` | `(jump?: boolean) => void` | Advances to the next slide. |
978
+ | `scrollPrev` | `(jump?: boolean) => void` | Moves to the previous slide. |
979
+ | `canScrollNext` | `() => boolean` | Whether next navigation is available. |
980
+ | `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
981
+ | `getIndex` | `() => number` | Current active index. |
982
+ | `selectCell` | `(index: number, jump?: boolean) => void` | Selects a cell by canonical index. |
983
+ | `scrollProgress` | `() => number` | Scroll progress from `0` to `1`. |
984
+ | `cellsInView` | `() => number[]` | Canonical cells currently visible. |
985
+ | `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
986
+ | `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
987
+ | `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
988
+ | `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
989
+ | `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
990
+ | `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
991
+ | `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
992
+ | `openFullscreenAt` | `({ index, method?, event? }) => void` | Programmatically opens fullscreen at an index. |
993
+
994
+ ## Video
995
+
996
+ `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.
997
+
998
+ ```tsx
999
+ import { Video } from "react-motion-gallery";
1000
+
1001
+ export function BasicVideo() {
1002
+ return (
1003
+ <div style={{ width: "100%", aspectRatio: "16 / 9", overflow: "hidden" }}>
1004
+ <Video
1005
+ src="https://cdn.plyr.io/static/blank.mp4"
1006
+ poster="https://picsum.photos/seed/video-poster/1600/900"
1007
+ options={{ controls: ["play", "progress", "mute", "fullscreen"] } as any}
1008
+ lazyLoad={{ enabled: true, spinner: true }}
1009
+ />
1010
+ </div>
1011
+ );
1012
+ }
1013
+ ```
1014
+
1015
+ ### `Video` props
1016
+
1017
+ `VideoProps` is exported from both the package root and `react-motion-gallery/video`. The table below summarizes the prop surface.
1018
+
1019
+ | Option | Type | Default | Notes |
1020
+ | --- | --- | --- | --- |
1021
+ | `src` | `string` | `—` | Source URL used to build the default Plyr source. |
1022
+ | `poster` | `string` | `—` | Poster image. |
1023
+ | `alt` | `string` | `—` | Optional metadata label; the `Video` component itself does not render a visible alt attribute. |
1024
+ | `source` | `Plyr.SourceInfo` | auto-built MP4 source | Direct Plyr source object. Overrides `sourceBuilder`. |
1025
+ | `sourceBuilder` | `({ src: string }) => Plyr.SourceInfo` | `—` | Builds the Plyr source from `src`. |
1026
+ | `options` | `Plyr.Options \| (({ src, index }) => Plyr.Options)` | `—` | Direct or computed Plyr options. When omitted, the component still applies `autoplay: false` and `preload: "none"` defaults internally. |
1027
+ | `className` | `string` | `—` | Player wrapper class. |
1028
+ | `style` | `React.CSSProperties` | `—` | Player wrapper style. |
1029
+ | `onApi` | `(api: APITypes \| null) => void` | `—` | Called whenever the Plyr API ref changes. |
1030
+ | `registerApiByIndex` | `(index: number, api: APITypes \| null) => void` | `—` | Registers the API by canonical gallery index. |
1031
+ | `lazyLoad.enabled` | `boolean` | `true` | `false` mounts immediately after reveal. |
1032
+ | `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `true` | `false` disables the spinner; `true` uses the built-in spinner. |
1033
+ | `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
1034
+ | `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
1035
+
1036
+ ### Supporting video types
1037
+
1038
+ These helper type names are available from both the package root and `react-motion-gallery/video`.
1039
+
1040
+ | Type | Shape | Notes |
1041
+ | --- | --- | --- |
1042
+ | `RmgPlyrSourceBuilder` | `({ src: string }) => Plyr.SourceInfo` | Used by `sourceBuilder`. |
1043
+ | `RmgPlyrOptionsResolver` | `Plyr.Options \| (({ src, index }) => Plyr.Options)` | Used by `options`. |
1044
+ | `RmgVideoLazyLoadOptions` | `{ enabled?, spinner?, spinnerClassName?, spinnerStyle? }` | Used by `lazyLoad`. |
1045
+
1046
+ If you do not use `Video`, you do not need `plyr` or `plyr-react`. Install those optional peer dependencies only for video playback.