uix-image-magnify 0.1.0 → 0.1.2

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/dist/index.cjs ADDED
@@ -0,0 +1,409 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/ImageMagnify.tsx
7
+ var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
8
+ function useImageMagnify(options = {}) {
9
+ const {
10
+ src,
11
+ zoomSrc,
12
+ zoomScale = 2,
13
+ zoomType = "hover",
14
+ lensSize = 150,
15
+ zoomPreload = true,
16
+ onZoomStart,
17
+ onZoomEnd
18
+ } = options;
19
+ const [isZoomed, setIsZoomed] = react.useState(false);
20
+ const isZoomedRef = react.useRef(false);
21
+ const containerRef = react.useRef(null);
22
+ const imageRef = react.useRef(null);
23
+ const lensRef = react.useRef(null);
24
+ const paneRef = react.useRef(null);
25
+ const cursorRef = react.useRef(null);
26
+ const pointerRef = react.useRef({ x: 0, y: 0 });
27
+ const rafRef = react.useRef(0);
28
+ const lastTouchRef = react.useRef(0);
29
+ const bgImage = zoomSrc || src;
30
+ const isRecentTouch = () => Date.now() - lastTouchRef.current < 700;
31
+ const setZoomed = react.useCallback(
32
+ (z) => {
33
+ if (isZoomedRef.current === z) return;
34
+ isZoomedRef.current = z;
35
+ setIsZoomed(z);
36
+ if (z) onZoomStart?.();
37
+ else onZoomEnd?.();
38
+ },
39
+ [onZoomStart, onZoomEnd]
40
+ );
41
+ const apply = react.useCallback(() => {
42
+ const container = containerRef.current;
43
+ if (!container) return;
44
+ const rect = container.getBoundingClientRect();
45
+ if (!rect.width || !rect.height) return;
46
+ const { x, y } = pointerRef.current;
47
+ const xPct = clamp(x / rect.width, 0, 1);
48
+ const yPct = clamp(y / rect.height, 0, 1);
49
+ const lens = lensRef.current;
50
+ if (lens) {
51
+ const maxX = Math.max(0, rect.width - lensSize);
52
+ const maxY = Math.max(0, rect.height - lensSize);
53
+ const lx = clamp(x - lensSize / 2, 0, maxX);
54
+ const ly = clamp(y - lensSize / 2, 0, maxY);
55
+ const cx = lx + lensSize / 2;
56
+ const cy = ly + lensSize / 2;
57
+ lens.style.transform = `translate3d(${lx}px, ${ly}px, 0)`;
58
+ lens.style.backgroundPosition = `${lensSize / 2 - cx * zoomScale}px ${lensSize / 2 - cy * zoomScale}px`;
59
+ }
60
+ const pane = paneRef.current;
61
+ if (pane) {
62
+ const pw = pane.offsetWidth;
63
+ const ph = pane.offsetHeight;
64
+ const bgW = rect.width * zoomScale;
65
+ const bgH = rect.height * zoomScale;
66
+ let bgX = pw / 2 - xPct * bgW;
67
+ let bgY = ph / 2 - yPct * bgH;
68
+ if (bgW > pw) bgX = clamp(bgX, pw - bgW, 0);
69
+ else bgX = (pw - bgW) / 2;
70
+ if (bgH > ph) bgY = clamp(bgY, ph - bgH, 0);
71
+ else bgY = (ph - bgH) / 2;
72
+ pane.style.backgroundPosition = `${bgX}px ${bgY}px`;
73
+ }
74
+ const cursor = cursorRef.current;
75
+ if (cursor) {
76
+ cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`;
77
+ }
78
+ }, [zoomScale, lensSize]);
79
+ const schedule = react.useCallback(() => {
80
+ if (rafRef.current) return;
81
+ rafRef.current = requestAnimationFrame(() => {
82
+ rafRef.current = 0;
83
+ apply();
84
+ });
85
+ }, [apply]);
86
+ react.useEffect(() => {
87
+ if (isZoomed) schedule();
88
+ }, [isZoomed, schedule]);
89
+ react.useEffect(() => {
90
+ if (!bgImage) return;
91
+ const url = `url("${bgImage}")`;
92
+ if (lensRef.current) lensRef.current.style.backgroundImage = url;
93
+ if (paneRef.current) paneRef.current.style.backgroundImage = url;
94
+ }, [bgImage, isZoomed]);
95
+ react.useEffect(() => {
96
+ const container = containerRef.current;
97
+ if (!container) return;
98
+ const update = () => {
99
+ const rect = container.getBoundingClientRect();
100
+ if (!rect.width || !rect.height) return;
101
+ const size = `${rect.width * zoomScale}px ${rect.height * zoomScale}px`;
102
+ if (lensRef.current) lensRef.current.style.backgroundSize = size;
103
+ if (paneRef.current) paneRef.current.style.backgroundSize = size;
104
+ };
105
+ update();
106
+ const ro = new ResizeObserver(update);
107
+ ro.observe(container);
108
+ return () => ro.disconnect();
109
+ }, [zoomScale, isZoomed]);
110
+ react.useEffect(() => {
111
+ if (!zoomPreload || !bgImage) return;
112
+ const img = new Image();
113
+ img.src = bgImage;
114
+ }, [zoomPreload, bgImage]);
115
+ react.useEffect(
116
+ () => () => {
117
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
118
+ },
119
+ []
120
+ );
121
+ const updatePointer = (clientX, clientY) => {
122
+ const container = containerRef.current;
123
+ if (!container) return;
124
+ const rect = container.getBoundingClientRect();
125
+ if (!rect.width || !rect.height) return;
126
+ pointerRef.current = {
127
+ x: clamp(clientX - rect.left, 0, rect.width),
128
+ y: clamp(clientY - rect.top, 0, rect.height)
129
+ };
130
+ };
131
+ const handlers = {
132
+ onMouseEnter: (e) => {
133
+ if (isRecentTouch()) return;
134
+ updatePointer(e.clientX, e.clientY);
135
+ if (zoomType === "hover") setZoomed(true);
136
+ schedule();
137
+ },
138
+ onMouseMove: (e) => {
139
+ if (isRecentTouch()) return;
140
+ updatePointer(e.clientX, e.clientY);
141
+ if (isZoomedRef.current) schedule();
142
+ },
143
+ onMouseLeave: () => {
144
+ if (isRecentTouch()) return;
145
+ if (zoomType === "hover") setZoomed(false);
146
+ },
147
+ onClick: (e) => {
148
+ if (isRecentTouch()) return;
149
+ if (zoomType !== "click") return;
150
+ updatePointer(e.clientX, e.clientY);
151
+ setZoomed(!isZoomedRef.current);
152
+ schedule();
153
+ },
154
+ onDoubleClick: (e) => {
155
+ if (isRecentTouch()) return;
156
+ if (zoomType !== "double-click") return;
157
+ updatePointer(e.clientX, e.clientY);
158
+ setZoomed(!isZoomedRef.current);
159
+ schedule();
160
+ },
161
+ onTouchStart: (e) => {
162
+ lastTouchRef.current = Date.now();
163
+ const t = e.touches[0];
164
+ if (!t) return;
165
+ updatePointer(t.clientX, t.clientY);
166
+ if (zoomType === "hover") setZoomed(true);
167
+ else setZoomed(!isZoomedRef.current);
168
+ schedule();
169
+ },
170
+ onTouchMove: (e) => {
171
+ lastTouchRef.current = Date.now();
172
+ const t = e.touches[0];
173
+ if (!t) return;
174
+ updatePointer(t.clientX, t.clientY);
175
+ schedule();
176
+ },
177
+ onTouchEnd: () => {
178
+ lastTouchRef.current = Date.now();
179
+ if (zoomType === "hover") setZoomed(false);
180
+ },
181
+ onKeyDown: (e) => {
182
+ const container = containerRef.current;
183
+ if (!container) return;
184
+ const rect = container.getBoundingClientRect();
185
+ const step = 20;
186
+ const { x, y } = pointerRef.current;
187
+ let handled = true;
188
+ switch (e.key) {
189
+ case "ArrowLeft":
190
+ pointerRef.current.x = clamp(x - step, 0, rect.width);
191
+ break;
192
+ case "ArrowRight":
193
+ pointerRef.current.x = clamp(x + step, 0, rect.width);
194
+ break;
195
+ case "ArrowUp":
196
+ pointerRef.current.y = clamp(y - step, 0, rect.height);
197
+ break;
198
+ case "ArrowDown":
199
+ pointerRef.current.y = clamp(y + step, 0, rect.height);
200
+ break;
201
+ case "Enter":
202
+ case " ":
203
+ setZoomed(!isZoomedRef.current);
204
+ break;
205
+ case "Escape":
206
+ setZoomed(false);
207
+ break;
208
+ default:
209
+ handled = false;
210
+ }
211
+ if (handled) {
212
+ e.preventDefault();
213
+ schedule();
214
+ }
215
+ },
216
+ onFocus: () => {
217
+ if (isRecentTouch()) return;
218
+ if (zoomType === "hover") setZoomed(true);
219
+ },
220
+ onBlur: () => {
221
+ if (zoomType === "hover") setZoomed(false);
222
+ }
223
+ };
224
+ return {
225
+ isZoomed,
226
+ containerRef,
227
+ imageRef,
228
+ lensRef,
229
+ paneRef,
230
+ cursorRef,
231
+ handlers
232
+ };
233
+ }
234
+ var ImageMagnify = react.forwardRef(
235
+ (props, ref) => {
236
+ const {
237
+ src,
238
+ alt,
239
+ width,
240
+ mode = "side-by-side",
241
+ className,
242
+ style,
243
+ imageClassName,
244
+ imageStyle,
245
+ lensClassName,
246
+ lensStyle,
247
+ paneClassName,
248
+ paneStyle,
249
+ paneWidth = 400,
250
+ paneHeight = 400,
251
+ lazy,
252
+ panePosition = "right",
253
+ lensSize = 150,
254
+ lensShape = "circle",
255
+ zoomType = "hover",
256
+ ...hookOptions
257
+ } = props;
258
+ const {
259
+ isZoomed,
260
+ containerRef,
261
+ imageRef,
262
+ lensRef,
263
+ paneRef,
264
+ cursorRef,
265
+ handlers
266
+ } = useImageMagnify({ src, zoomType, lensSize, ...hookOptions });
267
+ const bgImage = props.zoomSrc || src;
268
+ const paneOffset = {};
269
+ if (panePosition === "right") {
270
+ paneOffset.left = "calc(100% + 12px)";
271
+ paneOffset.top = 0;
272
+ } else if (panePosition === "left") {
273
+ paneOffset.right = "calc(100% + 12px)";
274
+ paneOffset.top = 0;
275
+ } else if (panePosition === "top") {
276
+ paneOffset.left = 0;
277
+ paneOffset.bottom = "calc(100% + 12px)";
278
+ } else {
279
+ paneOffset.left = 0;
280
+ paneOffset.top = "calc(100% + 12px)";
281
+ }
282
+ return /* @__PURE__ */ jsxRuntime.jsxs(
283
+ "div",
284
+ {
285
+ ref,
286
+ className,
287
+ style: {
288
+ position: "relative",
289
+ display: "inline-block",
290
+ lineHeight: 0,
291
+ width,
292
+ ...style
293
+ },
294
+ children: [
295
+ /* @__PURE__ */ jsxRuntime.jsxs(
296
+ "div",
297
+ {
298
+ ref: containerRef,
299
+ tabIndex: 0,
300
+ role: "img",
301
+ "aria-label": alt,
302
+ style: {
303
+ position: "relative",
304
+ overflow: "hidden",
305
+ width: "100%",
306
+ cursor: zoomType === "hover" ? "crosshair" : isZoomed ? "zoom-out" : "zoom-in",
307
+ touchAction: "none",
308
+ outline: "none",
309
+ lineHeight: 0
310
+ },
311
+ ...handlers,
312
+ children: [
313
+ /* @__PURE__ */ jsxRuntime.jsx(
314
+ "img",
315
+ {
316
+ ref: imageRef,
317
+ src,
318
+ alt,
319
+ loading: lazy ? "lazy" : void 0,
320
+ draggable: false,
321
+ className: imageClassName,
322
+ style: {
323
+ display: "block",
324
+ width: "100%",
325
+ height: "auto",
326
+ userSelect: "none",
327
+ pointerEvents: "none",
328
+ ...imageStyle
329
+ }
330
+ }
331
+ ),
332
+ mode === "lens" && isZoomed && /* @__PURE__ */ jsxRuntime.jsx(
333
+ "div",
334
+ {
335
+ ref: lensRef,
336
+ className: lensClassName,
337
+ style: {
338
+ position: "absolute",
339
+ top: 0,
340
+ left: 0,
341
+ width: lensSize,
342
+ height: lensSize,
343
+ borderRadius: lensShape === "circle" ? "50%" : 0,
344
+ backgroundImage: bgImage ? `url("${bgImage}")` : void 0,
345
+ backgroundRepeat: "no-repeat",
346
+ pointerEvents: "none",
347
+ boxShadow: "0 4px 12px rgba(0,0,0,0.25)",
348
+ border: "1px solid rgba(255,255,255,0.6)",
349
+ willChange: "transform",
350
+ ...lensStyle
351
+ }
352
+ }
353
+ ),
354
+ mode === "side-by-side" && isZoomed && /* @__PURE__ */ jsxRuntime.jsx(
355
+ "div",
356
+ {
357
+ ref: cursorRef,
358
+ style: {
359
+ position: "absolute",
360
+ top: 0,
361
+ left: 0,
362
+ width: 12,
363
+ height: 12,
364
+ marginLeft: -6,
365
+ marginTop: -6,
366
+ borderRadius: "50%",
367
+ background: "rgba(255,255,255,0.7)",
368
+ border: "1px solid rgba(0,0,0,0.5)",
369
+ pointerEvents: "none",
370
+ boxShadow: "0 0 0 1px rgba(255,255,255,0.8)",
371
+ willChange: "transform"
372
+ }
373
+ }
374
+ )
375
+ ]
376
+ }
377
+ ),
378
+ mode === "side-by-side" && isZoomed && /* @__PURE__ */ jsxRuntime.jsx(
379
+ "div",
380
+ {
381
+ ref: paneRef,
382
+ className: paneClassName,
383
+ "aria-hidden": "true",
384
+ style: {
385
+ position: "absolute",
386
+ width: paneWidth,
387
+ height: paneHeight,
388
+ backgroundImage: bgImage ? `url("${bgImage}")` : void 0,
389
+ backgroundRepeat: "no-repeat",
390
+ border: "1px solid #e5e5e5",
391
+ borderRadius: 4,
392
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
393
+ pointerEvents: "none",
394
+ ...paneOffset,
395
+ ...paneStyle
396
+ }
397
+ }
398
+ )
399
+ ]
400
+ }
401
+ );
402
+ }
403
+ );
404
+ ImageMagnify.displayName = "ImageMagnify";
405
+
406
+ exports.ImageMagnify = ImageMagnify;
407
+ exports.useImageMagnify = useImageMagnify;
408
+ //# sourceMappingURL=index.cjs.map
409
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useImageMagnify.ts","../src/components/ImageMagnify.tsx"],"names":["useState","useRef","useCallback","useEffect","forwardRef","jsxs","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport type * as React from \"react\";\r\n\r\nexport type ZoomType = \"hover\" | \"click\" | \"double-click\";\r\nexport type PanePosition = \"left\" | \"right\" | \"top\" | \"bottom\";\r\nexport type LensShape = \"circle\" | \"square\";\r\n\r\nexport interface UseImageMagnifyOptions {\r\n /** Base image URL (required when used standalone). */\r\n src?: string;\r\n /** High-resolution image used for the zoomed view. Defaults to `src`. */\r\n zoomSrc?: string;\r\n /** Magnification factor (2 = 200%). */\r\n zoomScale?: number;\r\n /** Interaction that activates zoom. */\r\n zoomType?: ZoomType;\r\n /** Where the side-by-side pane sits relative to the image. */\r\n panePosition?: PanePosition;\r\n /** Diameter / side length of the lens in px. */\r\n lensSize?: number;\r\n /** Lens shape. */\r\n lensShape?: LensShape;\r\n /** Preload the high-res image to avoid a flash. */\r\n zoomPreload?: boolean;\r\n onZoomStart?: () => void;\r\n onZoomEnd?: () => void;\r\n}\r\n\r\nconst clamp = (v: number, min: number, max: number) =>\r\n Math.max(min, Math.min(max, v));\r\n\r\nexport function useImageMagnify(options: UseImageMagnifyOptions = {}) {\r\n const {\r\n src,\r\n zoomSrc,\r\n zoomScale = 2,\r\n zoomType = \"hover\",\r\n lensSize = 150,\r\n zoomPreload = true,\r\n onZoomStart,\r\n onZoomEnd,\r\n } = options;\r\n\r\n const [isZoomed, setIsZoomed] = useState(false);\r\n const isZoomedRef = useRef(false);\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const imageRef = useRef<HTMLImageElement>(null);\r\n const lensRef = useRef<HTMLDivElement>(null);\r\n const paneRef = useRef<HTMLDivElement>(null);\r\n const cursorRef = useRef<HTMLDivElement>(null);\r\n const pointerRef = useRef({ x: 0, y: 0 });\r\n const rafRef = useRef(0);\r\n const lastTouchRef = useRef(0);\r\n\r\n const bgImage = zoomSrc || src;\r\n\r\n const isRecentTouch = () => Date.now() - lastTouchRef.current < 700;\r\n\r\n const setZoomed = useCallback(\r\n (z: boolean) => {\r\n if (isZoomedRef.current === z) return;\r\n isZoomedRef.current = z;\r\n setIsZoomed(z);\r\n if (z) onZoomStart?.();\r\n else onZoomEnd?.();\r\n },\r\n [onZoomStart, onZoomEnd],\r\n );\r\n\r\n /** Writes directly to DOM — no React re-renders on mousemove. */\r\n const apply = useCallback(() => {\r\n const container = containerRef.current;\r\n if (!container) return;\r\n const rect = container.getBoundingClientRect();\r\n if (!rect.width || !rect.height) return;\r\n\r\n const { x, y } = pointerRef.current;\r\n const xPct = clamp(x / rect.width, 0, 1);\r\n const yPct = clamp(y / rect.height, 0, 1);\r\n\r\n const lens = lensRef.current;\r\n if (lens) {\r\n // Keep the lens fully inside the container so it never gets clipped\r\n // (and never shows empty background near the edges).\r\n const maxX = Math.max(0, rect.width - lensSize);\r\n const maxY = Math.max(0, rect.height - lensSize);\r\n const lx = clamp(x - lensSize / 2, 0, maxX);\r\n const ly = clamp(y - lensSize / 2, 0, maxY);\r\n const cx = lx + lensSize / 2;\r\n const cy = ly + lensSize / 2;\r\n lens.style.transform = `translate3d(${lx}px, ${ly}px, 0)`;\r\n lens.style.backgroundPosition = `${\r\n lensSize / 2 - cx * zoomScale\r\n }px ${lensSize / 2 - cy * zoomScale}px`;\r\n }\r\n\r\n const pane = paneRef.current;\r\n if (pane) {\r\n const pw = pane.offsetWidth;\r\n const ph = pane.offsetHeight;\r\n const bgW = rect.width * zoomScale;\r\n const bgH = rect.height * zoomScale;\r\n let bgX = pw / 2 - xPct * bgW;\r\n let bgY = ph / 2 - yPct * bgH;\r\n if (bgW > pw) bgX = clamp(bgX, pw - bgW, 0);\r\n else bgX = (pw - bgW) / 2;\r\n if (bgH > ph) bgY = clamp(bgY, ph - bgH, 0);\r\n else bgY = (ph - bgH) / 2;\r\n pane.style.backgroundPosition = `${bgX}px ${bgY}px`;\r\n }\r\n\r\n const cursor = cursorRef.current;\r\n if (cursor) {\r\n cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`;\r\n }\r\n }, [zoomScale, lensSize]);\r\n\r\n /** Coalesce DOM writes into a single rAF. */\r\n const schedule = useCallback(() => {\r\n if (rafRef.current) return;\r\n rafRef.current = requestAnimationFrame(() => {\r\n rafRef.current = 0;\r\n apply();\r\n });\r\n }, [apply]);\r\n\r\n // Position after the lens/pane actually mount.\r\n useEffect(() => {\r\n if (isZoomed) schedule();\r\n }, [isZoomed, schedule]);\r\n\r\n // Background image.\r\n useEffect(() => {\r\n if (!bgImage) return;\r\n const url = `url(\"${bgImage}\")`;\r\n if (lensRef.current) lensRef.current.style.backgroundImage = url;\r\n if (paneRef.current) paneRef.current.style.backgroundImage = url;\r\n }, [bgImage, isZoomed]);\r\n\r\n // Background size tracks the container.\r\n useEffect(() => {\r\n const container = containerRef.current;\r\n if (!container) return;\r\n const update = () => {\r\n const rect = container.getBoundingClientRect();\r\n if (!rect.width || !rect.height) return;\r\n const size = `${rect.width * zoomScale}px ${rect.height * zoomScale}px`;\r\n if (lensRef.current) lensRef.current.style.backgroundSize = size;\r\n if (paneRef.current) paneRef.current.style.backgroundSize = size;\r\n };\r\n update();\r\n const ro = new ResizeObserver(update);\r\n ro.observe(container);\r\n return () => ro.disconnect();\r\n }, [zoomScale, isZoomed]);\r\n\r\n // Preload the high-res image.\r\n useEffect(() => {\r\n if (!zoomPreload || !bgImage) return;\r\n const img = new Image();\r\n img.src = bgImage;\r\n }, [zoomPreload, bgImage]);\r\n\r\n // Cleanup rAF on unmount.\r\n useEffect(\r\n () => () => {\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n },\r\n [],\r\n );\r\n\r\n const updatePointer = (clientX: number, clientY: number) => {\r\n const container = containerRef.current;\r\n if (!container) return;\r\n const rect = container.getBoundingClientRect();\r\n if (!rect.width || !rect.height) return;\r\n pointerRef.current = {\r\n x: clamp(clientX - rect.left, 0, rect.width),\r\n y: clamp(clientY - rect.top, 0, rect.height),\r\n };\r\n };\r\n\r\n const handlers = {\r\n onMouseEnter: (e: React.MouseEvent) => {\r\n if (isRecentTouch()) return;\r\n updatePointer(e.clientX, e.clientY);\r\n if (zoomType === \"hover\") setZoomed(true);\r\n schedule();\r\n },\r\n onMouseMove: (e: React.MouseEvent) => {\r\n if (isRecentTouch()) return;\r\n updatePointer(e.clientX, e.clientY);\r\n if (isZoomedRef.current) schedule();\r\n },\r\n onMouseLeave: () => {\r\n if (isRecentTouch()) return;\r\n if (zoomType === \"hover\") setZoomed(false);\r\n },\r\n onClick: (e: React.MouseEvent) => {\r\n if (isRecentTouch()) return;\r\n if (zoomType !== \"click\") return;\r\n updatePointer(e.clientX, e.clientY);\r\n setZoomed(!isZoomedRef.current);\r\n schedule();\r\n },\r\n onDoubleClick: (e: React.MouseEvent) => {\r\n if (isRecentTouch()) return;\r\n if (zoomType !== \"double-click\") return;\r\n updatePointer(e.clientX, e.clientY);\r\n setZoomed(!isZoomedRef.current);\r\n schedule();\r\n },\r\n onTouchStart: (e: React.TouchEvent) => {\r\n lastTouchRef.current = Date.now();\r\n const t = e.touches[0];\r\n if (!t) return;\r\n updatePointer(t.clientX, t.clientY);\r\n if (zoomType === \"hover\") setZoomed(true);\r\n else setZoomed(!isZoomedRef.current);\r\n schedule();\r\n },\r\n onTouchMove: (e: React.TouchEvent) => {\r\n lastTouchRef.current = Date.now();\r\n const t = e.touches[0];\r\n if (!t) return;\r\n updatePointer(t.clientX, t.clientY);\r\n schedule();\r\n },\r\n onTouchEnd: () => {\r\n lastTouchRef.current = Date.now();\r\n if (zoomType === \"hover\") setZoomed(false);\r\n },\r\n onKeyDown: (e: React.KeyboardEvent) => {\r\n const container = containerRef.current;\r\n if (!container) return;\r\n const rect = container.getBoundingClientRect();\r\n const step = 20;\r\n const { x, y } = pointerRef.current;\r\n let handled = true;\r\n\r\n switch (e.key) {\r\n case \"ArrowLeft\":\r\n pointerRef.current.x = clamp(x - step, 0, rect.width);\r\n break;\r\n case \"ArrowRight\":\r\n pointerRef.current.x = clamp(x + step, 0, rect.width);\r\n break;\r\n case \"ArrowUp\":\r\n pointerRef.current.y = clamp(y - step, 0, rect.height);\r\n break;\r\n case \"ArrowDown\":\r\n pointerRef.current.y = clamp(y + step, 0, rect.height);\r\n break;\r\n case \"Enter\":\r\n case \" \":\r\n setZoomed(!isZoomedRef.current);\r\n break;\r\n case \"Escape\":\r\n setZoomed(false);\r\n break;\r\n default:\r\n handled = false;\r\n }\r\n if (handled) {\r\n e.preventDefault();\r\n schedule();\r\n }\r\n },\r\n onFocus: () => {\r\n if (isRecentTouch()) return;\r\n if (zoomType === \"hover\") setZoomed(true);\r\n },\r\n onBlur: () => {\r\n if (zoomType === \"hover\") setZoomed(false);\r\n },\r\n };\r\n\r\n return {\r\n isZoomed,\r\n containerRef,\r\n imageRef,\r\n lensRef,\r\n paneRef,\r\n cursorRef,\r\n handlers,\r\n };\r\n}\r\n","import { forwardRef } from \"react\";\r\nimport type { CSSProperties } from \"react\";\r\nimport { useImageMagnify } from \"../hooks/useImageMagnify\";\r\nimport type { UseImageMagnifyOptions } from \"../hooks/useImageMagnify\";\r\n\r\nexport interface ImageMagnifyProps extends UseImageMagnifyOptions {\r\n src: string;\r\n alt: string;\r\n /** Rendered width of the image (height follows aspect ratio). */\r\n width?: number | string;\r\n mode?: \"lens\" | \"side-by-side\";\r\n className?: string;\r\n style?: CSSProperties;\r\n imageClassName?: string;\r\n imageStyle?: CSSProperties;\r\n lensClassName?: string;\r\n lensStyle?: CSSProperties;\r\n paneClassName?: string;\r\n paneStyle?: CSSProperties;\r\n paneWidth?: number;\r\n paneHeight?: number;\r\n lazy?: boolean;\r\n}\r\n\r\nexport const ImageMagnify = forwardRef<HTMLDivElement, ImageMagnifyProps>(\r\n (props, ref) => {\r\n const {\r\n src,\r\n alt,\r\n width,\r\n mode = \"side-by-side\",\r\n className,\r\n style,\r\n imageClassName,\r\n imageStyle,\r\n lensClassName,\r\n lensStyle,\r\n paneClassName,\r\n paneStyle,\r\n paneWidth = 400,\r\n paneHeight = 400,\r\n lazy,\r\n panePosition = \"right\",\r\n lensSize = 150,\r\n lensShape = \"circle\",\r\n zoomType = \"hover\",\r\n ...hookOptions\r\n } = props;\r\n\r\n const {\r\n isZoomed,\r\n containerRef,\r\n imageRef,\r\n lensRef,\r\n paneRef,\r\n cursorRef,\r\n handlers,\r\n } = useImageMagnify({ src, zoomType, lensSize, ...hookOptions });\r\n\r\n const bgImage = props.zoomSrc || src;\r\n\r\n const paneOffset: CSSProperties = {};\r\n if (panePosition === \"right\") {\r\n paneOffset.left = \"calc(100% + 12px)\";\r\n paneOffset.top = 0;\r\n } else if (panePosition === \"left\") {\r\n paneOffset.right = \"calc(100% + 12px)\";\r\n paneOffset.top = 0;\r\n } else if (panePosition === \"top\") {\r\n paneOffset.left = 0;\r\n paneOffset.bottom = \"calc(100% + 12px)\";\r\n } else {\r\n paneOffset.left = 0;\r\n paneOffset.top = \"calc(100% + 12px)\";\r\n }\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n className={className}\r\n style={{\r\n position: \"relative\",\r\n display: \"inline-block\",\r\n lineHeight: 0,\r\n width,\r\n ...style,\r\n }}\r\n >\r\n <div\r\n ref={containerRef}\r\n tabIndex={0}\r\n role=\"img\"\r\n aria-label={alt}\r\n style={{\r\n position: \"relative\",\r\n overflow: \"hidden\",\r\n width: \"100%\",\r\n cursor:\r\n zoomType === \"hover\"\r\n ? \"crosshair\"\r\n : isZoomed\r\n ? \"zoom-out\"\r\n : \"zoom-in\",\r\n touchAction: \"none\",\r\n outline: \"none\",\r\n lineHeight: 0,\r\n }}\r\n {...handlers}\r\n >\r\n <img\r\n ref={imageRef}\r\n src={src}\r\n alt={alt}\r\n loading={lazy ? \"lazy\" : undefined}\r\n draggable={false}\r\n className={imageClassName}\r\n style={{\r\n display: \"block\",\r\n width: \"100%\",\r\n height: \"auto\",\r\n userSelect: \"none\",\r\n pointerEvents: \"none\",\r\n ...imageStyle,\r\n }}\r\n />\r\n\r\n {mode === \"lens\" && isZoomed && (\r\n <div\r\n ref={lensRef}\r\n className={lensClassName}\r\n style={{\r\n position: \"absolute\",\r\n top: 0,\r\n left: 0,\r\n width: lensSize,\r\n height: lensSize,\r\n borderRadius: lensShape === \"circle\" ? \"50%\" : 0,\r\n backgroundImage: bgImage ? `url(\"${bgImage}\")` : undefined,\r\n backgroundRepeat: \"no-repeat\",\r\n pointerEvents: \"none\",\r\n boxShadow: \"0 4px 12px rgba(0,0,0,0.25)\",\r\n border: \"1px solid rgba(255,255,255,0.6)\",\r\n willChange: \"transform\",\r\n ...lensStyle,\r\n }}\r\n />\r\n )}\r\n\r\n {mode === \"side-by-side\" && isZoomed && (\r\n <div\r\n ref={cursorRef}\r\n style={{\r\n position: \"absolute\",\r\n top: 0,\r\n left: 0,\r\n width: 12,\r\n height: 12,\r\n marginLeft: -6,\r\n marginTop: -6,\r\n borderRadius: \"50%\",\r\n background: \"rgba(255,255,255,0.7)\",\r\n border: \"1px solid rgba(0,0,0,0.5)\",\r\n pointerEvents: \"none\",\r\n boxShadow: \"0 0 0 1px rgba(255,255,255,0.8)\",\r\n willChange: \"transform\",\r\n }}\r\n />\r\n )}\r\n </div>\r\n\r\n {mode === \"side-by-side\" && isZoomed && (\r\n <div\r\n ref={paneRef}\r\n className={paneClassName}\r\n aria-hidden=\"true\"\r\n style={{\r\n position: \"absolute\",\r\n width: paneWidth,\r\n height: paneHeight,\r\n backgroundImage: bgImage ? `url(\"${bgImage}\")` : undefined,\r\n backgroundRepeat: \"no-repeat\",\r\n border: \"1px solid #e5e5e5\",\r\n borderRadius: 4,\r\n boxShadow: \"0 8px 24px rgba(0,0,0,0.12)\",\r\n pointerEvents: \"none\",\r\n ...paneOffset,\r\n ...paneStyle,\r\n }}\r\n />\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\n\r\nImageMagnify.displayName = \"ImageMagnify\";\r\n"]}
@@ -0,0 +1,69 @@
1
+ import * as React from 'react';
2
+ import { CSSProperties } from 'react';
3
+
4
+ type ZoomType = "hover" | "click" | "double-click";
5
+ type PanePosition = "left" | "right" | "top" | "bottom";
6
+ type LensShape = "circle" | "square";
7
+ interface UseImageMagnifyOptions {
8
+ /** Base image URL (required when used standalone). */
9
+ src?: string;
10
+ /** High-resolution image used for the zoomed view. Defaults to `src`. */
11
+ zoomSrc?: string;
12
+ /** Magnification factor (2 = 200%). */
13
+ zoomScale?: number;
14
+ /** Interaction that activates zoom. */
15
+ zoomType?: ZoomType;
16
+ /** Where the side-by-side pane sits relative to the image. */
17
+ panePosition?: PanePosition;
18
+ /** Diameter / side length of the lens in px. */
19
+ lensSize?: number;
20
+ /** Lens shape. */
21
+ lensShape?: LensShape;
22
+ /** Preload the high-res image to avoid a flash. */
23
+ zoomPreload?: boolean;
24
+ onZoomStart?: () => void;
25
+ onZoomEnd?: () => void;
26
+ }
27
+ declare function useImageMagnify(options?: UseImageMagnifyOptions): {
28
+ isZoomed: boolean;
29
+ containerRef: React.RefObject<HTMLDivElement>;
30
+ imageRef: React.RefObject<HTMLImageElement>;
31
+ lensRef: React.RefObject<HTMLDivElement>;
32
+ paneRef: React.RefObject<HTMLDivElement>;
33
+ cursorRef: React.RefObject<HTMLDivElement>;
34
+ handlers: {
35
+ onMouseEnter: (e: React.MouseEvent) => void;
36
+ onMouseMove: (e: React.MouseEvent) => void;
37
+ onMouseLeave: () => void;
38
+ onClick: (e: React.MouseEvent) => void;
39
+ onDoubleClick: (e: React.MouseEvent) => void;
40
+ onTouchStart: (e: React.TouchEvent) => void;
41
+ onTouchMove: (e: React.TouchEvent) => void;
42
+ onTouchEnd: () => void;
43
+ onKeyDown: (e: React.KeyboardEvent) => void;
44
+ onFocus: () => void;
45
+ onBlur: () => void;
46
+ };
47
+ };
48
+
49
+ interface ImageMagnifyProps extends UseImageMagnifyOptions {
50
+ src: string;
51
+ alt: string;
52
+ /** Rendered width of the image (height follows aspect ratio). */
53
+ width?: number | string;
54
+ mode?: "lens" | "side-by-side";
55
+ className?: string;
56
+ style?: CSSProperties;
57
+ imageClassName?: string;
58
+ imageStyle?: CSSProperties;
59
+ lensClassName?: string;
60
+ lensStyle?: CSSProperties;
61
+ paneClassName?: string;
62
+ paneStyle?: CSSProperties;
63
+ paneWidth?: number;
64
+ paneHeight?: number;
65
+ lazy?: boolean;
66
+ }
67
+ declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
68
+
69
+ export { ImageMagnify, type ImageMagnifyProps, type LensShape, type PanePosition, type UseImageMagnifyOptions, type ZoomType, useImageMagnify };
@@ -0,0 +1,69 @@
1
+ import * as React from 'react';
2
+ import { CSSProperties } from 'react';
3
+
4
+ type ZoomType = "hover" | "click" | "double-click";
5
+ type PanePosition = "left" | "right" | "top" | "bottom";
6
+ type LensShape = "circle" | "square";
7
+ interface UseImageMagnifyOptions {
8
+ /** Base image URL (required when used standalone). */
9
+ src?: string;
10
+ /** High-resolution image used for the zoomed view. Defaults to `src`. */
11
+ zoomSrc?: string;
12
+ /** Magnification factor (2 = 200%). */
13
+ zoomScale?: number;
14
+ /** Interaction that activates zoom. */
15
+ zoomType?: ZoomType;
16
+ /** Where the side-by-side pane sits relative to the image. */
17
+ panePosition?: PanePosition;
18
+ /** Diameter / side length of the lens in px. */
19
+ lensSize?: number;
20
+ /** Lens shape. */
21
+ lensShape?: LensShape;
22
+ /** Preload the high-res image to avoid a flash. */
23
+ zoomPreload?: boolean;
24
+ onZoomStart?: () => void;
25
+ onZoomEnd?: () => void;
26
+ }
27
+ declare function useImageMagnify(options?: UseImageMagnifyOptions): {
28
+ isZoomed: boolean;
29
+ containerRef: React.RefObject<HTMLDivElement>;
30
+ imageRef: React.RefObject<HTMLImageElement>;
31
+ lensRef: React.RefObject<HTMLDivElement>;
32
+ paneRef: React.RefObject<HTMLDivElement>;
33
+ cursorRef: React.RefObject<HTMLDivElement>;
34
+ handlers: {
35
+ onMouseEnter: (e: React.MouseEvent) => void;
36
+ onMouseMove: (e: React.MouseEvent) => void;
37
+ onMouseLeave: () => void;
38
+ onClick: (e: React.MouseEvent) => void;
39
+ onDoubleClick: (e: React.MouseEvent) => void;
40
+ onTouchStart: (e: React.TouchEvent) => void;
41
+ onTouchMove: (e: React.TouchEvent) => void;
42
+ onTouchEnd: () => void;
43
+ onKeyDown: (e: React.KeyboardEvent) => void;
44
+ onFocus: () => void;
45
+ onBlur: () => void;
46
+ };
47
+ };
48
+
49
+ interface ImageMagnifyProps extends UseImageMagnifyOptions {
50
+ src: string;
51
+ alt: string;
52
+ /** Rendered width of the image (height follows aspect ratio). */
53
+ width?: number | string;
54
+ mode?: "lens" | "side-by-side";
55
+ className?: string;
56
+ style?: CSSProperties;
57
+ imageClassName?: string;
58
+ imageStyle?: CSSProperties;
59
+ lensClassName?: string;
60
+ lensStyle?: CSSProperties;
61
+ paneClassName?: string;
62
+ paneStyle?: CSSProperties;
63
+ paneWidth?: number;
64
+ paneHeight?: number;
65
+ lazy?: boolean;
66
+ }
67
+ declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
68
+
69
+ export { ImageMagnify, type ImageMagnifyProps, type LensShape, type PanePosition, type UseImageMagnifyOptions, type ZoomType, useImageMagnify };