Pan Zoom

DnD & Interaction

Pannable 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