"use client"; import { useRef, useState } from "react"; import { motion, useMotionTemplate, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react"; type FocusLensProps = { src: string; alt: string; /** How much the lens enlarges. */ zoom?: number; /** Lens radius in pixels. */ radius?: number; /** Blur on the rest of the picture while the lens is out, in pixels. */ blur?: number; /** Set the size here. */ className?: string; }; const FOLLOW = { stiffness: 600, damping: 45 }; const POP = { stiffness: 320, damping: 26 }; /* FocusLens: the picture is sharp at rest. With the mouse, a finger or the arrow keys, the rest goes soft and a round lens shows that spot enlarged. */ export function FocusLens({ src, alt, zoom = 1.8, radius = 80, blur = 4, className = "aspect-video" }: FocusLensProps) { const reduce = useReducedMotion(); const boxRef = useRef(null); const x = useMotionValue(0); const y = useMotionValue(0); const smoothX = useSpring(x, FOLLOW); const smoothY = useSpring(y, FOLLOW); const lensX = reduce ? x : smoothX; const lensY = reduce ? y : smoothY; const size = useMotionValue(0); const smoothSize = useSpring(size, POP); const lensSize = reduce ? size : smoothSize; const clipSize = useTransform(lensSize, (v) => Math.max(0, v) / zoom); const ringSize = useTransform(lensSize, (v) => Math.max(0, v) * 2); const clipPath = useMotionTemplate`circle(${clipSize}px at ${lensX}px ${lensY}px)`; const transformOrigin = useMotionTemplate`${lensX}px ${lensY}px`; const [on, setOn] = useState(false); const show = (px: number, py: number) => { x.jump(px); y.jump(py); smoothX.jump(px); smoothY.jump(py); size.set(radius); setOn(true); }; const hide = () => { size.set(0); setOn(false); }; const enter = (e: React.PointerEvent) => { const r = e.currentTarget.getBoundingClientRect(); show(e.clientX - r.left, e.clientY - r.top); }; const move = (e: React.PointerEvent) => { const r = e.currentTarget.getBoundingClientRect(); x.set(e.clientX - r.left); y.set(e.clientY - r.top); }; const onFocus = (e: React.FocusEvent) => { if (!e.currentTarget.matches(":focus-visible")) return; show(e.currentTarget.offsetWidth / 2, e.currentTarget.offsetHeight / 2); }; const onKey = (e: React.KeyboardEvent) => { const step: Record = { ArrowLeft: [-20, 0], ArrowRight: [20, 0], ArrowUp: [0, -20], ArrowDown: [0, 20] }; const box = boxRef.current; if (e.key === "Escape") hide(); if (!(e.key in step) || !box) return; e.preventDefault(); if (!on) show(box.offsetWidth / 2, box.offsetHeight / 2); x.set(Math.max(0, Math.min(box.offsetWidth, x.get() + step[e.key][0]))); y.set(Math.max(0, Math.min(box.offsetHeight, y.get() + step[e.key][1]))); }; return (
{/* eslint-disable @next/next/no-img-element */} {alt} {/* eslint-enable @next/next/no-img-element */}
); }