"use client"; import { useRef } from "react"; import { motion, useMotionTemplate, useMotionValue, useReducedMotion, useSpring } from "motion/react"; type HoverZoomProps = { src: string; alt: string; /** How much the picture enlarges. */ zoom?: number; /** Set the size here. */ className?: string; }; const FOLLOW = { stiffness: 300, damping: 40 }; const GROW = { stiffness: 220, damping: 28 }; /* HoverZoom: the picture enlarges inside its frame and pans with the pointer. Tap to zoom on a phone; arrow keys pan from the keyboard. */ export function HoverZoom({ src, alt, zoom = 2, className = "aspect-video" }: HoverZoomProps) { const reduce = useReducedMotion(); const start = useRef({ x: 0, y: 0 }); const x = useMotionValue(50); const y = useMotionValue(50); const level = useMotionValue(1); const smoothX = useSpring(x, FOLLOW); const smoothY = useSpring(y, FOLLOW); const smoothLevel = useSpring(level, GROW); const transformOrigin = useMotionTemplate`${reduce ? x : smoothX}% ${reduce ? y : smoothY}%`; const aim = (e: React.PointerEvent) => { const r = e.currentTarget.getBoundingClientRect(); x.set(((e.clientX - r.left) / r.width) * 100); y.set(((e.clientY - r.top) / r.height) * 100); }; const enter = (e: React.PointerEvent) => { if (e.pointerType !== "mouse") return; aim(e); smoothX.jump(x.get()); smoothY.jump(y.get()); level.set(zoom); }; const down = (e: React.PointerEvent) => { start.current = { x: e.clientX, y: e.clientY }; if (e.pointerType !== "mouse" && level.get() === 1) aim(e); }; const move = (e: React.PointerEvent) => { if (e.pointerType === "mouse" || level.get() > 1) aim(e); }; const up = (e: React.PointerEvent) => { const still = Math.hypot(e.clientX - start.current.x, e.clientY - start.current.y) < 10; if (e.pointerType !== "mouse" && still) level.set(level.get() > 1 ? 1 : zoom); }; const leave = (e: React.PointerEvent) => { if (e.pointerType === "mouse") level.set(1); }; const onFocus = (e: React.FocusEvent) => { if (!e.currentTarget.matches(":focus-visible")) return; x.set(50); y.set(50); level.set(zoom); }; const onKey = (e: React.KeyboardEvent) => { const step: Record = { ArrowLeft: [-10, 0], ArrowRight: [10, 0], ArrowUp: [0, -10], ArrowDown: [0, 10] }; if (e.key === "Escape") level.set(1); if (!(e.key in step)) return; e.preventDefault(); level.set(zoom); x.set(Math.max(0, Math.min(100, x.get() + step[e.key][0]))); y.set(Math.max(0, Math.min(100, y.get() + step[e.key][1]))); }; return (
level.set(1)} onKeyDown={onKey} className={`relative cursor-zoom-in touch-pan-y select-none overflow-hidden outline-none focus-visible:ring-2 focus-visible:ring-white/70 ${className}`} >
); }