react-smart-image-viewer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +480 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +649 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# react-smart-image-viewer
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>A high-performance, TypeScript-first React image viewer with zoom, pan, keyboard, and mobile gesture support.</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="#features">Features</a> •
|
|
9
|
+
<a href="#installation">Installation</a> •
|
|
10
|
+
<a href="#quick-start">Quick Start</a> •
|
|
11
|
+
<a href="#api">API</a> •
|
|
12
|
+
<a href="#examples">Examples</a> •
|
|
13
|
+
<a href="#accessibility">Accessibility</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why react-smart-image-viewer?
|
|
19
|
+
|
|
20
|
+
Modern web applications need image viewers that are:
|
|
21
|
+
|
|
22
|
+
- **Fast** - No unnecessary re-renders, optimized with `requestAnimationFrame`
|
|
23
|
+
- **Accessible** - Full keyboard support, ARIA labels, focus management
|
|
24
|
+
- **Mobile-friendly** - Touch gestures, pinch-to-zoom, swipe navigation
|
|
25
|
+
- **Flexible** - Controlled/uncontrolled modes, headless-friendly API
|
|
26
|
+
- **Type-safe** - Built with TypeScript from the ground up
|
|
27
|
+
- **Next.js ready** - SSR-safe, no hydration mismatches
|
|
28
|
+
|
|
29
|
+
This package solves these problems with a lightweight (~15KB gzipped), tree-shakable solution.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
✨ **Modal/Lightbox**
|
|
34
|
+
- Open images in a fullscreen overlay
|
|
35
|
+
- Close via ESC key, overlay click, or close button
|
|
36
|
+
- Prevents body scroll when open
|
|
37
|
+
- Smooth animations
|
|
38
|
+
|
|
39
|
+
🔍 **Zoom & Pan**
|
|
40
|
+
- Mouse wheel zoom (zooms toward cursor)
|
|
41
|
+
- Button controls for zoom in/out/reset
|
|
42
|
+
- Double-click to zoom in, double-click again to reset
|
|
43
|
+
- Drag to pan when zoomed
|
|
44
|
+
- Pinch-to-zoom on mobile devices
|
|
45
|
+
|
|
46
|
+
🖼️ **Gallery Support**
|
|
47
|
+
- Single image or array of images
|
|
48
|
+
- Next/Previous navigation with arrows
|
|
49
|
+
- Keyboard navigation (← →)
|
|
50
|
+
- Optional loop mode
|
|
51
|
+
- Image counter display
|
|
52
|
+
|
|
53
|
+
⌨️ **Keyboard Shortcuts**
|
|
54
|
+
- `ESC` - Close viewer
|
|
55
|
+
- `←` / `→` - Navigate images
|
|
56
|
+
- `+` / `=` - Zoom in
|
|
57
|
+
- `-` - Zoom out
|
|
58
|
+
- `0` - Reset zoom
|
|
59
|
+
|
|
60
|
+
♿ **Accessibility**
|
|
61
|
+
- `role="dialog"` with `aria-modal`
|
|
62
|
+
- Focus trap inside modal
|
|
63
|
+
- ARIA labels on all interactive elements
|
|
64
|
+
- Screen reader announcements for gallery position
|
|
65
|
+
- Respects `prefers-reduced-motion`
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install react-smart-image-viewer
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
yarn add react-smart-image-viewer
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm add react-smart-image-viewer
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { ImageViewer } from 'react-smart-image-viewer';
|
|
85
|
+
import 'react-smart-image-viewer/styles.css';
|
|
86
|
+
|
|
87
|
+
function App() {
|
|
88
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<button onClick={() => setIsOpen(true)}>Open Image</button>
|
|
93
|
+
|
|
94
|
+
<ImageViewer
|
|
95
|
+
images="https://example.com/image.jpg"
|
|
96
|
+
isOpen={isOpen}
|
|
97
|
+
onClose={() => setIsOpen(false)}
|
|
98
|
+
/>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
### `<ImageViewer />` Props
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Description |
|
|
109
|
+
|------|------|---------|-------------|
|
|
110
|
+
| `images` | `string \| ImageSource \| Array` | *required* | Image(s) to display |
|
|
111
|
+
| `isOpen` | `boolean` | - | Controlled open state |
|
|
112
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
|
|
113
|
+
| `initialIndex` | `number` | `0` | Starting image index for gallery |
|
|
114
|
+
| `onClose` | `() => void` | - | Called when viewer should close |
|
|
115
|
+
| `onIndexChange` | `(index: number) => void` | - | Called when image index changes |
|
|
116
|
+
| `zoomStep` | `number` | `0.5` | Zoom increment per step |
|
|
117
|
+
| `minZoom` | `number` | `0.5` | Minimum zoom level |
|
|
118
|
+
| `maxZoom` | `number` | `4` | Maximum zoom level |
|
|
119
|
+
| `showControls` | `boolean` | `true` | Show zoom controls |
|
|
120
|
+
| `showNavigation` | `boolean` | `true` | Show prev/next arrows |
|
|
121
|
+
| `showCounter` | `boolean` | `true` | Show image counter |
|
|
122
|
+
| `closeOnOverlayClick` | `boolean` | `true` | Close on overlay click |
|
|
123
|
+
| `closeOnEscape` | `boolean` | `true` | Close on ESC key |
|
|
124
|
+
| `enableKeyboardNavigation` | `boolean` | `true` | Enable ←/→ navigation |
|
|
125
|
+
| `loop` | `boolean` | `false` | Loop gallery navigation |
|
|
126
|
+
| `className` | `string` | - | Custom overlay class |
|
|
127
|
+
| `imageClassName` | `string` | - | Custom image class |
|
|
128
|
+
| `animationDuration` | `number` | `200` | Animation duration (ms) |
|
|
129
|
+
| `ariaLabel` | `string` | `'Image viewer'` | Accessible label |
|
|
130
|
+
| `renderControls` | `(props) => ReactNode` | - | Custom controls renderer |
|
|
131
|
+
| `renderNavigation` | `(props) => ReactNode` | - | Custom navigation renderer |
|
|
132
|
+
|
|
133
|
+
### `ImageSource` Type
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
interface ImageSource {
|
|
137
|
+
src: string; // Image URL (required)
|
|
138
|
+
alt?: string; // Alt text for accessibility
|
|
139
|
+
thumbnail?: string; // Thumbnail URL (for gallery previews)
|
|
140
|
+
title?: string; // Title to display
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `useImageViewer` Hook
|
|
145
|
+
|
|
146
|
+
A hook for programmatic control of the viewer.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useImageViewer, ImageViewer } from 'react-smart-image-viewer';
|
|
150
|
+
|
|
151
|
+
function Gallery() {
|
|
152
|
+
const images = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
|
|
153
|
+
|
|
154
|
+
const viewer = useImageViewer({
|
|
155
|
+
totalImages: images.length,
|
|
156
|
+
loop: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
<div className="thumbnails">
|
|
162
|
+
{images.map((img, i) => (
|
|
163
|
+
<img
|
|
164
|
+
key={i}
|
|
165
|
+
src={img}
|
|
166
|
+
onClick={() => viewer.open(i)}
|
|
167
|
+
alt={`Thumbnail ${i + 1}`}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<ImageViewer images={images} {...viewer.getViewerProps()} />
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Hook Options
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Description |
|
|
181
|
+
|--------|------|---------|-------------|
|
|
182
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state |
|
|
183
|
+
| `defaultIndex` | `number` | `0` | Initial image index |
|
|
184
|
+
| `totalImages` | `number` | `1` | Total number of images |
|
|
185
|
+
| `zoomStep` | `number` | `0.5` | Zoom increment |
|
|
186
|
+
| `minZoom` | `number` | `0.5` | Minimum zoom |
|
|
187
|
+
| `maxZoom` | `number` | `4` | Maximum zoom |
|
|
188
|
+
| `loop` | `boolean` | `false` | Loop navigation |
|
|
189
|
+
| `onOpenChange` | `(isOpen: boolean) => void` | - | Open state callback |
|
|
190
|
+
| `onIndexChange` | `(index: number) => void` | - | Index change callback |
|
|
191
|
+
|
|
192
|
+
#### Hook Return Value
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface UseImageViewerReturn {
|
|
196
|
+
isOpen: boolean;
|
|
197
|
+
open: (index?: number) => void;
|
|
198
|
+
close: () => void;
|
|
199
|
+
toggle: () => void;
|
|
200
|
+
currentIndex: number;
|
|
201
|
+
setCurrentIndex: (index: number) => void;
|
|
202
|
+
goToNext: () => void;
|
|
203
|
+
goToPrevious: () => void;
|
|
204
|
+
zoom: number;
|
|
205
|
+
zoomIn: () => void;
|
|
206
|
+
zoomOut: () => void;
|
|
207
|
+
resetZoom: () => void;
|
|
208
|
+
setZoom: (zoom: number) => void;
|
|
209
|
+
getViewerProps: () => Partial<ImageViewerProps>;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Examples
|
|
214
|
+
|
|
215
|
+
### Single Image
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<ImageViewer
|
|
219
|
+
images="https://example.com/photo.jpg"
|
|
220
|
+
isOpen={isOpen}
|
|
221
|
+
onClose={() => setIsOpen(false)}
|
|
222
|
+
/>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Gallery with Metadata
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const images = [
|
|
229
|
+
{
|
|
230
|
+
src: 'https://example.com/photo1.jpg',
|
|
231
|
+
alt: 'Mountain landscape',
|
|
232
|
+
title: 'Swiss Alps',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
src: 'https://example.com/photo2.jpg',
|
|
236
|
+
alt: 'Ocean sunset',
|
|
237
|
+
title: 'Pacific Coast',
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
<ImageViewer
|
|
242
|
+
images={images}
|
|
243
|
+
isOpen={isOpen}
|
|
244
|
+
onClose={() => setIsOpen(false)}
|
|
245
|
+
initialIndex={0}
|
|
246
|
+
loop
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Controlled Mode
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
function ControlledExample() {
|
|
254
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
255
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<ImageViewer
|
|
259
|
+
images={images}
|
|
260
|
+
isOpen={isOpen}
|
|
261
|
+
onClose={() => setIsOpen(false)}
|
|
262
|
+
initialIndex={currentIndex}
|
|
263
|
+
onIndexChange={setCurrentIndex}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Uncontrolled Mode
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
<ImageViewer
|
|
273
|
+
images={images}
|
|
274
|
+
defaultOpen={true}
|
|
275
|
+
onClose={() => console.log('Viewer closed')}
|
|
276
|
+
/>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Custom Controls
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
<ImageViewer
|
|
283
|
+
images={images}
|
|
284
|
+
isOpen={isOpen}
|
|
285
|
+
onClose={() => setIsOpen(false)}
|
|
286
|
+
renderControls={({ zoomIn, zoomOut, resetZoom, currentZoom, close }) => (
|
|
287
|
+
<div className="my-controls">
|
|
288
|
+
<button onClick={zoomOut}>−</button>
|
|
289
|
+
<span>{Math.round(currentZoom * 100)}%</span>
|
|
290
|
+
<button onClick={zoomIn}>+</button>
|
|
291
|
+
<button onClick={resetZoom}>Reset</button>
|
|
292
|
+
<button onClick={close}>×</button>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
/>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Custom Navigation
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
<ImageViewer
|
|
302
|
+
images={images}
|
|
303
|
+
isOpen={isOpen}
|
|
304
|
+
onClose={() => setIsOpen(false)}
|
|
305
|
+
renderNavigation={({ goToPrevious, goToNext, currentIndex, totalImages }) => (
|
|
306
|
+
<div className="my-nav">
|
|
307
|
+
<button onClick={goToPrevious}>Previous</button>
|
|
308
|
+
<span>{currentIndex + 1} of {totalImages}</span>
|
|
309
|
+
<button onClick={goToNext}>Next</button>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
/>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### With Next.js
|
|
316
|
+
|
|
317
|
+
The component is fully SSR-safe and works with Next.js out of the box:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
// pages/gallery.tsx or app/gallery/page.tsx
|
|
321
|
+
'use client'; // Required for app directory
|
|
322
|
+
|
|
323
|
+
import { useState } from 'react';
|
|
324
|
+
import { ImageViewer } from 'react-smart-image-viewer';
|
|
325
|
+
import 'react-smart-image-viewer/styles.css';
|
|
326
|
+
|
|
327
|
+
export default function GalleryPage() {
|
|
328
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
329
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
330
|
+
|
|
331
|
+
const images = ['/image1.jpg', '/image2.jpg', '/image3.jpg'];
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<main>
|
|
335
|
+
<div className="grid">
|
|
336
|
+
{images.map((src, i) => (
|
|
337
|
+
<img
|
|
338
|
+
key={src}
|
|
339
|
+
src={src}
|
|
340
|
+
onClick={() => {
|
|
341
|
+
setSelectedIndex(i);
|
|
342
|
+
setIsOpen(true);
|
|
343
|
+
}}
|
|
344
|
+
alt={`Gallery image ${i + 1}`}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<ImageViewer
|
|
350
|
+
images={images}
|
|
351
|
+
isOpen={isOpen}
|
|
352
|
+
onClose={() => setIsOpen(false)}
|
|
353
|
+
initialIndex={selectedIndex}
|
|
354
|
+
onIndexChange={setSelectedIndex}
|
|
355
|
+
/>
|
|
356
|
+
</main>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Accessibility
|
|
362
|
+
|
|
363
|
+
This component follows WAI-ARIA best practices for modal dialogs:
|
|
364
|
+
|
|
365
|
+
### Semantic Structure
|
|
366
|
+
- Uses `role="dialog"` with `aria-modal="true"`
|
|
367
|
+
- Close button has `aria-label="Close image viewer"`
|
|
368
|
+
- Navigation buttons have descriptive labels
|
|
369
|
+
- Images include alt text support
|
|
370
|
+
|
|
371
|
+
### Keyboard Support
|
|
372
|
+
- **Tab** - Cycles through focusable elements
|
|
373
|
+
- **Shift+Tab** - Cycles backwards
|
|
374
|
+
- **Escape** - Closes the viewer
|
|
375
|
+
- **Arrow keys** - Navigate gallery
|
|
376
|
+
- Focus is trapped within the modal when open
|
|
377
|
+
- Focus returns to trigger element on close
|
|
378
|
+
|
|
379
|
+
### Screen Readers
|
|
380
|
+
- Announces "Image X of Y" when navigating
|
|
381
|
+
- Alt text is announced for each image
|
|
382
|
+
- Live regions announce state changes
|
|
383
|
+
|
|
384
|
+
### Motion
|
|
385
|
+
- Respects `prefers-reduced-motion` media query
|
|
386
|
+
- Animations are disabled for users who prefer reduced motion
|
|
387
|
+
|
|
388
|
+
## Performance
|
|
389
|
+
|
|
390
|
+
### Optimizations
|
|
391
|
+
|
|
392
|
+
1. **requestAnimationFrame** - Zoom and pan operations are throttled using rAF
|
|
393
|
+
2. **Lazy loading** - Images load on-demand with loading indicators
|
|
394
|
+
3. **CSS transforms** - Hardware-accelerated transforms for smooth animations
|
|
395
|
+
4. **Minimal re-renders** - Memoized callbacks and optimized state updates
|
|
396
|
+
5. **Tree-shakable** - Import only what you need
|
|
397
|
+
|
|
398
|
+
### Bundle Size
|
|
399
|
+
|
|
400
|
+
- **Full bundle**: ~15KB gzipped
|
|
401
|
+
- **Core component only**: ~10KB gzipped
|
|
402
|
+
- **Zero runtime dependencies**
|
|
403
|
+
|
|
404
|
+
## Customization
|
|
405
|
+
|
|
406
|
+
### CSS Variables
|
|
407
|
+
|
|
408
|
+
Override these CSS custom properties to customize the appearance:
|
|
409
|
+
|
|
410
|
+
```css
|
|
411
|
+
:root {
|
|
412
|
+
--rsiv-overlay-bg: rgba(0, 0, 0, 0.92);
|
|
413
|
+
--rsiv-control-bg: rgba(255, 255, 255, 0.12);
|
|
414
|
+
--rsiv-control-bg-hover: rgba(255, 255, 255, 0.22);
|
|
415
|
+
--rsiv-control-color: #ffffff;
|
|
416
|
+
--rsiv-control-size: 44px;
|
|
417
|
+
--rsiv-control-radius: 8px;
|
|
418
|
+
--rsiv-counter-bg: rgba(0, 0, 0, 0.6);
|
|
419
|
+
--rsiv-counter-color: #ffffff;
|
|
420
|
+
--rsiv-animation-duration: 200ms;
|
|
421
|
+
--rsiv-animation-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
422
|
+
--rsiv-focus-ring: 0 0 0 2px rgba(66, 153, 225, 0.6);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Example: Dark Theme
|
|
427
|
+
|
|
428
|
+
```css
|
|
429
|
+
.my-viewer {
|
|
430
|
+
--rsiv-overlay-bg: rgba(10, 10, 10, 0.98);
|
|
431
|
+
--rsiv-control-bg: rgba(255, 255, 255, 0.08);
|
|
432
|
+
--rsiv-control-bg-hover: rgba(255, 255, 255, 0.16);
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Example: Light Theme
|
|
437
|
+
|
|
438
|
+
```css
|
|
439
|
+
.my-light-viewer {
|
|
440
|
+
--rsiv-overlay-bg: rgba(255, 255, 255, 0.95);
|
|
441
|
+
--rsiv-control-bg: rgba(0, 0, 0, 0.08);
|
|
442
|
+
--rsiv-control-bg-hover: rgba(0, 0, 0, 0.16);
|
|
443
|
+
--rsiv-control-color: #1a1a1a;
|
|
444
|
+
--rsiv-counter-bg: rgba(0, 0, 0, 0.6);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Browser Support
|
|
449
|
+
|
|
450
|
+
- Chrome 80+
|
|
451
|
+
- Firefox 75+
|
|
452
|
+
- Safari 13.1+
|
|
453
|
+
- Edge 80+
|
|
454
|
+
|
|
455
|
+
Touch gestures require a device with touch support.
|
|
456
|
+
|
|
457
|
+
## TypeScript
|
|
458
|
+
|
|
459
|
+
Full TypeScript support with exported types:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import type {
|
|
463
|
+
ImageViewerProps,
|
|
464
|
+
ImageSource,
|
|
465
|
+
ImageInput,
|
|
466
|
+
UseImageViewerReturn,
|
|
467
|
+
UseImageViewerOptions,
|
|
468
|
+
ControlsRenderProps,
|
|
469
|
+
NavigationRenderProps,
|
|
470
|
+
} from 'react-smart-image-viewer';
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Contributing
|
|
474
|
+
|
|
475
|
+
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
|
|
476
|
+
|
|
477
|
+
## License
|
|
478
|
+
|
|
479
|
+
MIT © [Your Name]
|
|
480
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),e=require("react");function Q(t){return typeof t=="string"?{src:t,alt:""}:t}function ee(t){return(Array.isArray(t)?t:[t]).map(Q)}function P(t,r,o){return Math.min(Math.max(t,r),o)}function U(t){if(t.length<2)return 0;const r=t[0].clientX-t[1].clientX,o=t[0].clientY-t[1].clientY;return Math.sqrt(r*r+o*o)}function H(t){var r,o;return t.length<2?{x:((r=t[0])==null?void 0:r.clientX)??0,y:((o=t[0])==null?void 0:o.clientY)??0}:{x:(t[0].clientX+t[1].clientX)/2,y:(t[0].clientY+t[1].clientY)/2}}function Ce(){if(typeof document>"u")return()=>{};const t=document.body.style.overflow,r=document.body.style.paddingRight,o=window.innerWidth-document.documentElement.clientWidth;return document.body.style.overflow="hidden",o>0&&(document.body.style.paddingRight=`${o}px`),()=>{document.body.style.overflow=t,document.body.style.paddingRight=r}}function K(t){let r=null,o=null;return(...f)=>{o=f,r===null&&(r=requestAnimationFrame(()=>{o&&t(...o),r=null}))}}function Le(){return typeof window>"u"?!1:"ontouchstart"in window||navigator.maxTouchPoints>0}let De=0;function _(t="rsiv"){return`${t}-${++De}`}function te(){return typeof window>"u"}const Se=["button:not([disabled])","[href]","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function Re(t){const r=e.useRef(null),o=e.useRef(null),f=e.useCallback(()=>r.current?Array.from(r.current.querySelectorAll(Se)):[],[]),p=e.useCallback(n=>{if(n.key!=="Tab")return;const b=f();if(b.length===0)return;const k=b[0],x=b[b.length-1];n.shiftKey?document.activeElement===k&&(n.preventDefault(),x.focus()):document.activeElement===x&&(n.preventDefault(),k.focus())},[f]);return e.useEffect(()=>{if(!t)return;o.current=document.activeElement;const n=f();return n.length>0&&requestAnimationFrame(()=>{n[0].focus()}),document.addEventListener("keydown",p),()=>{document.removeEventListener("keydown",p),o.current instanceof HTMLElement&&o.current.focus()}},[t,f,p]),r}const J={scale:1,translateX:0,translateY:0};function Ye(t){const{minZoom:r,maxZoom:o,zoomStep:f,onZoomChange:p}=t,[n,b]=e.useState(J),[k,x]=e.useState(!1),y=e.useRef(null),S=e.useRef(null),c=e.useRef({isGesturing:!1,startDistance:0,startScale:1,lastPosition:null,pinchCenter:null});e.useEffect(()=>{p==null||p(n.scale)},[n.scale,p]);const m=e.useCallback(l=>{b(u=>{const h={...u,...l};return h.scale=P(h.scale,r,o),h})},[r,o]),N=e.useCallback(()=>{m({scale:n.scale+f})},[n.scale,f,m]),C=e.useCallback(()=>{m({scale:n.scale-f})},[n.scale,f,m]),L=e.useCallback(()=>{b(J)},[]),X=e.useCallback(l=>{const u=P(l,r,o);b({scale:u,translateX:0,translateY:0})},[r,o]),R=e.useCallback(l=>{l.preventDefault();const u=l.deltaY>0?-f*.5:f*.5,h=P(n.scale+u,r,o);if(y.current){const i=y.current.getBoundingClientRect(),v=l.clientX-i.left-i.width/2,g=l.clientY-i.top-i.height/2,a=h/n.scale,d=n.translateX*a-v*(a-1),Y=n.translateY*a-g*(a-1);b({scale:h,translateX:d,translateY:Y})}else m({scale:h})},[n,f,r,o,m]),z=e.useCallback(l=>{if(l.preventDefault(),n.scale>1)L();else{const u=Math.min(2,o);if(y.current){const h=y.current.getBoundingClientRect(),i=l.clientX-h.left-h.width/2,v=l.clientY-h.top-h.height/2;b({scale:u,translateX:-i*(u-1),translateY:-v*(u-1)})}else m({scale:u})}},[n.scale,o,L,m]),j=e.useCallback(l=>{if(l.button!==0||n.scale<=1)return;l.preventDefault(),x(!0),c.current.lastPosition={x:l.clientX,y:l.clientY};const u=K(i=>{if(!c.current.lastPosition)return;const v=i.clientX-c.current.lastPosition.x,g=i.clientY-c.current.lastPosition.y;b(a=>({...a,translateX:a.translateX+v,translateY:a.translateY+g})),c.current.lastPosition={x:i.clientX,y:i.clientY}}),h=()=>{x(!1),c.current.lastPosition=null,document.removeEventListener("mousemove",u),document.removeEventListener("mouseup",h)};document.addEventListener("mousemove",u),document.addEventListener("mouseup",h)},[n.scale]),M=e.useCallback(l=>{l.touches.length===2?(l.preventDefault(),c.current={isGesturing:!0,startDistance:U(l.touches),startScale:n.scale,lastPosition:null,pinchCenter:H(l.touches)}):l.touches.length===1&&n.scale>1&&(c.current.lastPosition={x:l.touches[0].clientX,y:l.touches[0].clientY},x(!0));const u=K(i=>{if(i.touches.length===2&&c.current.isGesturing){i.preventDefault();const g=U(i.touches)/c.current.startDistance*c.current.startScale,a=P(g,r,o),d=H(i.touches);if(c.current.pinchCenter&&y.current){const Y=y.current.getBoundingClientRect(),w=c.current.pinchCenter.x-Y.left-Y.width/2,$=c.current.pinchCenter.y-Y.top-Y.height/2,I=a/n.scale;b({scale:a,translateX:n.translateX*I-w*(I-1)+(d.x-c.current.pinchCenter.x),translateY:n.translateY*I-$*(I-1)+(d.y-c.current.pinchCenter.y)})}else m({scale:a})}else if(i.touches.length===1&&c.current.lastPosition){const v=i.touches[0],g=v.clientX-c.current.lastPosition.x,a=v.clientY-c.current.lastPosition.y;b(d=>({...d,translateX:d.translateX+g,translateY:d.translateY+a})),c.current.lastPosition={x:v.clientX,y:v.clientY}}}),h=i=>{i.touches.length===0?(c.current={isGesturing:!1,startDistance:0,startScale:1,lastPosition:null,pinchCenter:null},x(!1),document.removeEventListener("touchmove",u),document.removeEventListener("touchend",h)):i.touches.length===1&&(c.current.isGesturing=!1,c.current.lastPosition={x:i.touches[0].clientX,y:i.touches[0].clientY})};document.addEventListener("touchmove",u,{passive:!1}),document.addEventListener("touchend",h)},[n,r,o,m]);return{transform:n,isDragging:k,zoomIn:N,zoomOut:C,resetZoom:L,setZoom:X,handleWheel:R,handleMouseDown:j,handleTouchStart:M,handleDoubleClick:z,containerRef:y,imageRef:S}}const ne=t=>s.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:[s.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),s.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]}),se=t=>s.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:[s.jsx("circle",{cx:"11",cy:"11",r:"8"}),s.jsx("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"}),s.jsx("line",{x1:"11",y1:"8",x2:"11",y2:"14"}),s.jsx("line",{x1:"8",y1:"11",x2:"14",y2:"11"})]}),re=t=>s.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:[s.jsx("circle",{cx:"11",cy:"11",r:"8"}),s.jsx("line",{x1:"21",y1:"21",x2:"16.65",y2:"16.65"}),s.jsx("line",{x1:"8",y1:"11",x2:"14",y2:"11"})]}),oe=t=>s.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:[s.jsx("polyline",{points:"1 4 1 10 7 10"}),s.jsx("path",{d:"M3.51 15a9 9 0 1 0 2.13-9.36L1 10"})]}),le=t=>s.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:s.jsx("polyline",{points:"15 18 9 12 15 6"})}),ae=t=>s.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",...t,children:s.jsx("polyline",{points:"9 18 15 12 9 6"})}),ce=({images:t,initialIndex:r=0,isOpen:o,defaultOpen:f=!1,onClose:p,onIndexChange:n,zoomStep:b=.5,minZoom:k=.5,maxZoom:x=4,showControls:y=!0,showNavigation:S=!0,showCounter:c=!0,closeOnOverlayClick:m=!0,closeOnEscape:N=!0,enableKeyboardNavigation:C=!0,loop:L=!1,className:X="",imageClassName:R="",animationDuration:z=200,ariaLabel:j="Image viewer",renderControls:M,renderNavigation:l})=>{const u=o!==void 0,[h,i]=e.useState(f),v=u?o:h,g=e.useMemo(()=>ee(t),[t]),a=g.length>1,[d,Y]=e.useState(r),w=g[d],[$,I]=e.useState(!0),ie=e.useRef(_("rsiv-viewer")),W=e.useRef(_("rsiv-title")),{transform:T,isDragging:ue,zoomIn:B,zoomOut:G,resetZoom:E,handleWheel:de,handleMouseDown:fe,handleTouchStart:me,handleDoubleClick:he,containerRef:ge,imageRef:ve}=Ye({minZoom:k,maxZoom:x,zoomStep:b}),pe=Re(v);e.useEffect(()=>{te()||document.documentElement.style.setProperty("--rsiv-animation-duration",`${z}ms`)},[z]),e.useEffect(()=>{if(v)return Ce()},[v]),e.useEffect(()=>{E(),I(!0)},[d,E]),e.useEffect(()=>{r!==d&&r>=0&&r<g.length&&Y(r)},[r,g.length]);const A=e.useCallback(()=>{u||i(!1),E(),p==null||p()},[u,p,E]),O=L||d<g.length-1,q=L||d>0,F=e.useCallback(()=>{if(!O)return;const D=d<g.length-1?d+1:0;Y(D),n==null||n(D)},[d,g.length,O,n]),V=e.useCallback(()=>{if(!q)return;const D=d>0?d-1:g.length-1;Y(D),n==null||n(D)},[d,g.length,q,n]);e.useEffect(()=>{if(!v)return;const D=Z=>{switch(Z.key){case"Escape":N&&(Z.preventDefault(),A());break;case"ArrowLeft":C&&a&&(Z.preventDefault(),V());break;case"ArrowRight":C&&a&&(Z.preventDefault(),F());break;case"+":case"=":Z.preventDefault(),B();break;case"-":Z.preventDefault(),G();break;case"0":Z.preventDefault(),E();break}};return document.addEventListener("keydown",D),()=>document.removeEventListener("keydown",D)},[v,N,C,a,A,F,V,B,G,E]);const be=e.useCallback(D=>{m&&D.target===D.currentTarget&&A()},[m,A]),xe=e.useCallback(()=>{I(!1)},[]),ye=e.useCallback(()=>{I(!1)},[]),we={transform:`translate(${T.translateX}px, ${T.translateY}px) scale(${T.scale})`,transition:ue?"none":void 0};if(!v)return null;const ke={zoomIn:B,zoomOut:G,resetZoom:E,currentZoom:T.scale,minZoom:k,maxZoom:x,close:A},je={goToPrevious:V,goToNext:F,currentIndex:d,totalImages:g.length,canGoPrevious:q,canGoNext:O};return s.jsxs("div",{ref:pe,className:`rsiv-overlay ${X}`,"data-open":v,role:"dialog","aria-modal":"true","aria-label":j,"aria-labelledby":w!=null&&w.title?W.current:void 0,id:ie.current,onClick:be,children:[s.jsx("button",{className:"rsiv-button rsiv-close",onClick:A,"aria-label":"Close image viewer",type:"button",children:s.jsx(ne,{"aria-hidden":!0})}),(w==null?void 0:w.title)&&s.jsx("div",{className:"rsiv-title",id:W.current,children:w.title}),s.jsxs("div",{ref:ge,className:"rsiv-container",onWheel:de,onTouchStart:me,children:[$&&s.jsx("div",{className:"rsiv-loader","aria-label":"Loading image"}),s.jsx("div",{className:"rsiv-image-wrapper",style:we,children:w&&s.jsx("img",{ref:ve,src:w.src,alt:w.alt||"",className:`rsiv-image ${R}`,"data-loading":$,"data-zoomed":T.scale>1,onLoad:xe,onError:ye,onMouseDown:fe,onDoubleClick:he,draggable:!1})})]}),a&&S&&(l?l(je):s.jsxs(s.Fragment,{children:[s.jsx("button",{className:"rsiv-button rsiv-nav rsiv-nav-prev",onClick:V,disabled:!q,"aria-label":"Previous image",type:"button",children:s.jsx(le,{"aria-hidden":!0})}),s.jsx("button",{className:"rsiv-button rsiv-nav rsiv-nav-next",onClick:F,disabled:!O,"aria-label":"Next image",type:"button",children:s.jsx(ae,{"aria-hidden":!0})})]})),a&&c&&s.jsxs("div",{className:"rsiv-counter","aria-live":"polite",children:[d+1," / ",g.length]}),y&&(M?M(ke):s.jsxs("div",{className:"rsiv-zoom-controls",children:[s.jsx("button",{className:"rsiv-button",onClick:G,disabled:T.scale<=k,"aria-label":"Zoom out",type:"button",children:s.jsx(re,{"aria-hidden":!0})}),s.jsx("button",{className:"rsiv-button",onClick:E,disabled:T.scale===1,"aria-label":"Reset zoom",type:"button",children:s.jsx(oe,{"aria-hidden":!0})}),s.jsx("button",{className:"rsiv-button",onClick:B,disabled:T.scale>=x,"aria-label":"Zoom in",type:"button",children:s.jsx(se,{"aria-hidden":!0})})]})),s.jsxs("div",{className:"rsiv-sr-only","aria-live":"polite","aria-atomic":"true",children:[a&&`Image ${d+1} of ${g.length}`,(w==null?void 0:w.alt)&&`. ${w.alt}`]})]})};ce.displayName="ImageViewer";function Xe(t={}){const{defaultOpen:r=!1,defaultIndex:o=0,totalImages:f=1,zoomStep:p=.5,minZoom:n=.5,maxZoom:b=4,loop:k=!1,onOpenChange:x,onIndexChange:y}=t,[S,c]=e.useState(r),[m,N]=e.useState(o),[C,L]=e.useState(1),X=e.useCallback(a=>{typeof a=="number"&&(N(P(a,0,f-1)),y==null||y(a)),c(!0),x==null||x(!0)},[f,x,y]),R=e.useCallback(()=>{c(!1),L(1),x==null||x(!1)},[x]),z=e.useCallback(()=>{S?R():X()},[S,X,R]),j=e.useCallback(a=>{const d=P(a,0,f-1);N(d),L(1),y==null||y(d)},[f,y]),M=e.useCallback(()=>{m<f-1?j(m+1):k&&j(0)},[m,f,k,j]),l=e.useCallback(()=>{m>0?j(m-1):k&&j(f-1)},[m,f,k,j]),u=e.useCallback(a=>{L(P(a,n,b))},[n,b]),h=e.useCallback(()=>{u(C+p)},[C,p,u]),i=e.useCallback(()=>{u(C-p)},[C,p,u]),v=e.useCallback(()=>{L(1)},[]),g=e.useCallback(()=>({isOpen:S,onClose:R,initialIndex:m,onIndexChange:j,zoomStep:p,minZoom:n,maxZoom:b,loop:k}),[S,R,m,j,p,n,b,k]);return e.useMemo(()=>({isOpen:S,open:X,close:R,toggle:z,currentIndex:m,setCurrentIndex:j,goToNext:M,goToPrevious:l,zoom:C,zoomIn:h,zoomOut:i,resetZoom:v,setZoom:u,getViewerProps:g}),[S,X,R,z,m,j,M,l,C,h,i,v,u,g])}exports.ChevronLeftIcon=le;exports.ChevronRightIcon=ae;exports.CloseIcon=ne;exports.ImageViewer=ce;exports.ResetIcon=oe;exports.ZoomInIcon=se;exports.ZoomOutIcon=re;exports.clamp=P;exports.isSSR=te;exports.isTouchDevice=Le;exports.normalizeImage=Q;exports.normalizeImages=ee;exports.useImageViewer=Xe;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/utils/index.ts","../src/hooks/useFocusTrap.ts","../src/hooks/useZoomPan.ts","../src/components/Icons.tsx","../src/components/ImageViewer.tsx","../src/hooks/useImageViewer.ts"],"sourcesContent":["import type { ImageInput, NormalizedImage, Position, TransformState } from '../types';\n\n/**\n * Normalizes image input to consistent ImageSource format\n */\nexport function normalizeImage(image: ImageInput): NormalizedImage {\n if (typeof image === 'string') {\n return { src: image, alt: '' };\n }\n return image;\n}\n\n/**\n * Normalizes array of images\n */\nexport function normalizeImages(images: ImageInput | ImageInput[]): NormalizedImage[] {\n const imageArray = Array.isArray(images) ? images : [images];\n return imageArray.map(normalizeImage);\n}\n\n/**\n * Clamps a value between min and max\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Touch-like interface for compatibility\n */\ninterface TouchLike {\n clientX: number;\n clientY: number;\n}\n\ninterface TouchListLike {\n length: number;\n [index: number]: TouchLike;\n}\n\n/**\n * Calculates distance between two touch points\n */\nexport function getTouchDistance(touches: TouchListLike): number {\n if (touches.length < 2) return 0;\n const dx = touches[0].clientX - touches[1].clientX;\n const dy = touches[0].clientY - touches[1].clientY;\n return Math.sqrt(dx * dx + dy * dy);\n}\n\n/**\n * Gets center point between two touches\n */\nexport function getTouchCenter(touches: TouchListLike): Position {\n if (touches.length < 2) {\n return { x: touches[0]?.clientX ?? 0, y: touches[0]?.clientY ?? 0 };\n }\n return {\n x: (touches[0].clientX + touches[1].clientX) / 2,\n y: (touches[0].clientY + touches[1].clientY) / 2,\n };\n}\n\n/**\n * Constrains transform to prevent image from going too far off-screen\n */\nexport function constrainTransform(\n transform: TransformState,\n containerWidth: number,\n containerHeight: number,\n imageWidth: number,\n imageHeight: number\n): TransformState {\n const scaledWidth = imageWidth * transform.scale;\n const scaledHeight = imageHeight * transform.scale;\n \n // If image is smaller than container, center it\n if (scaledWidth <= containerWidth) {\n transform.translateX = 0;\n } else {\n // Allow panning but keep image edges visible\n const maxTranslateX = (scaledWidth - containerWidth) / 2;\n transform.translateX = clamp(transform.translateX, -maxTranslateX, maxTranslateX);\n }\n \n if (scaledHeight <= containerHeight) {\n transform.translateY = 0;\n } else {\n const maxTranslateY = (scaledHeight - containerHeight) / 2;\n transform.translateY = clamp(transform.translateY, -maxTranslateY, maxTranslateY);\n }\n \n return transform;\n}\n\n/**\n * Prevents body scroll\n */\nexport function preventBodyScroll(): () => void {\n // Check if we're in a browser environment\n if (typeof document === 'undefined') {\n return () => {};\n }\n \n const originalStyle = document.body.style.overflow;\n const originalPaddingRight = document.body.style.paddingRight;\n \n // Get scrollbar width\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n \n document.body.style.overflow = 'hidden';\n if (scrollbarWidth > 0) {\n document.body.style.paddingRight = `${scrollbarWidth}px`;\n }\n \n return () => {\n document.body.style.overflow = originalStyle;\n document.body.style.paddingRight = originalPaddingRight;\n };\n}\n\n/**\n * Creates a throttled function using requestAnimationFrame\n */\nexport function rafThrottle<T extends (...args: never[]) => void>(\n fn: T\n): (...args: Parameters<T>) => void {\n let rafId: number | null = null;\n let lastArgs: Parameters<T> | null = null;\n \n return (...args: Parameters<T>) => {\n lastArgs = args;\n \n if (rafId === null) {\n rafId = requestAnimationFrame(() => {\n if (lastArgs) {\n fn(...lastArgs);\n }\n rafId = null;\n });\n }\n };\n}\n\n/**\n * Detects if device supports touch\n */\nexport function isTouchDevice(): boolean {\n if (typeof window === 'undefined') return false;\n return 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n}\n\n/**\n * Generates unique ID for accessibility\n */\nlet idCounter = 0;\nexport function generateId(prefix = 'rsiv'): string {\n return `${prefix}-${++idCounter}`;\n}\n\n/**\n * Check if we're in SSR environment\n */\nexport function isSSR(): boolean {\n return typeof window === 'undefined';\n}\n\n","import { useEffect, useRef, useCallback } from 'react';\n\nconst FOCUSABLE_ELEMENTS = [\n 'button:not([disabled])',\n '[href]',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\n/**\n * Hook to trap focus within a container element\n * Essential for modal accessibility\n */\nexport function useFocusTrap(isActive: boolean) {\n const containerRef = useRef<HTMLDivElement>(null);\n const previousActiveElement = useRef<Element | null>(null);\n\n const getFocusableElements = useCallback(() => {\n if (!containerRef.current) return [];\n return Array.from(\n containerRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS)\n );\n }, []);\n\n const handleKeyDown = useCallback((event: KeyboardEvent) => {\n if (event.key !== 'Tab') return;\n\n const focusableElements = getFocusableElements();\n if (focusableElements.length === 0) return;\n\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n // Shift + Tab (backwards)\n if (event.shiftKey) {\n if (document.activeElement === firstElement) {\n event.preventDefault();\n lastElement.focus();\n }\n } else {\n // Tab (forwards)\n if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n }\n }, [getFocusableElements]);\n\n useEffect(() => {\n if (!isActive) return;\n\n // Store the currently focused element\n previousActiveElement.current = document.activeElement;\n\n // Focus the first focusable element in the trap\n const focusableElements = getFocusableElements();\n if (focusableElements.length > 0) {\n // Small delay to ensure the modal is rendered\n requestAnimationFrame(() => {\n focusableElements[0].focus();\n });\n }\n\n // Add keyboard listener\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n\n // Restore focus to the previous element\n if (previousActiveElement.current instanceof HTMLElement) {\n previousActiveElement.current.focus();\n }\n };\n }, [isActive, getFocusableElements, handleKeyDown]);\n\n return containerRef;\n}\n\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport type { TransformState, GestureState } from '../types';\nimport { clamp, getTouchDistance, getTouchCenter, rafThrottle } from '../utils';\n\ninterface UseZoomPanOptions {\n minZoom: number;\n maxZoom: number;\n zoomStep: number;\n onZoomChange?: (zoom: number) => void;\n}\n\ninterface UseZoomPanReturn {\n transform: TransformState;\n isDragging: boolean;\n zoomIn: () => void;\n zoomOut: () => void;\n resetZoom: () => void;\n setZoom: (zoom: number) => void;\n handleWheel: (e: React.WheelEvent) => void;\n handleMouseDown: (e: React.MouseEvent) => void;\n handleTouchStart: (e: React.TouchEvent) => void;\n handleDoubleClick: (e: React.MouseEvent) => void;\n containerRef: React.RefObject<HTMLDivElement>;\n imageRef: React.RefObject<HTMLImageElement>;\n}\n\nconst initialTransform: TransformState = {\n scale: 1,\n translateX: 0,\n translateY: 0,\n};\n\n/**\n * Hook to handle zoom and pan interactions\n */\nexport function useZoomPan(options: UseZoomPanOptions): UseZoomPanReturn {\n const { minZoom, maxZoom, zoomStep, onZoomChange } = options;\n\n const [transform, setTransform] = useState<TransformState>(initialTransform);\n const [isDragging, setIsDragging] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const imageRef = useRef<HTMLImageElement>(null);\n const gestureRef = useRef<GestureState>({\n isGesturing: false,\n startDistance: 0,\n startScale: 1,\n lastPosition: null,\n pinchCenter: null,\n });\n\n // Notify parent of zoom changes\n useEffect(() => {\n onZoomChange?.(transform.scale);\n }, [transform.scale, onZoomChange]);\n\n const updateTransform = useCallback((updates: Partial<TransformState>) => {\n setTransform((prev) => {\n const newTransform = { ...prev, ...updates };\n newTransform.scale = clamp(newTransform.scale, minZoom, maxZoom);\n return newTransform;\n });\n }, [minZoom, maxZoom]);\n\n const zoomIn = useCallback(() => {\n updateTransform({ scale: transform.scale + zoomStep });\n }, [transform.scale, zoomStep, updateTransform]);\n\n const zoomOut = useCallback(() => {\n updateTransform({ scale: transform.scale - zoomStep });\n }, [transform.scale, zoomStep, updateTransform]);\n\n const resetZoom = useCallback(() => {\n setTransform(initialTransform);\n }, []);\n\n const setZoom = useCallback((zoom: number) => {\n const clampedZoom = clamp(zoom, minZoom, maxZoom);\n // Reset position when setting zoom directly\n setTransform({\n scale: clampedZoom,\n translateX: 0,\n translateY: 0,\n });\n }, [minZoom, maxZoom]);\n\n // Mouse wheel zoom\n const handleWheel = useCallback((e: React.WheelEvent) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -zoomStep * 0.5 : zoomStep * 0.5;\n const newScale = clamp(transform.scale + delta, minZoom, maxZoom);\n\n // Zoom towards cursor position\n if (containerRef.current) {\n const rect = containerRef.current.getBoundingClientRect();\n const x = e.clientX - rect.left - rect.width / 2;\n const y = e.clientY - rect.top - rect.height / 2;\n\n const scaleDiff = newScale / transform.scale;\n const newTranslateX = transform.translateX * scaleDiff - x * (scaleDiff - 1);\n const newTranslateY = transform.translateY * scaleDiff - y * (scaleDiff - 1);\n\n setTransform({\n scale: newScale,\n translateX: newTranslateX,\n translateY: newTranslateY,\n });\n } else {\n updateTransform({ scale: newScale });\n }\n }, [transform, zoomStep, minZoom, maxZoom, updateTransform]);\n\n // Double click to zoom\n const handleDoubleClick = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n \n if (transform.scale > 1) {\n // Reset zoom\n resetZoom();\n } else {\n // Zoom in to 2x at click position\n const targetScale = Math.min(2, maxZoom);\n \n if (containerRef.current) {\n const rect = containerRef.current.getBoundingClientRect();\n const x = e.clientX - rect.left - rect.width / 2;\n const y = e.clientY - rect.top - rect.height / 2;\n \n setTransform({\n scale: targetScale,\n translateX: -x * (targetScale - 1),\n translateY: -y * (targetScale - 1),\n });\n } else {\n updateTransform({ scale: targetScale });\n }\n }\n }, [transform.scale, maxZoom, resetZoom, updateTransform]);\n\n // Mouse drag handlers\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n if (e.button !== 0) return; // Only left click\n if (transform.scale <= 1) return; // Only drag when zoomed\n\n e.preventDefault();\n setIsDragging(true);\n gestureRef.current.lastPosition = { x: e.clientX, y: e.clientY };\n\n const handleMouseMove = rafThrottle((moveEvent: MouseEvent) => {\n if (!gestureRef.current.lastPosition) return;\n\n const deltaX = moveEvent.clientX - gestureRef.current.lastPosition.x;\n const deltaY = moveEvent.clientY - gestureRef.current.lastPosition.y;\n\n setTransform((prev) => ({\n ...prev,\n translateX: prev.translateX + deltaX,\n translateY: prev.translateY + deltaY,\n }));\n\n gestureRef.current.lastPosition = { x: moveEvent.clientX, y: moveEvent.clientY };\n });\n\n const handleMouseUp = () => {\n setIsDragging(false);\n gestureRef.current.lastPosition = null;\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n }, [transform.scale]);\n\n // Touch handlers for pinch-to-zoom and drag\n const handleTouchStart = useCallback((e: React.TouchEvent) => {\n if (e.touches.length === 2) {\n // Pinch gesture start\n e.preventDefault();\n gestureRef.current = {\n isGesturing: true,\n startDistance: getTouchDistance(e.touches),\n startScale: transform.scale,\n lastPosition: null,\n pinchCenter: getTouchCenter(e.touches),\n };\n } else if (e.touches.length === 1 && transform.scale > 1) {\n // Single touch drag when zoomed\n gestureRef.current.lastPosition = {\n x: e.touches[0].clientX,\n y: e.touches[0].clientY,\n };\n setIsDragging(true);\n }\n\n const handleTouchMove = rafThrottle((moveEvent: TouchEvent) => {\n if (moveEvent.touches.length === 2 && gestureRef.current.isGesturing) {\n // Pinch zoom\n moveEvent.preventDefault();\n const currentDistance = getTouchDistance(moveEvent.touches);\n const scale = (currentDistance / gestureRef.current.startDistance) * gestureRef.current.startScale;\n const clampedScale = clamp(scale, minZoom, maxZoom);\n\n // Zoom towards pinch center\n const newCenter = getTouchCenter(moveEvent.touches);\n if (gestureRef.current.pinchCenter && containerRef.current) {\n const rect = containerRef.current.getBoundingClientRect();\n const centerX = gestureRef.current.pinchCenter.x - rect.left - rect.width / 2;\n const centerY = gestureRef.current.pinchCenter.y - rect.top - rect.height / 2;\n\n const scaleDiff = clampedScale / transform.scale;\n \n setTransform({\n scale: clampedScale,\n translateX: transform.translateX * scaleDiff - centerX * (scaleDiff - 1) + (newCenter.x - gestureRef.current.pinchCenter.x),\n translateY: transform.translateY * scaleDiff - centerY * (scaleDiff - 1) + (newCenter.y - gestureRef.current.pinchCenter.y),\n });\n } else {\n updateTransform({ scale: clampedScale });\n }\n } else if (moveEvent.touches.length === 1 && gestureRef.current.lastPosition) {\n // Single touch drag\n const touch = moveEvent.touches[0];\n const deltaX = touch.clientX - gestureRef.current.lastPosition.x;\n const deltaY = touch.clientY - gestureRef.current.lastPosition.y;\n\n setTransform((prev) => ({\n ...prev,\n translateX: prev.translateX + deltaX,\n translateY: prev.translateY + deltaY,\n }));\n\n gestureRef.current.lastPosition = { x: touch.clientX, y: touch.clientY };\n }\n });\n\n const handleTouchEnd = (endEvent: TouchEvent) => {\n if (endEvent.touches.length === 0) {\n gestureRef.current = {\n isGesturing: false,\n startDistance: 0,\n startScale: 1,\n lastPosition: null,\n pinchCenter: null,\n };\n setIsDragging(false);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n } else if (endEvent.touches.length === 1) {\n // Transition from pinch to drag\n gestureRef.current.isGesturing = false;\n gestureRef.current.lastPosition = {\n x: endEvent.touches[0].clientX,\n y: endEvent.touches[0].clientY,\n };\n }\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n }, [transform, minZoom, maxZoom, updateTransform]);\n\n return {\n transform,\n isDragging,\n zoomIn,\n zoomOut,\n resetZoom,\n setZoom,\n handleWheel,\n handleMouseDown,\n handleTouchStart,\n handleDoubleClick,\n containerRef,\n imageRef,\n };\n}\n\n","import React from 'react';\n\ninterface IconProps {\n className?: string;\n 'aria-hidden'?: boolean;\n}\n\n/**\n * SVG Icons for the ImageViewer controls\n * Using inline SVGs to avoid external dependencies\n */\n\nexport const CloseIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const ZoomInIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n <line x1=\"11\" y1=\"8\" x2=\"11\" y2=\"14\" />\n <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\" />\n </svg>\n);\n\nexport const ZoomOutIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n <line x1=\"8\" y1=\"11\" x2=\"14\" y2=\"11\" />\n </svg>\n);\n\nexport const ResetIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <polyline points=\"1 4 1 10 7 10\" />\n <path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\" />\n </svg>\n);\n\nexport const ChevronLeftIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <polyline points=\"15 18 9 12 15 6\" />\n </svg>\n);\n\nexport const ChevronRightIcon: React.FC<IconProps> = (props) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n {...props}\n >\n <polyline points=\"9 18 15 12 9 6\" />\n </svg>\n);\n\n","import React, {\n useState,\n useEffect,\n useCallback,\n useMemo,\n useRef,\n} from 'react';\nimport type { ImageViewerProps, NormalizedImage } from '../types';\nimport { normalizeImages, preventBodyScroll, generateId, isSSR } from '../utils';\nimport { useFocusTrap } from '../hooks/useFocusTrap';\nimport { useZoomPan } from '../hooks/useZoomPan';\nimport {\n CloseIcon,\n ZoomInIcon,\n ZoomOutIcon,\n ResetIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n} from './Icons';\nimport '../styles/index.css';\n\n/**\n * ImageViewer - A high-performance image viewer with zoom, pan, and gallery support\n *\n * @example\n * ```tsx\n * // Single image\n * <ImageViewer images=\"https://example.com/image.jpg\" isOpen={isOpen} onClose={() => setIsOpen(false)} />\n *\n * // Gallery\n * <ImageViewer images={['image1.jpg', 'image2.jpg']} isOpen={isOpen} onClose={() => setIsOpen(false)} />\n * ```\n */\nexport const ImageViewer: React.FC<ImageViewerProps> = ({\n images,\n initialIndex = 0,\n isOpen: controlledIsOpen,\n defaultOpen = false,\n onClose,\n onIndexChange,\n zoomStep = 0.5,\n minZoom = 0.5,\n maxZoom = 4,\n showControls = true,\n showNavigation = true,\n showCounter = true,\n closeOnOverlayClick = true,\n closeOnEscape = true,\n enableKeyboardNavigation = true,\n loop = false,\n className = '',\n imageClassName = '',\n animationDuration = 200,\n ariaLabel = 'Image viewer',\n renderControls,\n renderNavigation,\n}) => {\n // Determine controlled vs uncontrolled mode\n const isControlled = controlledIsOpen !== undefined;\n const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);\n const isOpen = isControlled ? controlledIsOpen : internalIsOpen;\n\n // Normalize images\n const normalizedImages = useMemo(() => normalizeImages(images), [images]);\n const isGallery = normalizedImages.length > 1;\n\n // Image index state\n const [currentIndex, setCurrentIndex] = useState(initialIndex);\n const currentImage: NormalizedImage | undefined = normalizedImages[currentIndex];\n\n // Loading state\n const [isLoading, setIsLoading] = useState(true);\n\n // Generate unique IDs for accessibility\n const viewerId = useRef(generateId('rsiv-viewer'));\n const titleId = useRef(generateId('rsiv-title'));\n\n // Zoom and pan\n const {\n transform,\n isDragging,\n zoomIn,\n zoomOut,\n resetZoom,\n handleWheel,\n handleMouseDown,\n handleTouchStart,\n handleDoubleClick,\n containerRef,\n imageRef,\n } = useZoomPan({\n minZoom,\n maxZoom,\n zoomStep,\n });\n\n // Focus trap for accessibility\n const focusTrapRef = useFocusTrap(isOpen);\n\n // Apply CSS custom property for animation duration\n useEffect(() => {\n if (!isSSR()) {\n document.documentElement.style.setProperty(\n '--rsiv-animation-duration',\n `${animationDuration}ms`\n );\n }\n }, [animationDuration]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (isOpen) {\n return preventBodyScroll();\n }\n }, [isOpen]);\n\n // Reset zoom when image changes\n useEffect(() => {\n resetZoom();\n setIsLoading(true);\n }, [currentIndex, resetZoom]);\n\n // Sync external index changes\n useEffect(() => {\n if (initialIndex !== currentIndex && initialIndex >= 0 && initialIndex < normalizedImages.length) {\n setCurrentIndex(initialIndex);\n }\n }, [initialIndex, normalizedImages.length]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Close handler\n const handleClose = useCallback(() => {\n if (!isControlled) {\n setInternalIsOpen(false);\n }\n resetZoom();\n onClose?.();\n }, [isControlled, onClose, resetZoom]);\n\n // Navigation handlers\n const canGoNext = loop || currentIndex < normalizedImages.length - 1;\n const canGoPrevious = loop || currentIndex > 0;\n\n const goToNext = useCallback(() => {\n if (!canGoNext) return;\n\n const nextIndex = currentIndex < normalizedImages.length - 1\n ? currentIndex + 1\n : 0;\n setCurrentIndex(nextIndex);\n onIndexChange?.(nextIndex);\n }, [currentIndex, normalizedImages.length, canGoNext, onIndexChange]);\n\n const goToPrevious = useCallback(() => {\n if (!canGoPrevious) return;\n\n const prevIndex = currentIndex > 0\n ? currentIndex - 1\n : normalizedImages.length - 1;\n setCurrentIndex(prevIndex);\n onIndexChange?.(prevIndex);\n }, [currentIndex, normalizedImages.length, canGoPrevious, onIndexChange]);\n\n // Keyboard navigation\n useEffect(() => {\n if (!isOpen) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'Escape':\n if (closeOnEscape) {\n e.preventDefault();\n handleClose();\n }\n break;\n case 'ArrowLeft':\n if (enableKeyboardNavigation && isGallery) {\n e.preventDefault();\n goToPrevious();\n }\n break;\n case 'ArrowRight':\n if (enableKeyboardNavigation && isGallery) {\n e.preventDefault();\n goToNext();\n }\n break;\n case '+':\n case '=':\n e.preventDefault();\n zoomIn();\n break;\n case '-':\n e.preventDefault();\n zoomOut();\n break;\n case '0':\n e.preventDefault();\n resetZoom();\n break;\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [\n isOpen,\n closeOnEscape,\n enableKeyboardNavigation,\n isGallery,\n handleClose,\n goToNext,\n goToPrevious,\n zoomIn,\n zoomOut,\n resetZoom,\n ]);\n\n // Overlay click handler\n const handleOverlayClick = useCallback((e: React.MouseEvent) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n handleClose();\n }\n }, [closeOnOverlayClick, handleClose]);\n\n // Image load handler\n const handleImageLoad = useCallback(() => {\n setIsLoading(false);\n }, []);\n\n // Image error handler\n const handleImageError = useCallback(() => {\n setIsLoading(false);\n }, []);\n\n // Transform style\n const transformStyle: React.CSSProperties = {\n transform: `translate(${transform.translateX}px, ${transform.translateY}px) scale(${transform.scale})`,\n transition: isDragging ? 'none' : undefined,\n };\n\n // Don't render anything if not open and SSR\n if (!isOpen) {\n return null;\n }\n\n // Custom controls render\n const controlsRenderProps = {\n zoomIn,\n zoomOut,\n resetZoom,\n currentZoom: transform.scale,\n minZoom,\n maxZoom,\n close: handleClose,\n };\n\n // Custom navigation render\n const navigationRenderProps = {\n goToPrevious,\n goToNext,\n currentIndex,\n totalImages: normalizedImages.length,\n canGoPrevious,\n canGoNext,\n };\n\n return (\n <div\n ref={focusTrapRef}\n className={`rsiv-overlay ${className}`}\n data-open={isOpen}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={ariaLabel}\n aria-labelledby={currentImage?.title ? titleId.current : undefined}\n id={viewerId.current}\n onClick={handleOverlayClick}\n >\n {/* Close button */}\n <button\n className=\"rsiv-button rsiv-close\"\n onClick={handleClose}\n aria-label=\"Close image viewer\"\n type=\"button\"\n >\n <CloseIcon aria-hidden />\n </button>\n\n {/* Image title */}\n {currentImage?.title && (\n <div className=\"rsiv-title\" id={titleId.current}>\n {currentImage.title}\n </div>\n )}\n\n {/* Main container */}\n <div\n ref={containerRef}\n className=\"rsiv-container\"\n onWheel={handleWheel}\n onTouchStart={handleTouchStart}\n >\n {/* Loading indicator */}\n {isLoading && <div className=\"rsiv-loader\" aria-label=\"Loading image\" />}\n\n {/* Image */}\n <div className=\"rsiv-image-wrapper\" style={transformStyle}>\n {currentImage && (\n <img\n ref={imageRef}\n src={currentImage.src}\n alt={currentImage.alt || ''}\n className={`rsiv-image ${imageClassName}`}\n data-loading={isLoading}\n data-zoomed={transform.scale > 1}\n onLoad={handleImageLoad}\n onError={handleImageError}\n onMouseDown={handleMouseDown}\n onDoubleClick={handleDoubleClick}\n draggable={false}\n />\n )}\n </div>\n </div>\n\n {/* Navigation arrows */}\n {isGallery && showNavigation && (\n renderNavigation ? (\n renderNavigation(navigationRenderProps)\n ) : (\n <>\n <button\n className=\"rsiv-button rsiv-nav rsiv-nav-prev\"\n onClick={goToPrevious}\n disabled={!canGoPrevious}\n aria-label=\"Previous image\"\n type=\"button\"\n >\n <ChevronLeftIcon aria-hidden />\n </button>\n <button\n className=\"rsiv-button rsiv-nav rsiv-nav-next\"\n onClick={goToNext}\n disabled={!canGoNext}\n aria-label=\"Next image\"\n type=\"button\"\n >\n <ChevronRightIcon aria-hidden />\n </button>\n </>\n )\n )}\n\n {/* Image counter */}\n {isGallery && showCounter && (\n <div className=\"rsiv-counter\" aria-live=\"polite\">\n {currentIndex + 1} / {normalizedImages.length}\n </div>\n )}\n\n {/* Zoom controls */}\n {showControls && (\n renderControls ? (\n renderControls(controlsRenderProps)\n ) : (\n <div className=\"rsiv-zoom-controls\">\n <button\n className=\"rsiv-button\"\n onClick={zoomOut}\n disabled={transform.scale <= minZoom}\n aria-label=\"Zoom out\"\n type=\"button\"\n >\n <ZoomOutIcon aria-hidden />\n </button>\n <button\n className=\"rsiv-button\"\n onClick={resetZoom}\n disabled={transform.scale === 1}\n aria-label=\"Reset zoom\"\n type=\"button\"\n >\n <ResetIcon aria-hidden />\n </button>\n <button\n className=\"rsiv-button\"\n onClick={zoomIn}\n disabled={transform.scale >= maxZoom}\n aria-label=\"Zoom in\"\n type=\"button\"\n >\n <ZoomInIcon aria-hidden />\n </button>\n </div>\n )\n )}\n\n {/* Screen reader announcements */}\n <div className=\"rsiv-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n {isGallery && `Image ${currentIndex + 1} of ${normalizedImages.length}`}\n {currentImage?.alt && `. ${currentImage.alt}`}\n </div>\n </div>\n );\n};\n\nImageViewer.displayName = 'ImageViewer';\n\n","import { useState, useCallback, useMemo } from 'react';\nimport type { UseImageViewerOptions, UseImageViewerReturn, ImageViewerProps } from '../types';\nimport { clamp } from '../utils';\n\n/**\n * Hook for controlling the ImageViewer programmatically\n * \n * @example\n * ```tsx\n * const viewer = useImageViewer({ totalImages: 5 });\n * \n * return (\n * <>\n * <button onClick={() => viewer.open(0)}>Open Gallery</button>\n * <ImageViewer images={images} {...viewer.getViewerProps()} />\n * </>\n * );\n * ```\n */\nexport function useImageViewer(options: UseImageViewerOptions = {}): UseImageViewerReturn {\n const {\n defaultOpen = false,\n defaultIndex = 0,\n totalImages = 1,\n zoomStep = 0.5,\n minZoom = 0.5,\n maxZoom = 4,\n loop = false,\n onOpenChange,\n onIndexChange,\n } = options;\n\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const [currentIndex, setCurrentIndexState] = useState(defaultIndex);\n const [zoom, setZoomState] = useState(1);\n\n const open = useCallback((index?: number) => {\n if (typeof index === 'number') {\n setCurrentIndexState(clamp(index, 0, totalImages - 1));\n onIndexChange?.(index);\n }\n setIsOpen(true);\n onOpenChange?.(true);\n }, [totalImages, onOpenChange, onIndexChange]);\n\n const close = useCallback(() => {\n setIsOpen(false);\n setZoomState(1);\n onOpenChange?.(false);\n }, [onOpenChange]);\n\n const toggle = useCallback(() => {\n if (isOpen) {\n close();\n } else {\n open();\n }\n }, [isOpen, open, close]);\n\n const setCurrentIndex = useCallback((index: number) => {\n const clampedIndex = clamp(index, 0, totalImages - 1);\n setCurrentIndexState(clampedIndex);\n setZoomState(1); // Reset zoom when changing images\n onIndexChange?.(clampedIndex);\n }, [totalImages, onIndexChange]);\n\n const goToNext = useCallback(() => {\n if (currentIndex < totalImages - 1) {\n setCurrentIndex(currentIndex + 1);\n } else if (loop) {\n setCurrentIndex(0);\n }\n }, [currentIndex, totalImages, loop, setCurrentIndex]);\n\n const goToPrevious = useCallback(() => {\n if (currentIndex > 0) {\n setCurrentIndex(currentIndex - 1);\n } else if (loop) {\n setCurrentIndex(totalImages - 1);\n }\n }, [currentIndex, totalImages, loop, setCurrentIndex]);\n\n const setZoom = useCallback((newZoom: number) => {\n setZoomState(clamp(newZoom, minZoom, maxZoom));\n }, [minZoom, maxZoom]);\n\n const zoomIn = useCallback(() => {\n setZoom(zoom + zoomStep);\n }, [zoom, zoomStep, setZoom]);\n\n const zoomOut = useCallback(() => {\n setZoom(zoom - zoomStep);\n }, [zoom, zoomStep, setZoom]);\n\n const resetZoom = useCallback(() => {\n setZoomState(1);\n }, []);\n\n const getViewerProps = useCallback((): Partial<ImageViewerProps> => ({\n isOpen,\n onClose: close,\n initialIndex: currentIndex,\n onIndexChange: setCurrentIndex,\n zoomStep,\n minZoom,\n maxZoom,\n loop,\n }), [isOpen, close, currentIndex, setCurrentIndex, zoomStep, minZoom, maxZoom, loop]);\n\n return useMemo(() => ({\n isOpen,\n open,\n close,\n toggle,\n currentIndex,\n setCurrentIndex,\n goToNext,\n goToPrevious,\n zoom,\n zoomIn,\n zoomOut,\n resetZoom,\n setZoom,\n getViewerProps,\n }), [\n isOpen,\n open,\n close,\n toggle,\n currentIndex,\n setCurrentIndex,\n goToNext,\n goToPrevious,\n zoom,\n zoomIn,\n zoomOut,\n resetZoom,\n setZoom,\n getViewerProps,\n ]);\n}\n\n"],"names":["normalizeImage","image","normalizeImages","images","clamp","value","min","max","getTouchDistance","touches","dx","dy","getTouchCenter","_a","_b","preventBodyScroll","originalStyle","originalPaddingRight","scrollbarWidth","rafThrottle","fn","rafId","lastArgs","args","isTouchDevice","idCounter","generateId","prefix","isSSR","FOCUSABLE_ELEMENTS","useFocusTrap","isActive","containerRef","useRef","previousActiveElement","getFocusableElements","useCallback","handleKeyDown","event","focusableElements","firstElement","lastElement","useEffect","initialTransform","useZoomPan","options","minZoom","maxZoom","zoomStep","onZoomChange","transform","setTransform","useState","isDragging","setIsDragging","imageRef","gestureRef","updateTransform","updates","prev","newTransform","zoomIn","zoomOut","resetZoom","setZoom","zoom","clampedZoom","handleWheel","e","delta","newScale","rect","x","y","scaleDiff","newTranslateX","newTranslateY","handleDoubleClick","targetScale","handleMouseDown","handleMouseMove","moveEvent","deltaX","deltaY","handleMouseUp","handleTouchStart","handleTouchMove","scale","clampedScale","newCenter","centerX","centerY","touch","handleTouchEnd","endEvent","CloseIcon","props","jsxs","jsx","ZoomInIcon","ZoomOutIcon","ResetIcon","ChevronLeftIcon","ChevronRightIcon","ImageViewer","initialIndex","controlledIsOpen","defaultOpen","onClose","onIndexChange","showControls","showNavigation","showCounter","closeOnOverlayClick","closeOnEscape","enableKeyboardNavigation","loop","className","imageClassName","animationDuration","ariaLabel","renderControls","renderNavigation","isControlled","internalIsOpen","setInternalIsOpen","isOpen","normalizedImages","useMemo","isGallery","currentIndex","setCurrentIndex","currentImage","isLoading","setIsLoading","viewerId","titleId","focusTrapRef","handleClose","canGoNext","canGoPrevious","goToNext","nextIndex","goToPrevious","prevIndex","handleOverlayClick","handleImageLoad","handleImageError","transformStyle","controlsRenderProps","navigationRenderProps","Fragment","useImageViewer","defaultIndex","totalImages","onOpenChange","setIsOpen","setCurrentIndexState","setZoomState","open","index","close","toggle","clampedIndex","newZoom","getViewerProps"],"mappings":"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"}
|