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 +1 -1
- package/README.md +446 -24
- package/dist/index.cjs +1 -189
- package/dist/index.d.cts +198 -6
- package/dist/index.d.ts +198 -6
- package/dist/index.js +1 -187
- package/package.json +36 -10
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/LICENSE
CHANGED
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
|
+
[](https://www.npmjs.com/package/use-zoom-pinch)
|
|
6
|
+
[](https://github.com/NemeZZiZZ/use-zoom-pinch/actions/workflows/ci.yml)
|
|
7
|
+
[](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,
|
|
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={() =>
|
|
71
|
-
<button onClick={() =>
|
|
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
|
|
99
|
-
| ------------------- | -------------------------------- |
|
|
100
|
-
| `containerRef` | `RefObject<HTMLElement \| null>` | _required_
|
|
101
|
-
| `minScale` | `number` | `0.1`
|
|
102
|
-
| `maxScale` | `number` | `50`
|
|
103
|
-
| `panSpeed` | `number` | `1`
|
|
104
|
-
| `zoomSpeed` | `number` | `1`
|
|
105
|
-
| `initialViewState` | `ViewState` | `{ x: 0, y: 0, zoom: 1 }`
|
|
106
|
-
| `viewState` | `ViewState` | —
|
|
107
|
-
| `onViewStateChange` | `(view: ViewState) => void` | —
|
|
108
|
-
| `enabled` | `boolean` | `true`
|
|
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
|
|
113
|
-
|
|
|
114
|
-
| `view`
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
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
|
-
###
|
|
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
|