"use client"; import { motion, useMotionTemplate, useMotionValue, useReducedMotion, useSpring, useTransform, } from "motion/react"; import { useEffect } from "react"; import { SPRING_GLIDE } from "@/lib/ease"; import { type SliderOptions, useSlider } from "@/lib/hooks/use-slider"; import { cn } from "@/lib/utils"; // Bouncy grab feedback for the thumb scale only. const SPRING_BOUNCY = { type: "spring", stiffness: 500, damping: 14, mass: 0.7 } as const; export interface RangeSliderProps extends SliderOptions { /** Render a tick dot at each step. */ showTicks?: boolean; className?: string; } export function RangeSlider({ showTicks = true, className, ...options }: RangeSliderProps) { const reduce = useReducedMotion(); const { percent, dragging, min, max, step, trackProps, sliderProps } = useSlider(options); // Spring-smoothed position drives both the thumb and the fill. const target = useMotionValue(percent); useEffect(() => { target.set(percent); }, [percent, target]); const smooth = useSpring(target, SPRING_GLIDE); const pos = reduce ? target : smooth; const left = useMotionTemplate`${pos}%`; // Self-offset the thumb from 0% (flush left) to -100% (flush right) of its // own width so it stays fully inside the track at both ends — no clip, no gap. const thumbX = useTransform(pos, (p) => `${-p}%`); // Floor rather than round, so a range the step does not divide (0 to 10 by 4) // stops its dots at the last whole step instead of drawing one past max. // toFixed comes first because 0.3/0.1 is 2.9999999999999996, which would // floor to 2 and drop the last dot. const steps = Math.floor(Number(((max - min) / step).toFixed(6))); const ticks = showTicks && steps > 0 && steps <= 50 ? Array.from({ length: steps + 1 }, (_, i) => Number((min + i * step).toFixed(6))) : []; return (
{/* fill — runs from the left edge to the thumb, consistent tone */} {/* Ticks, inset by half the thumb's width. That inset is the span the thumb's own centre travels, so a dot sits where the thumb lands. */}
{ticks.map((t) => { const tp = ((t - min) / (max - min)) * 100; return ( ); })}
{/* vertical bar thumb — contained at both ends via thumbX */}
); }