Draggable Slider

Preview

50

Code

"use client";

import {
  motion,
  useMotionValue,
  useTransform,
  useSpring,
  type PanInfo,
} from "framer-motion";
import { useRef, useState, type FC, useEffect } from "react";

interface DraggableSliderProps {
  minValue?: number;
  maxValue?: number;
  initialValue?: number;
  onValueChange?: (value: number) => void;
  className?: string;
  handleClassName?: string;
  trackClassName?: string;
  trailClassName?: string;
}

const DraggableSlider: FC<DraggableSliderProps> = ({
  minValue = 0,
  maxValue = 100,
  initialValue = 50,
  onValueChange,
  className,
  handleClassName,
  trackClassName,
  trailClassName,
}) => {
  const trackRef = useRef<HTMLDivElement>(null);
  const [value, setValue] = useState(initialValue);

  const x = useMotionValue(0);
  const springX = useSpring(x, { stiffness: 300, damping: 30 });

  const trailWidth = useTransform(springX, (val) => `${val}px`);

  const handleDrag = (event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
    if (!trackRef.current) return;
    const trackWidth = trackRef.current.offsetWidth;
    const newX = Math.max(0, Math.min(trackWidth, x.get() + info.offset.x));
    x.set(newX);

    const newValue = (newX / trackWidth) * (maxValue - minValue) + minValue;
    setValue(Math.round(newValue));
    if (onValueChange) {
      onValueChange(Math.round(newValue));
    }
  };

  useEffect(() => {
    if (trackRef.current) {
      const trackWidth = trackRef.current.offsetWidth;
      const newX = ((initialValue - minValue) / (maxValue - minValue)) * trackWidth;
      x.set(newX);
    }
  }, [initialValue, minValue, maxValue, x]);

  return (
    <div className={`flex flex-col items-center w-full max-w-xs ${className}`}>
      <div
        ref={trackRef}
        className={`relative w-full h-2 rounded-full cursor-pointer bg-neutral-700 ${trackClassName}`}
      >
        <motion.div
          className={`absolute top-0 left-0 h-full rounded-full bg-purple-500 ${trailClassName}`}
          style={{ width: trailWidth }}
        />
        <motion.div
          drag="x"
          dragConstraints={trackRef}
          dragElastic={0}
          dragMomentum={false}
          onDrag={handleDrag}
          style={{ x: springX }}
          className={`absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-6 h-6 rounded-full bg-white shadow-lg cursor-grab active:cursor-grabbing ${handleClassName}`}
        />
      </div>
      <p className="mt-4 text-lg font-semibold tabular-nums text-white">
        {value}
      </p>
    </div>
  );
};

export default DraggableSlider;