Playground

Interactive Bars

January 2026

"use client";

import {cn} from "@/lib/cn";
import {motion, useMotionValue, useSpring, useTransform} from "motion/react";
import {useRef} from "react";

const COLUMNS = 43;

const BASE_VALUE = 1;
const INTENSITY = 2;
const MAX_DISTANCE = 6;

const calculateScale = (distance: number): number => {
    if (distance >= MAX_DISTANCE) return BASE_VALUE;

    const normalizedDistance = distance / MAX_DISTANCE;
    const factor = Math.pow(1 - normalizedDistance, 2);
    return BASE_VALUE + INTENSITY * factor;
};

const AnimatedBar = ({
    index,
    mouseX,
    barWidth,
    gap,
}: {
    index: number;
    mouseX: ReturnType<typeof useMotionValue<number>>;
    barWidth: number;
    gap: number;
}) => {
    const scaleY = useTransform(mouseX, (x) => {
        if (x < 0) return BASE_VALUE;

        const barCenter = index * (barWidth + gap) + barWidth / 2;
        const distance = Math.abs(x - barCenter) / (barWidth + gap);

        return calculateScale(distance);
    });

    const smoothScaleY = useSpring(scaleY, {
        stiffness: 200,
        damping: 20,
    });

    return (
        <motion.div
            className={cn(
                "h-6 w-px origin-bottom bg-white/50",
                index % 6 === 0 && "h-10 bg-amber-700"
            )}
            style={{
                scaleY: smoothScaleY,
            }}
        />
    );
};

const InteractiveBars = () => {
    const containerRef = useRef<HTMLDivElement>(null);
    const mouseX = useMotionValue(-1);

    const handlePointerMove = (e: React.PointerEvent) => {
        if (!containerRef.current) return;

        const rect = containerRef.current.getBoundingClientRect();
        const x = e.clientX - rect.left;

        mouseX.set(x);
    };

    const resetMouse = () => {
        mouseX.set(-1);
    };

    return (
        <article className="grid min-h-96 place-items-center bg-white/5 sm:aspect-video sm:min-h-0">
            <div
                className="flex items-end gap-x-2"
                ref={containerRef}
                onPointerMove={handlePointerMove}
                onPointerOut={resetMouse}
                onPointerCancel={resetMouse}>
                {Array.from({length: COLUMNS}, (_, index) => (
                    <AnimatedBar
                        key={index}
                        index={index}
                        mouseX={mouseX}
                        barWidth={1}
                        gap={8}
                    />
                ))}
            </div>
        </article>
    );
};

export default InteractiveBars;
Next

Interactive 3D Book