'use client'; import * as React from 'react'; import { Slider as SliderPrimitive } from 'radix-ui'; import { cn } from '@/lib/utils'; import { focusRing } from '@/lib/cva-presets'; export interface RangeSliderMark { /** Position on the scale, in the same units as `min` and `max`. */ value: number; /** Text under the tick. Falls back to the value itself. */ label?: React.ReactNode; } /** Per-slot overrides for the parts a caller cannot reach through `className`. */ export interface RangeSliderClassNames { track?: string; range?: string; thumb?: string; mark?: string; } export interface RangeSliderProps extends Omit, 'asChild'> { /** Ticks drawn under the track. Purely decorative — they are not selectable. */ marks?: RangeSliderMark[]; /** * Accessible name for each thumb, in order. * * A thumb carries `role="slider"`, and that role takes **no** name from * surrounding content — without one, a screen reader announces a bare number. * Pass one name per thumb (`['Minimum', 'Maximum']`). For a single thumb an * `aria-label` on the slider itself is enough; with several, each thumb falls * back to that label suffixed with its position. */ thumbLabels?: string[]; classNames?: RangeSliderClassNames; } /** * Numeric slider for a single value or a range. * * The number of thumbs follows the length of `value`/`defaultValue`: one number * gives a plain slider, two give a range. Values are always arrays, even for a * single thumb. * * ```tsx * * * ``` */ function RangeSlider({ className, classNames, marks, thumbLabels, value, defaultValue, min = 0, max = 100, orientation = 'horizontal', ...props }: RangeSliderProps) { /* Radix derives the thumb count from the value it is given; mirroring that here is what lets one component cover both the single and range cases. */ const values = React.useMemo( () => value ?? defaultValue ?? [min], [value, defaultValue, min] ); const isVertical = orientation === 'vertical'; const rootLabel = props['aria-label']; const labelFor = (index: number) => { if (thumbLabels?.[index]) return thumbLabels[index]; if (!rootLabel) return undefined; return values.length > 1 ? `${rootLabel} ${index + 1}` : rootLabel; }; /** Where a mark sits along the track, as a percentage of the scale. */ const offsetOf = (markValue: number) => { const span = max - min; const ratio = span === 0 ? 0 : (markValue - min) / span; return `${Math.min(Math.max(ratio, 0), 1) * 100}%`; }; return ( {values.map((_, index) => ( ))} {marks?.length ? ( /* Decorative: the thumb already announces the live value, so repeating the scale to a screen reader would only add noise. */
{marks.map((mark) => ( {mark.label ?? mark.value} ))}
) : null}
); } export { RangeSlider };