use-zoom-pinch 0.1.0 → 0.3.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 nemezzizz
3
+ Copyright (c) 2026 NemeZZiZZ
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # useZoomPinch
2
2
 
3
- Lightweight React hook for **pan**, **pinch-to-zoom**, and **scroll zoom** with trackpad and touch support. Zero dependencies beyond React.
3
+ Lightweight React hook for **pan**, **pinch-to-zoom**, **rotation**, and **scroll zoom** with trackpad and touch support. Zero dependencies beyond React.
4
+
5
+ [![npm](https://img.shields.io/npm/v/use-zoom-pinch)](https://www.npmjs.com/package/use-zoom-pinch)
6
+ [![CI](https://github.com/NemeZZiZZ/use-zoom-pinch/actions/workflows/ci.yml/badge.svg)](https://github.com/NemeZZiZZ/use-zoom-pinch/actions/workflows/ci.yml)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/use-zoom-pinch)](https://bundlephobia.com/package/use-zoom-pinch)
4
8
 
5
9
  [**Live Demo**](https://nemezzizz.github.io/use-zoom-pinch/)
6
10
 
@@ -9,11 +13,33 @@ Lightweight React hook for **pan**, **pinch-to-zoom**, and **scroll zoom** with
9
13
  - **Scroll to pan** — mouse wheel and trackpad two-finger scroll
10
14
  - **Pinch to zoom** — trackpad pinch (via `ctrlKey` + wheel) and multi-touch pinch on mobile
11
15
  - **Pointer drag** — mouse drag and single-touch drag for panning
16
+ - **Rotation** — two-finger rotation with `rotateTo`/`rotateBy` methods
17
+ - **Gesture toggles** — enable/disable pan, zoom, and rotate independently
18
+ - **Double-tap zoom** — configurable toggle/zoomIn/reset modes
19
+ - **Inertia** — momentum-based pan with configurable friction
20
+ - **Animated transitions** — smooth easing for programmatic view changes
21
+ - **Bounds** — constrain panning within rectangular limits
22
+ - **Keyboard navigation** — arrow keys, +/-, rotation with [ / ]
23
+ - **Coordinate conversion** — `screenToContent` / `contentToScreen`
24
+ - **Zoom snap levels** — snap to predefined zoom levels on gesture end
25
+ - **Snap to grid** — snap positions to a grid (continuous or on end)
26
+ - **Auto-fit content** — `fitToContent` with ResizeObserver auto-resize
27
+ - **Configurable pan button** — left, middle, or right mouse button
28
+ - **Bounce at bounds** — rubber-band overscroll with snap-back animation
29
+ - **Axis locking** — restrict pan to horizontal or vertical only
30
+ - **Cursor management** — automatic grab/grabbing cursor on drag
31
+ - **Wheel mode** — swap default: wheel zooms, ctrl+wheel pans
32
+ - **Rotation constraints** — min/max angle and rotation snap levels
33
+ - **Activation keys** — require Shift/Alt/Ctrl for specific gestures
34
+ - **`onTransformEnd`** — unified callback after any gesture ends
35
+ - **Navigation** — `panTo`, `panBy`, `zoomTo`, `fitToRect` for precise viewport control
12
36
  - **Controlled & uncontrolled** modes
37
+ - **Granular events** — `onPanStart/End`, `onZoomStart/End`, `onPinchStart/End`, `onRotateStart/End`
38
+ - **Event filtering** — `shouldHandleEvent` to exclude interactive elements
13
39
  - **Stable listeners** — config changes don't re-register event listeners
14
40
  - **TypeScript-first** with full type exports
15
41
  - **Tree-shakeable** ESM + CJS dual build
16
- - **~2 KB** minified + gzipped
42
+ - **~5.2 KB** minified + gzipped
17
43
 
18
44
  ## Installation
19
45
 
@@ -59,7 +85,7 @@ function ControlledCanvas() {
59
85
  const containerRef = useRef<HTMLDivElement>(null)
60
86
  const [viewState, setViewState] = useState<ViewState>({ x: 0, y: 0, zoom: 1 })
61
87
 
62
- const { view, centerZoom, resetView } = useZoomPinch({
88
+ const { view, zoomIn, zoomOut, resetView } = useZoomPinch({
63
89
  containerRef,
64
90
  viewState,
65
91
  onViewStateChange: setViewState,
@@ -67,9 +93,9 @@ function ControlledCanvas() {
67
93
 
68
94
  return (
69
95
  <div>
70
- <button onClick={() => centerZoom(view.zoom * 1.5)}>Zoom In</button>
71
- <button onClick={() => centerZoom(view.zoom / 1.5)}>Zoom Out</button>
72
- <button onClick={resetView}>Reset</button>
96
+ <button onClick={() => zoomIn(1.5, { animate: true })}>Zoom In</button>
97
+ <button onClick={() => zoomOut(1.5, { animate: true })}>Zoom Out</button>
98
+ <button onClick={() => resetView({ animate: true })}>Reset</button>
73
99
 
74
100
  <div
75
101
  ref={containerRef}
@@ -89,43 +115,439 @@ function ControlledCanvas() {
89
115
  }
90
116
  ```
91
117
 
118
+ ## Animated Transitions
119
+
120
+ All programmatic methods accept an optional `AnimationOptions` parameter:
121
+
122
+ ```tsx
123
+ const {
124
+ setView,
125
+ centerZoom,
126
+ resetView,
127
+ zoomIn,
128
+ zoomOut,
129
+ zoomToElement,
130
+ panTo,
131
+ panBy,
132
+ zoomTo,
133
+ fitToRect,
134
+ } = useZoomPinch({
135
+ containerRef,
136
+ })
137
+
138
+ // Instant (default, backward-compatible)
139
+ setView({ x: 100, y: 200, zoom: 2 })
140
+
141
+ // Animated
142
+ setView({ x: 100, y: 200, zoom: 2 }, { animate: true, duration: 300 })
143
+
144
+ // With custom easing
145
+ import { easeInOut } from "use-zoom-pinch"
146
+ resetView({ animate: true, duration: 500, easing: easeInOut })
147
+
148
+ // Zoom to a specific element
149
+ const elRef = useRef<HTMLDivElement>(null)
150
+ zoomToElement(elRef.current!, 2, { animate: true })
151
+ ```
152
+
153
+ ### Built-in Easings
154
+
155
+ - `linear` — constant speed
156
+ - `easeOut` — fast start, slow end (default)
157
+ - `easeInOut` — slow start, fast middle, slow end
158
+
159
+ ## Navigation
160
+
161
+ ```tsx
162
+ const { panTo, panBy, zoomTo, fitToRect } = useZoomPinch({ containerRef })
163
+
164
+ // Center on a point in content space
165
+ panTo(500, 300, { animate: true })
166
+
167
+ // Shift viewport by 100px right, 50px down
168
+ panBy(100, 50)
169
+
170
+ // Zoom to 3x centered on a content-space point
171
+ zoomTo(3, { x: 500, y: 300 }, { animate: true })
172
+
173
+ // Fit a region into view with padding
174
+ fitToRect({ x: 100, y: 100, width: 400, height: 300 }, { animate: true, padding: 20 })
175
+ ```
176
+
177
+ ## Rotation
178
+
179
+ Two-finger rotation is detected during pinch gestures. Disabled by default — enable via `gestures`:
180
+
181
+ ```tsx
182
+ const { view, rotateTo, rotateBy } = useZoomPinch({
183
+ containerRef,
184
+ gestures: { rotate: true },
185
+ })
186
+
187
+ // Apply with CSS:
188
+ // transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom}) rotate(${view.rotation ?? 0}deg)`
189
+
190
+ // Programmatic rotation
191
+ rotateTo(45, { animate: true })
192
+ rotateBy(90, { animate: true })
193
+ ```
194
+
195
+ ## Gesture Toggles
196
+
197
+ Enable or disable individual gesture types:
198
+
199
+ ```tsx
200
+ // Only zoom, no pan or rotate
201
+ useZoomPinch({ containerRef, gestures: { pan: false, zoom: true, rotate: false } })
202
+
203
+ // Only rotate (e.g. image rotation tool)
204
+ useZoomPinch({ containerRef, gestures: { pan: false, zoom: false, rotate: true } })
205
+
206
+ // All gestures (pan + zoom + rotate)
207
+ useZoomPinch({ containerRef, gestures: { rotate: true } })
208
+ ```
209
+
210
+ Imperative methods (`setView`, `panTo`, `rotateTo`, etc.) always work regardless of gesture toggles.
211
+
212
+ ## Double-Tap Zoom
213
+
214
+ Double-tap (or double-click) zooms to the tap point. Enabled by default in `toggle` mode.
215
+
216
+ ```tsx
217
+ // Default: toggle between 1x and 2x zoom
218
+ useZoomPinch({ containerRef })
219
+
220
+ // Customize behavior
221
+ useZoomPinch({
222
+ containerRef,
223
+ doubleTap: { mode: "zoomIn", step: 3 }, // always zoom in by 3x
224
+ })
225
+
226
+ // Disable double-tap
227
+ useZoomPinch({ containerRef, doubleTap: false })
228
+ ```
229
+
230
+ ### Modes
231
+
232
+ - `"toggle"` (default) — zoom in if at 1x, reset if zoomed in
233
+ - `"zoomIn"` — always zoom in by `step`
234
+ - `"reset"` — always reset to `{ x: 0, y: 0, zoom: 1 }`
235
+
236
+ ## Inertia
237
+
238
+ Pan with momentum after releasing the pointer. Enabled by default.
239
+
240
+ ```tsx
241
+ // Default: friction 0.92
242
+ useZoomPinch({ containerRef })
243
+
244
+ // Custom friction (lower = more friction, faster stop)
245
+ useZoomPinch({ containerRef, inertia: { friction: 0.85 } })
246
+
247
+ // Disable inertia
248
+ useZoomPinch({ containerRef, inertia: false })
249
+ ```
250
+
251
+ ## Event Filtering
252
+
253
+ Exclude interactive elements (buttons, inputs) from gesture handling:
254
+
255
+ ```tsx
256
+ useZoomPinch({
257
+ containerRef,
258
+ shouldHandleEvent: (e) => !(e.target as HTMLElement).closest(".no-pan"),
259
+ })
260
+ ```
261
+
262
+ ## Granular Events
263
+
264
+ ```tsx
265
+ useZoomPinch({
266
+ containerRef,
267
+ onPanStart: (view) => console.log("Pan started", view),
268
+ onPanEnd: (view) => console.log("Pan ended", view),
269
+ onZoomStart: (view) => console.log("Zoom started", view),
270
+ onZoomEnd: (view) => console.log("Zoom ended", view),
271
+ onPinchStart: (view) => console.log("Pinch started", view),
272
+ onPinchEnd: (view) => console.log("Pinch ended", view),
273
+ onRotateStart: (view) => console.log("Rotate started", view),
274
+ onRotateEnd: (view) => console.log("Rotate ended", view),
275
+ })
276
+ ```
277
+
278
+ ## Bounds
279
+
280
+ Constrain panning within rectangular limits:
281
+
282
+ ```tsx
283
+ useZoomPinch({
284
+ containerRef,
285
+ bounds: { minX: -500, maxX: 500, minY: -300, maxY: 300 },
286
+ })
287
+ ```
288
+
289
+ ## Keyboard Navigation
290
+
291
+ Enable keyboard controls with arrow keys, zoom (+/-), reset (0), and rotation ([ / ]):
292
+
293
+ ```tsx
294
+ // Enable with defaults (panStep: 50, zoomStep: 1.5, rotateStep: 15)
295
+ useZoomPinch({ containerRef, keyboard: true })
296
+
297
+ // Custom steps
298
+ useZoomPinch({
299
+ containerRef,
300
+ keyboard: { enabled: true, panStep: 100, zoomStep: 2 },
301
+ })
302
+ ```
303
+
304
+ ## Coordinate Conversion
305
+
306
+ Convert between screen and content coordinates:
307
+
308
+ ```tsx
309
+ const { screenToContent, contentToScreen } = useZoomPinch({ containerRef })
310
+
311
+ // Map a click to content space
312
+ const handleClick = (e: MouseEvent) => {
313
+ const pos = screenToContent(e.clientX, e.clientY)
314
+ console.log("Clicked at content position:", pos)
315
+ }
316
+
317
+ // Position a tooltip over content
318
+ const screenPos = contentToScreen(itemX, itemY)
319
+ ```
320
+
321
+ ## Zoom Snap Levels
322
+
323
+ Snap zoom to predefined levels on gesture end:
324
+
325
+ ```tsx
326
+ useZoomPinch({
327
+ containerRef,
328
+ zoomSnapLevels: [0.5, 1, 2, 4],
329
+ })
330
+ ```
331
+
332
+ ## Snap to Grid
333
+
334
+ Snap pan position to a grid:
335
+
336
+ ```tsx
337
+ // Snap on gesture end (with animation)
338
+ useZoomPinch({ containerRef, snapToGrid: { size: 50 } })
339
+
340
+ // Snap continuously during gestures
341
+ useZoomPinch({ containerRef, snapToGrid: { size: 50, mode: "always" } })
342
+ ```
343
+
344
+ ## Fit to Content
345
+
346
+ Auto-fit content to container on mount and resize:
347
+
348
+ ```tsx
349
+ const { fitToContent } = useZoomPinch({
350
+ containerRef,
351
+ contentRect: { width: 1920, height: 1080 }, // auto-fits on mount + resize
352
+ })
353
+
354
+ // Manual fit with padding
355
+ fitToContent({ animate: true, padding: 20 })
356
+ ```
357
+
358
+ ## Pan Button
359
+
360
+ Configure which mouse button triggers panning:
361
+
362
+ ```tsx
363
+ // Use middle mouse button for pan (frees left click for selection)
364
+ useZoomPinch({ containerRef, panButton: 1 })
365
+ ```
366
+
92
367
  ## API
93
368
 
94
369
  ### `useZoomPinch(options)`
95
370
 
96
371
  #### Options
97
372
 
98
- | Option | Type | Default | Description |
99
- | ------------------- | -------------------------------- | ------------------------- | ----------------------------------- |
100
- | `containerRef` | `RefObject<HTMLElement \| null>` | _required_ | Ref to the container element |
101
- | `minScale` | `number` | `0.1` | Minimum zoom level |
102
- | `maxScale` | `number` | `50` | Maximum zoom level |
103
- | `panSpeed` | `number` | `1` | Pan speed multiplier (mouse wheel) |
104
- | `zoomSpeed` | `number` | `1` | Zoom speed multiplier (mouse wheel) |
105
- | `initialViewState` | `ViewState` | `{ x: 0, y: 0, zoom: 1 }` | Initial view for uncontrolled mode |
106
- | `viewState` | `ViewState` | — | Controlled view state |
107
- | `onViewStateChange` | `(view: ViewState) => void` | — | Callback on view change |
108
- | `enabled` | `boolean` | `true` | Enable/disable gesture handling |
373
+ | Option | Type | Default | Description |
374
+ | ------------------- | -------------------------------- | ------------------------------------------ | ------------------------------------------------ |
375
+ | `containerRef` | `RefObject<HTMLElement \| null>` | _required_ | Ref to the container element |
376
+ | `minScale` | `number` | `0.1` | Minimum zoom level |
377
+ | `maxScale` | `number` | `50` | Maximum zoom level |
378
+ | `panSpeed` | `number` | `1` | Pan speed multiplier (mouse wheel) |
379
+ | `zoomSpeed` | `number` | `1` | Zoom speed multiplier (mouse wheel) |
380
+ | `initialViewState` | `ViewState` | `{ x: 0, y: 0, zoom: 1 }` | Initial view for uncontrolled mode |
381
+ | `viewState` | `ViewState` | — | Controlled view state |
382
+ | `onViewStateChange` | `(view: ViewState) => void` | — | Callback on view change |
383
+ | `enabled` | `boolean` | `true` | Enable/disable gesture handling |
384
+ | `gestures` | `GesturesOptions` | `{ pan: true, zoom: true, rotate: false }` | Toggle individual gesture types |
385
+ | `panButton` | `0 \| 1 \| 2` | `0` | Mouse button for pan (0=left, 1=middle, 2=right) |
386
+ | `bounds` | `BoundsOptions` | — | Constrain panning within bounds |
387
+ | `keyboard` | `KeyboardOptions \| boolean` | `false` | Keyboard navigation (pass `true` for defaults) |
388
+ | `zoomSnapLevels` | `number[]` | — | Snap zoom to nearest level on gesture end |
389
+ | `snapToGrid` | `SnapToGridOptions \| false` | `false` | Snap position to grid |
390
+ | `contentRect` | `{ width; height }` | — | Content size for `fitToContent` + auto-fit |
391
+ | `rotation` | `RotationOptions` | — | Min/max angle and rotation snap levels |
392
+ | `wheelMode` | `"pan" \| "zoom"` | `"pan"` | Default wheel behavior |
393
+ | `cursor` | `CursorOptions \| false` | enabled | Auto cursor (grab/grabbing). `false` to disable |
394
+ | `axis` | `"x" \| "y"` | — | Restrict gestures to a single axis |
395
+ | `activationKeys` | `ActivationKeyOptions` | — | Require key held for specific gestures |
396
+ | `shouldHandleEvent` | `(event) => boolean` | — | Filter which events are handled |
397
+ | `doubleTap` | `DoubleTapOptions \| false` | `{ mode: "toggle", step: 2 }` | Double-tap zoom config, or `false` to disable |
398
+ | `inertia` | `InertiaOptions \| false` | `{ friction: 0.92 }` | Pan inertia config, or `false` to disable |
399
+ | `onPanStart` | `(view: ViewState) => void` | — | Fired when drag starts |
400
+ | `onPanEnd` | `(view: ViewState) => void` | — | Fired when drag ends |
401
+ | `onZoomStart` | `(view: ViewState) => void` | — | Fired when wheel zoom starts |
402
+ | `onZoomEnd` | `(view: ViewState) => void` | — | Fired when wheel zoom ends (150ms debounce) |
403
+ | `onPinchStart` | `(view: ViewState) => void` | — | Fired when pinch starts |
404
+ | `onPinchEnd` | `(view: ViewState) => void` | — | Fired when pinch ends |
405
+ | `onRotateStart` | `(view: ViewState) => void` | — | Fired when rotation starts |
406
+ | `onRotateEnd` | `(view: ViewState) => void` | — | Fired when rotation ends |
407
+ | `onTransformEnd` | `(view: ViewState) => void` | — | Fired after any gesture ends |
109
408
 
110
409
  #### Returns
111
410
 
112
- | Property | Type | Description |
113
- | ------------ | --------------------------- | ------------------------------------ |
114
- | `view` | `ViewState` | Current view state |
115
- | `setView` | `(view: ViewState) => void` | Imperatively set the view |
116
- | `centerZoom` | `(zoom: number) => void` | Zoom to level, centered in container |
117
- | `resetView` | `() => void` | Reset to `{ x: 0, y: 0, zoom: 1 }` |
411
+ | Property | Type | Description |
412
+ | ----------------- | ---------------------------------- | ---------------------------------------------- |
413
+ | `view` | `ViewState` | Current view state |
414
+ | `isAnimating` | `boolean` | Whether an animation is running |
415
+ | `setView` | `(view, options?) => void` | Imperatively set the view |
416
+ | `centerZoom` | `(zoom, options?) => void` | Zoom to level, centered in container |
417
+ | `resetView` | `(options?) => void` | Reset to `{ x: 0, y: 0, zoom: 1 }` |
418
+ | `zoomIn` | `(step?, options?) => void` | Zoom in by step (default 1.5x) |
419
+ | `zoomOut` | `(step?, options?) => void` | Zoom out by step (default 1.5x) |
420
+ | `zoomToElement` | `(el, scale?, options?) => void` | Zoom and center on an element |
421
+ | `panTo` | `(x, y, options?) => void` | Center viewport on content-space point |
422
+ | `panBy` | `(dx, dy, options?) => void` | Shift viewport by relative offset |
423
+ | `zoomTo` | `(zoom, point?, options?) => void` | Zoom to level, optionally anchored |
424
+ | `fitToRect` | `(rect, options?) => void` | Fit a content-space rectangle into view |
425
+ | `rotateTo` | `(angle, options?) => void` | Set rotation to absolute angle (degrees) |
426
+ | `rotateBy` | `(delta, options?) => void` | Rotate by relative delta (degrees) |
427
+ | `screenToContent` | `(screenX, screenY) => { x, y }` | Convert screen to content coordinates |
428
+ | `contentToScreen` | `(contentX, contentY) => { x, y }` | Convert content to screen coordinates |
429
+ | `fitToContent` | `(options?) => void` | Fit content to container (needs `contentRect`) |
430
+ | `snapZoom` | `(options?) => void` | Snap zoom to nearest `zoomSnapLevels` |
118
431
 
119
- ### `ViewState`
432
+ ### Types
120
433
 
121
434
  ```ts
122
435
  interface ViewState {
123
436
  x: number // horizontal offset in pixels
124
437
  y: number // vertical offset in pixels
125
438
  zoom: number // scale factor (1 = 100%)
439
+ rotation?: number // angle in degrees (default 0)
440
+ }
441
+
442
+ interface GesturesOptions {
443
+ pan?: boolean // default true
444
+ zoom?: boolean // default true
445
+ rotate?: boolean // default false
446
+ }
447
+
448
+ interface AnimationOptions {
449
+ animate?: boolean // default false
450
+ duration?: number // default 300ms
451
+ easing?: EasingFunction // default easeOut
452
+ }
453
+
454
+ type EasingFunction = (t: number) => number
455
+
456
+ interface DoubleTapOptions {
457
+ enabled?: boolean // default true
458
+ mode?: "zoomIn" | "reset" | "toggle" // default "toggle"
459
+ step?: number // default 2
460
+ }
461
+
462
+ interface InertiaOptions {
463
+ enabled?: boolean // default true
464
+ friction?: number // 0–1, default 0.92
465
+ }
466
+
467
+ interface BoundsOptions {
468
+ minX?: number
469
+ maxX?: number
470
+ minY?: number
471
+ maxY?: number
472
+ mode?: "clamp" | "bounce" // default "clamp"
473
+ }
474
+
475
+ interface KeyboardOptions {
476
+ enabled?: boolean // default false
477
+ panStep?: number // default 50
478
+ zoomStep?: number // default 1.5
479
+ rotateStep?: number // default 15
480
+ }
481
+
482
+ interface SnapToGridOptions {
483
+ size: number
484
+ mode?: "end" | "always" // default "end"
485
+ }
486
+
487
+ type ZoomSnapLevel = number
488
+
489
+ interface RotationOptions {
490
+ minAngle?: number
491
+ maxAngle?: number
492
+ snapLevels?: number[] // e.g. [0, 90, 180, 270]
126
493
  }
494
+
495
+ interface CursorOptions {
496
+ enabled?: boolean // default true
497
+ idle?: string // default "grab"
498
+ dragging?: string // default "grabbing"
499
+ zooming?: string // default "zoom-in"
500
+ }
501
+
502
+ interface ActivationKeyOptions {
503
+ pan?: string // e.g. "Shift"
504
+ zoom?: string // e.g. "Alt"
505
+ rotate?: string // e.g. "Control"
506
+ }
507
+ ```
508
+
509
+ ## Container Setup
510
+
511
+ The container element **must** have `touchAction: "none"` to prevent the browser from intercepting touch gestures (scroll, pinch-zoom) before the hook can handle them:
512
+
513
+ ```tsx
514
+ <div ref={containerRef} style={{ touchAction: "none", overflow: "hidden" }}>
127
515
  ```
128
516
 
517
+ Without `touchAction: "none"`, pinch-to-zoom and touch drag will trigger native browser behavior instead of your custom handling. The `overflow: "hidden"` prevents content from leaking outside the viewport.
518
+
519
+ ## Browser Compatibility
520
+
521
+ The hook uses standard Web APIs available in all modern browsers:
522
+
523
+ | Feature | Chrome | Firefox | Safari | Edge | iOS Safari | Android Chrome |
524
+ | --------------------- | ------ | ------- | ------ | ---- | ---------- | -------------- |
525
+ | Pointer Events | 55+ | 59+ | 13+ | 12+ | 13+ | 55+ |
526
+ | Wheel Events | 31+ | 17+ | 7+ | 12+ | 7+ | 31+ |
527
+ | Touch Events | 22+ | 52+ | 10+ | 12+ | 10+ | 22+ |
528
+ | ResizeObserver | 64+ | 69+ | 13.1+ | 79+ | 13.4+ | 64+ |
529
+ | requestAnimationFrame | 10+ | 23+ | 6+ | 12+ | 6+ | 10+ |
530
+
531
+ **Minimum:** Chrome/Edge 64+, Firefox 69+, Safari 13.1+, iOS 13.4+.
532
+
533
+ > Safari Gesture Events (`GestureEvent`) are used when available for native trackpad pinch-zoom on macOS.
534
+
535
+ ## Comparison
536
+
537
+ | Feature | useZoomPinch | react-zoom-pan-pinch | @use-gesture/react | motion/react |
538
+ | --------------------- | ------------ | -------------------- | ------------------ | ------------------- |
539
+ | Size (min+gzip) | **~5.2 KB** | ~13.2 KB | ~8.9 KB | ~41.6 KB |
540
+ | Approach | Hook | Components + hook | Gesture primitives | Animation + gesture |
541
+ | Controlled mode | ✅ Native | ❌ | ❌ | ❌ |
542
+ | DOM wrappers | ✅ None | +2 divs | ✅ None | +1 div |
543
+ | Bounds / constraints | ✅ | ✅ | 🔧 Manual | 🔧 dragConstraints |
544
+ | Keyboard navigation | ✅ | ❌ | ❌ | ❌ |
545
+ | Coordinate conversion | ✅ | ❌ | ❌ | ❌ |
546
+ | Snap to grid | ✅ | ❌ | ❌ | ❌ |
547
+ | Zoom snap levels | ✅ | ❌ | ❌ | ❌ |
548
+ | Rotation gestures | ✅ | ❌ | 🔧 Manual | ❌ |
549
+ | Ready-to-use zoom/pan | ✅ | ✅ | 🔧 Manual | 🔧 Manual |
550
+
129
551
  ## License
130
552
 
131
553
  MIT