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;