{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "Base UI slider with a spring-driven thumb — direct 1:1 tracking while dragging, a spring settle on release or track-press, and a thumb-anchored value label that fades away cleanly. Supports single-value and two-thumb range use out of the box.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/ui/slider.tsx",
      "content": "\"use client\";\n\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\n\nexport type SliderValue = number | readonly number[];\n\nfunction defaultFormatValue(value: number) {\n  return Number.isInteger(value) ? String(value) : value.toFixed(1);\n}\n\nfunction percentOf(value: number, min: number, max: number) {\n  return max === min ? 0 : ((value - min) / (max - min)) * 100;\n}\n\ninterface SliderProps extends Omit<SliderPrimitive.Root.Props, \"children\" | \"orientation\"> {\n  className?: string;\n  trackClassName?: string;\n  thumbClassName?: string;\n  label?: ReactNode;\n  thumbLabels?: string[];\n  formatValue?: (value: number) => string;\n}\n\nfunction Slider({\n  className,\n  trackClassName,\n  thumbClassName,\n  label,\n  thumbLabels,\n  formatValue = defaultFormatValue,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onValueCommitted,\n  min = 0,\n  max = 100,\n  step = 1,\n  disabled = false,\n  ...props\n}: SliderProps) {\n  const [uncontrolledValue, setUncontrolledValue] = useState<SliderValue>(\n    () => defaultValue ?? min,\n  );\n  const value = valueProp ?? uncontrolledValue;\n  const values: readonly number[] = Array.isArray(value) ? value : [value as number];\n  const [dragging, setDragging] = useState(false);\n  const [activeIndex, setActiveIndex] = useState(-1);\n\n  const motion0 = useMotionValue(percentOf(values[0] ?? min, min, max));\n  const motion1 = useMotionValue(percentOf(values[1] ?? values[0] ?? min, min, max));\n  const motionValues = [motion0, motion1] as const;\n  const hasMountedRef = useRef(false);\n  const reduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    hasMountedRef.current = true;\n  }, []);\n\n  useEffect(() => {\n    const controls = values.map((rawValue, index) => {\n      const mv = motionValues[index];\n      if (!mv) return undefined;\n      const percent = percentOf(rawValue, min, max);\n      if (!hasMountedRef.current || dragging || reduceMotion) {\n        mv.set(percent);\n        return undefined;\n      }\n      return animate(mv, percent, spring.moderate.enter);\n    });\n    return () => controls.forEach((control) => control?.stop());\n    // motionValues are stable refs; re-run only when the actual values/range change.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [values.join(\",\"), min, max, dragging, reduceMotion]);\n\n  const leftPercent0 = useTransform(motion0, (position) => `${position}%`);\n  const leftPercent1 = useTransform(motion1, (position) => `${position}%`);\n  const leftPercents = [leftPercent0, leftPercent1] as const;\n  const rangeWidthPercent = useTransform([motion0, motion1], (positions) => {\n    const [first, second] = positions as number[];\n    return `${Math.max(0, second - first)}%`;\n  });\n  const isRange = values.length > 1;\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      value={value}\n      min={min}\n      max={max}\n      step={step}\n      disabled={disabled}\n      onValueChange={(next, eventDetails) => {\n        if (valueProp === undefined) setUncontrolledValue(next);\n        setActiveIndex(eventDetails.activeThumbIndex);\n        if (eventDetails.reason === \"drag\") setDragging(true);\n        onValueChange?.(next, eventDetails);\n      }}\n      onValueCommitted={(next, eventDetails) => {\n        setDragging(false);\n        onValueCommitted?.(next, eventDetails);\n      }}\n      className={cn(\n        \"flex w-full flex-col gap-2 data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {label ? (\n        <div className=\"flex items-center justify-between gap-2\">\n          <SliderPrimitive.Label data-slot=\"slider-label\" className=\"text-control text-foreground\">\n            {label}\n          </SliderPrimitive.Label>\n          <SliderPrimitive.Value\n            data-slot=\"slider-value\"\n            className=\"text-caption tabular-nums text-muted-foreground\"\n          >\n            {(_formatted, rawValues) =>\n              rawValues.map((rawValue) => formatValue(rawValue)).join(\" – \")\n            }\n          </SliderPrimitive.Value>\n        </div>\n      ) : null}\n      <SliderPrimitive.Control\n        data-slot=\"slider-control\"\n        className=\"relative flex w-full touch-none items-center py-2.5 select-none\"\n      >\n        <SliderPrimitive.Track\n          data-slot=\"slider-track\"\n          className={cn(\"h-1.5 w-full rounded-full bg-input\", trackClassName)}\n        >\n          <motion.div\n            data-slot=\"slider-indicator\"\n            aria-hidden\n            className=\"absolute inset-y-0 rounded-full bg-primary\"\n            style={\n              isRange\n                ? { left: leftPercent0, width: rangeWidthPercent }\n                : { left: 0, width: leftPercent0 }\n            }\n          />\n          {values.map((rawValue, index) => (\n            <SliderThumb\n              key={index}\n              index={index}\n              value={rawValue}\n              leftPercent={leftPercents[index] ?? leftPercent0}\n              isActiveDrag={dragging && activeIndex === index}\n              disabled={disabled}\n              reduceMotion={reduceMotion}\n              formatValue={formatValue}\n              ariaLabel={thumbLabels?.[index]}\n              className={thumbClassName}\n            />\n          ))}\n        </SliderPrimitive.Track>\n      </SliderPrimitive.Control>\n    </SliderPrimitive.Root>\n  );\n}\n\nconst HOVER_DELAY = 100;\n\nfunction SliderThumb({\n  index,\n  value,\n  leftPercent,\n  isActiveDrag,\n  disabled,\n  reduceMotion,\n  formatValue,\n  ariaLabel,\n  className,\n}: {\n  index: number;\n  value: number;\n  leftPercent: MotionValue<string>;\n  isActiveDrag: boolean;\n  disabled?: boolean;\n  reduceMotion: boolean | null;\n  formatValue: (value: number) => string;\n  ariaLabel?: string;\n  className?: string;\n}) {\n  const [hovered, setHovered] = useState(false);\n  const [hoverOpen, setHoverOpen] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    if (hovered) hoverTimeoutRef.current = setTimeout(() => setHoverOpen(true), HOVER_DELAY);\n    else setHoverOpen(false);\n    return () => {\n      if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);\n    };\n  }, [hovered]);\n\n  const tooltipOpen = !disabled && (hoverOpen || focused || isActiveDrag);\n  const tooltipOffset = reduceMotion ? 0 : 4;\n\n  return (\n    <SliderPrimitive.Thumb\n      data-slot=\"slider-thumb\"\n      index={index}\n      aria-label={ariaLabel}\n      onFocus={() => setFocused(true)}\n      onBlur={() => setFocused(false)}\n      onPointerEnter={(event) => {\n        if (event.pointerType === \"mouse\") setHovered(true);\n      }}\n      onPointerLeave={() => setHovered(false)}\n      render={(thumbProps) => {\n        const { children: hiddenInput, ...restThumbProps } = thumbProps as {\n          children?: ReactNode;\n        } & Record<string, unknown>;\n        return (\n          <motion.div\n            {...restThumbProps}\n            className={cn(\n              \"absolute top-1/2 z-10 size-4 -translate-x-1/2 -translate-y-1/2 cursor-grab rounded-full border border-border bg-background shadow-bevel outline-none data-disabled:pointer-events-none data-dragging:cursor-grabbing has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50\",\n              className,\n            )}\n            style={{ left: leftPercent, top: \"50%\" }}\n            whileHover={disabled ? undefined : { scale: 1.08 }}\n            transition={spring.fast.enter}\n          >\n            {hiddenInput}\n            <AnimatePresence>\n              {tooltipOpen ? (\n                <motion.span\n                  className=\"pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-1.5 py-0.5 text-micro text-background\"\n                  initial={{ opacity: 0, y: tooltipOffset }}\n                  animate={{ opacity: 1, y: 0, transition: spring.fast.enter }}\n                  exit={{ opacity: 0, transition: spring.fast.exit }}\n                >\n                  {formatValue(value)}\n                </motion.span>\n              ) : null}\n            </AnimatePresence>\n          </motion.div>\n        );\n      }}\n    />\n  );\n}\n\nexport { Slider };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}