Pan Zoom
DnD & InteractionPannable and zoomable content container.
Preview
πΊοΈ
Scroll to zoom, drag to pan
Usage
example.jsx
import { PanZoom } from "@/components/ui/pan-zoom";
export default function Example() {
return <PanZoom />;
}Source Code
Copy this file into components/ui/pan-zoom.jsx in your project.
pan-zoom.jsx
"use client";
import { forwardRef, useRef, useState, useCallback } from "react";
import { cn } from "@/lib/utils";
const PanZoom = forwardRef(({ className, children, ...props }, ref) => {
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
const containerRef = useRef(null);
const dragging = useRef(false);
const start = useRef({ x: 0, y: 0, tx: 0, ty: 0 });
const onWheel = useCallback((e) => {
e.preventDefault();
setTransform((t) => ({
...t,
scale: Math.min(3, Math.max(0.2, t.scale + (e.deltaY > 0 ? -0.1 : 0.1))),
}));
}, []);
const onMouseDown = useCallback((e) => {
dragging.current = true;
start.current = { x: e.clientX, y: e.clientY, tx: transform.x, ty: transform.y };
}, [transform]);
const onMouseMove = useCallback((e) => {
if (!dragging.current) return;
setTransform((t) => ({
...t,
x: start.current.tx + e.clientX - start.current.x,
y: start.current.ty + e.clientY - start.current.y,
}));
}, []);
const onMouseUp = useCallback(() => { dragging.current = false; }, []);
return (
<div ref={(el) => { containerRef.current = el; if (typeof ref === "function") ref(el); else if (ref) ref.current = el; }}
className={cn("overflow-hidden rounded-md border cursor-grab active:cursor-grabbing", className)}
onWheel={onWheel} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} onMouseLeave={onMouseUp}
{...props}
>
<div style={{ transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`, transformOrigin: "center" }} className="transition-transform duration-75">
{children}
</div>
</div>
);
});
PanZoom.displayName = "PanZoom";
export { PanZoom };
Quick Install
Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.
npm install clsx tailwind-merge