"use client" import * as React from "react" import { Slider } from "../ui/slider" import { cn } from "../../lib/utils" export interface FilterRangeControlProps { min: number max: number step?: number unit?: string values: string[] onValuesChange: (min: number, max: number) => void className?: string } function parseRangeValues(values: string[], min: number, max: number): [number, number] { const lo = values[0] !== undefined && values[0] !== "" ? Number(values[0]) : min const hi = values[1] !== undefined && values[1] !== "" ? Number(values[1]) : max const safeLo = Number.isFinite(lo) ? Math.max(min, Math.min(max, lo)) : min const safeHi = Number.isFinite(hi) ? Math.max(min, Math.min(max, hi)) : max return safeLo <= safeHi ? [safeLo, safeHi] : [safeHi, safeLo] } function normalizeRange(a: number, b: number, min: number, max: number): [number, number] { const clampedA = Math.max(min, Math.min(max, a)) const clampedB = Math.max(min, Math.min(max, b)) return clampedA <= clampedB ? [clampedA, clampedB] : [clampedB, clampedA] } /** Dual-thumb range filter — live draft while dragging; commits on release or keyboard. */ export function FilterRangeControl({ min, max, step = 1, unit = "", values, onValuesChange, className, }: FilterRangeControlProps) { const committed = parseRangeValues(values, min, max) // Scalars so the sync logic can depend on the bounds themselves — `committed` // is a fresh tuple every render. const [committedLo, committedHi] = committed const [draft, setDraft] = React.useState<[number, number]>(committed) const draftRef = React.useRef(draft) React.useEffect(() => { draftRef.current = draft }) const draggingRef = React.useRef(false) const hasCommittedRef = React.useRef(false) // Sync draft to committed values when they change from the outside — but only // when the user isn't mid-drag. This uses the render-time adjustment pattern // (track previous committed in state) instead of an effect, avoiding the // extra render cycle that effects cause. const [syncedLo, setSyncedLo] = React.useState(committedLo) const [syncedHi, setSyncedHi] = React.useState(committedHi) if (!draggingRef.current && (syncedLo !== committedLo || syncedHi !== committedHi)) { setSyncedLo(committedLo) setSyncedHi(committedHi) setDraft([committedLo, committedHi]) } const commit = React.useCallback( (a: number, b: number) => { const [lo, hi] = normalizeRange(a, b, min, max) setDraft([lo, hi]) onValuesChange(lo, hi) hasCommittedRef.current = true }, [max, min, onValuesChange], ) const [lo, hi] = draft const suffix = unit ? unit : "" return (
Drag either handle to set the allowed range{suffix ? ` (${min}${suffix}–${max}${suffix})` : ""}.