{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "A jelly slider with spring physics and bouncy animations",
  "dependencies": [],
  "registryDependencies": [
    "@boldkit/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/slider.tsx",
      "content": "import * as React from 'react'\nimport { cn } from '@/lib/utils'\n\nexport interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {\n  value?: number[]\n  defaultValue?: number[]\n  min?: number\n  max?: number\n  step?: number\n  onValueChange?: (value: number[]) => void\n  onValueCommit?: (value: number[]) => void\n  disabled?: boolean\n  orientation?: 'horizontal' | 'vertical'\n  // Jelly physics settings\n  stiffness?: number\n  damping?: number\n  mass?: number\n}\n\ninterface SpringState {\n  position: number\n  velocity: number\n}\n\nconst Slider = React.forwardRef<HTMLDivElement, SliderProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = [0],\n      min = 0,\n      max = 100,\n      step = 1,\n      onValueChange,\n      onValueCommit,\n      disabled = false,\n      orientation = 'horizontal',\n      stiffness = 400,\n      damping = 28,\n      mass = 1,\n      className,\n      'aria-label': ariaLabel,\n      'aria-labelledby': ariaLabelledby,\n      ...props\n    },\n    ref\n  ) => {\n    const trackRef = React.useRef<HTMLDivElement>(null)\n    const animationRef = React.useRef<number | null>(null)\n    const lastTimeRef = React.useRef<number>(0)\n    const isDraggingRef = React.useRef<boolean>(false)\n    const keyboardDragTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const currentValueRef = React.useRef<number[]>(defaultValue)\n\n    // Track active drag handlers for cleanup on unmount\n    const dragHandlersRef = React.useRef<{\n      onMove: ((e: PointerEvent) => void) | null\n      onUp: (() => void) | null\n    }>({ onMove: null, onUp: null })\n\n    // `max === min` made every percentage NaN/Infinity, which flowed straight\n    // into `left: calc(NaN% - 14px)`; `step === 0` made the snap Math.round\n    // produce NaN. Both are reachable from computed props.\n    const safeRange = max - min || Number.EPSILON\n    const safeStep = step > 0 ? step : 1\n    const toPercent = React.useCallback(\n      (v: number) => ((v - min) / safeRange) * 100,\n      [min, safeRange]\n    )\n\n    const isControlled = controlledValue !== undefined\n    const wasControlled = React.useRef(isControlled)\n    React.useEffect(() => {\n      if (wasControlled.current !== isControlled) {\n        if (process.env.NODE_ENV === 'development') {\n          console.warn('[Slider] Component is changing from ' + (wasControlled.current ? 'controlled' : 'uncontrolled') + ' to ' + (isControlled ? 'controlled' : 'uncontrolled') + '. This is not supported.')\n        }\n        wasControlled.current = isControlled\n      }\n    }, [isControlled])\n    const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n    const actualValue = isControlled ? controlledValue : uncontrolledValue\n\n    const isRange = actualValue.length > 1\n\n    // Spring physics state for each thumb\n    const [springs, setSprings] = React.useState<SpringState[]>(() =>\n      actualValue.map((v) => ({\n        position: toPercent(v),\n        velocity: 0,\n      }))\n    )\n\n    const [targets, setTargets] = React.useState<number[]>(() =>\n      actualValue.map(toPercent)\n    )\n    const targetsRef = React.useRef<number[]>(targets)\n\n    const [activeThumb, setActiveThumb] = React.useState<number | null>(null)\n    const [squishes, setSquishes] = React.useState<{ scaleX: number; scaleY: number }[]>(() =>\n      actualValue.map(() => ({ scaleX: 1, scaleY: 1 }))\n    )\n    const [hoveringThumb, setHoveringThumb] = React.useState<number | null>(null)\n\n    // Keep currentValueRef in sync for stale-closure-free commit\n    React.useEffect(() => {\n      currentValueRef.current = actualValue\n    }, [actualValue])\n\n    // Update targets when value changes externally\n    React.useEffect(() => {\n      const newTargets = actualValue.map(toPercent)\n      setTargets(newTargets)\n      targetsRef.current = newTargets\n\n      // Ensure springs array matches value array length\n      if (springs.length !== actualValue.length) {\n        setSprings(actualValue.map((v) => ({\n          position: toPercent(v),\n          velocity: 0,\n        })))\n        setSquishes(actualValue.map(() => ({ scaleX: 1, scaleY: 1 })))\n      }\n    }, [actualValue, min, max, springs.length, toPercent])\n\n    // Spring physics simulation — only runs while dragging\n    const startSpringLoop = React.useCallback(() => {\n      if (animationRef.current) return // already running\n\n      lastTimeRef.current = 0\n\n      const simulate = (timestamp: number) => {\n        if (!isDraggingRef.current) {\n          animationRef.current = null\n          return\n        }\n\n        if (!lastTimeRef.current) {\n          lastTimeRef.current = timestamp\n        }\n\n        const deltaTime = Math.min((timestamp - lastTimeRef.current) / 1000, 0.064)\n        lastTimeRef.current = timestamp\n\n        // Computed outside the updater: a state updater must be pure, and\n        // React StrictMode double-invokes it — calling setSquishes in there\n        // fired it twice per frame.\n        const pendingSquishes: { scaleX: number; scaleY: number }[] = []\n\n        setSprings((prev) => {\n          const newSprings: SpringState[] = []\n          const newSquishes: { scaleX: number; scaleY: number }[] = []\n\n          const len = Math.min(prev.length, targetsRef.current.length)\n          for (let i = 0; i < len; i++) {\n            const displacement = targetsRef.current[i] - prev[i].position\n            const springForce = stiffness * displacement\n            const dampingForce = damping * prev[i].velocity\n            const acceleration = (springForce - dampingForce) / mass\n\n            const newVelocity = prev[i].velocity + acceleration * deltaTime\n            const newPosition = prev[i].position + newVelocity * deltaTime\n\n            // Calculate squish based on velocity\n            const velocityMagnitude = Math.abs(newVelocity)\n            const squishAmount = Math.min(velocityMagnitude / 500, 0.25)\n            const direction = newVelocity > 0 ? 1 : -1\n\n            newSquishes.push({\n              scaleX: 1 + squishAmount * direction * 0.4,\n              scaleY: 1 - squishAmount * 0.25,\n            })\n\n            // Stop animation when settled\n            if (Math.abs(displacement) < 0.01 && Math.abs(newVelocity) < 0.01) {\n              newSprings.push({ position: targetsRef.current[i], velocity: 0 })\n              newSquishes[i] = { scaleX: 1, scaleY: 1 }\n            } else {\n              newSprings.push({ position: newPosition, velocity: newVelocity })\n            }\n          }\n\n          pendingSquishes.length = 0\n          pendingSquishes.push(...newSquishes)\n          return newSprings\n        })\n        setSquishes(pendingSquishes)\n\n        animationRef.current = requestAnimationFrame(simulate)\n      }\n\n      animationRef.current = requestAnimationFrame(simulate)\n    }, [stiffness, damping, mass])\n\n    // Cleanup rAF on unmount\n    React.useEffect(() => {\n      return () => {\n        if (animationRef.current) {\n          cancelAnimationFrame(animationRef.current)\n        }\n        if (keyboardDragTimeoutRef.current) {\n          clearTimeout(keyboardDragTimeoutRef.current)\n        }\n      }\n    }, [])\n\n    // Cleanup drag handlers on unmount\n    React.useEffect(() => {\n      return () => {\n        const { onMove, onUp } = dragHandlersRef.current\n        if (onMove) {\n          document.removeEventListener('pointermove', onMove)\n        }\n        if (onUp) {\n          document.removeEventListener('pointerup', onUp)\n        }\n      }\n    }, [])\n\n    const getValueFromPosition = (clientX: number, clientY: number) => {\n      if (!trackRef.current) return 0\n\n      const rect = trackRef.current.getBoundingClientRect()\n      let percent: number\n\n      if (orientation === 'vertical') {\n        percent = 1 - Math.max(0, Math.min(1, (clientY - rect.top) / rect.height))\n      } else {\n        percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))\n      }\n\n      const rawValue = min + percent * safeRange\n      const steppedValue = Math.round(rawValue / safeStep) * safeStep\n      return Math.max(min, Math.min(max, steppedValue))\n    }\n\n    const getPercentFromPosition = (clientX: number, clientY: number) => {\n      if (!trackRef.current) return 0\n\n      const rect = trackRef.current.getBoundingClientRect()\n\n      if (orientation === 'vertical') {\n        return (1 - Math.max(0, Math.min(1, (clientY - rect.top) / rect.height))) * 100\n      }\n      return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * 100\n    }\n\n    const findNearestThumb = (percent: number): number => {\n      if (!isRange) return 0\n\n      let nearestIndex = 0\n      let nearestDistance = Infinity\n\n      for (let i = 0; i < springs.length; i++) {\n        const distance = Math.abs(springs[i].position - percent)\n        if (distance < nearestDistance) {\n          nearestDistance = distance\n          nearestIndex = i\n        }\n      }\n\n      return nearestIndex\n    }\n\n    const updateValue = (index: number, newValue: number) => {\n      const newValues = [...actualValue]\n\n      if (isRange) {\n        // Ensure values don't cross over\n        if (index === 0) {\n          newValues[0] = Math.min(newValue, actualValue[1] - step)\n        } else {\n          newValues[1] = Math.max(newValue, actualValue[0] + step)\n        }\n      } else {\n        newValues[index] = newValue\n      }\n\n      // Clamp to min/max\n      newValues[index] = Math.max(min, Math.min(max, newValues[index]))\n\n      if (!isControlled) {\n        setUncontrolledValue(newValues)\n      }\n      onValueChange?.(newValues)\n    }\n\n    const handleTrackPointerDown = (e: React.PointerEvent) => {\n      if (disabled) return\n\n      e.preventDefault()\n      const percent = getPercentFromPosition(e.clientX, e.clientY)\n      const nearestThumbIndex = findNearestThumb(percent)\n      const newValue = getValueFromPosition(e.clientX, e.clientY)\n\n      setActiveThumb(nearestThumbIndex)\n      isDraggingRef.current = true\n      startSpringLoop()\n      updateValue(nearestThumbIndex, newValue)\n\n      // Add velocity boost for jelly effect\n      setSprings((prev) => {\n        const newSprings = [...prev]\n        newSprings[nearestThumbIndex] = {\n          ...newSprings[nearestThumbIndex],\n          velocity: (e.movementX || 0) * 10,\n        }\n        return newSprings\n      })\n\n      const handlePointerMove = (e: PointerEvent) => {\n        const newValue = getValueFromPosition(e.clientX, e.clientY)\n        updateValue(nearestThumbIndex, newValue)\n\n        // Add velocity for jelly effect\n        setSprings((prev) => {\n          const newSprings = [...prev]\n          newSprings[nearestThumbIndex] = {\n            ...newSprings[nearestThumbIndex],\n            velocity: newSprings[nearestThumbIndex].velocity + (e.movementX || 0) * 3,\n          }\n          return newSprings\n        })\n      }\n\n      const handlePointerUp = () => {\n        setActiveThumb(null)\n        isDraggingRef.current = false\n        onValueCommit?.(currentValueRef.current)\n        document.removeEventListener('pointermove', handlePointerMove)\n        document.removeEventListener('pointerup', handlePointerUp)\n        dragHandlersRef.current = { onMove: null, onUp: null }\n      }\n\n      // Store handlers for cleanup\n      dragHandlersRef.current = { onMove: handlePointerMove, onUp: handlePointerUp }\n\n      document.addEventListener('pointermove', handlePointerMove)\n      document.addEventListener('pointerup', handlePointerUp)\n    }\n\n    const handleThumbPointerDown = (index: number) => (e: React.PointerEvent) => {\n      if (disabled) return\n\n      e.preventDefault()\n      e.stopPropagation()\n      setActiveThumb(index)\n      isDraggingRef.current = true\n      startSpringLoop()\n\n      const handlePointerMove = (e: PointerEvent) => {\n        const newValue = getValueFromPosition(e.clientX, e.clientY)\n        updateValue(index, newValue)\n\n        // Add velocity for jelly effect\n        setSprings((prev) => {\n          const newSprings = [...prev]\n          newSprings[index] = {\n            ...newSprings[index],\n            velocity: newSprings[index].velocity + (e.movementX || 0) * 3,\n          }\n          return newSprings\n        })\n      }\n\n      const handlePointerUp = () => {\n        setActiveThumb(null)\n        isDraggingRef.current = false\n        onValueCommit?.(currentValueRef.current)\n        document.removeEventListener('pointermove', handlePointerMove)\n        document.removeEventListener('pointerup', handlePointerUp)\n        dragHandlersRef.current = { onMove: null, onUp: null }\n      }\n\n      // Store handlers for cleanup\n      dragHandlersRef.current = { onMove: handlePointerMove, onUp: handlePointerUp }\n\n      document.addEventListener('pointermove', handlePointerMove)\n      document.addEventListener('pointerup', handlePointerUp)\n    }\n\n    const handleKeyDown = (index: number) => (e: React.KeyboardEvent) => {\n      if (disabled) return\n\n      let newValue = actualValue[index]\n      const largeStep = safeRange / 10\n\n      switch (e.key) {\n        case 'ArrowRight':\n        case 'ArrowUp':\n          newValue = Math.min(max, actualValue[index] + step)\n          break\n        case 'ArrowLeft':\n        case 'ArrowDown':\n          newValue = Math.max(min, actualValue[index] - step)\n          break\n        case 'PageUp':\n          newValue = Math.min(max, actualValue[index] + largeStep)\n          break\n        case 'PageDown':\n          newValue = Math.max(min, actualValue[index] - largeStep)\n          break\n        case 'Home':\n          newValue = min\n          break\n        case 'End':\n          newValue = max\n          break\n        default:\n          return\n      }\n\n      e.preventDefault()\n      updateValue(index, newValue)\n\n      // Add velocity for keyboard navigation jelly effect\n      setSprings((prev) => {\n        const newSprings = [...prev]\n        newSprings[index] = {\n          ...newSprings[index],\n          velocity: (newValue > actualValue[index] ? 1 : -1) * 80,\n        }\n        return newSprings\n      })\n      isDraggingRef.current = true\n      startSpringLoop()\n      if (keyboardDragTimeoutRef.current) clearTimeout(keyboardDragTimeoutRef.current)\n      keyboardDragTimeoutRef.current = setTimeout(() => { isDraggingRef.current = false }, 300)\n    }\n\n    // Calculate range fill position\n    const getRangeFillStyle = () => {\n      if (isRange) {\n        const left = Math.min(springs[0]?.position ?? 0, springs[1]?.position ?? 0)\n        const right = Math.max(springs[0]?.position ?? 0, springs[1]?.position ?? 0)\n        return {\n          left: `${left}%`,\n          width: `${right - left}%`,\n        }\n      }\n      return {\n        left: '0%',\n        width: `${springs[0]?.position ?? 0}%`,\n      }\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          'relative flex touch-none select-none items-center',\n          orientation === 'vertical' ? 'h-full w-5 flex-col' : 'w-full py-2',\n          disabled && 'opacity-50 pointer-events-none',\n          className\n        )}\n        {...props}\n      >\n        {/* Track */}\n        <div\n          ref={trackRef}\n          className={cn(\n            'relative cursor-pointer overflow-hidden',\n            'border-3 border-foreground bg-muted',\n            'shadow-[4px_4px_0px_hsl(var(--shadow-color))]',\n            'transition-shadow duration-200',\n            orientation === 'vertical' ? 'h-full w-4' : 'h-4 w-full',\n            activeThumb !== null && 'shadow-[2px_2px_0px_hsl(var(--shadow-color))]'\n          )}\n          onPointerDown={handleTrackPointerDown}\n        >\n          {/* Range fill */}\n          <div\n            className={cn(\n              'absolute bg-primary',\n              orientation === 'vertical' ? 'w-full' : 'h-full'\n            )}\n            style={\n              orientation === 'vertical'\n                ? {\n                    bottom: getRangeFillStyle().left,\n                    height: getRangeFillStyle().width,\n                  }\n                : getRangeFillStyle()\n            }\n          />\n\n          {/* Decorative stripes */}\n          <div\n            className=\"absolute inset-0 opacity-10 pointer-events-none\"\n            style={{\n              backgroundImage: `repeating-linear-gradient(\n                ${orientation === 'vertical' ? '0deg' : '45deg'},\n                transparent,\n                transparent 3px,\n                currentColor 3px,\n                currentColor 4px\n              )`,\n            }}\n          />\n        </div>\n\n        {/* Thumbs */}\n        {springs.map((spring, index) => (\n          <div\n            key={`thumb-${index}`}\n            role=\"slider\"\n            tabIndex={disabled ? -1 : 0}\n            aria-valuemin={min}\n            aria-valuemax={max}\n            aria-valuenow={actualValue[index]}\n            aria-valuetext={`${actualValue[index]} of ${max}`}\n            aria-disabled={disabled}\n            aria-orientation={orientation}\n            aria-label={ariaLabel}\n            aria-labelledby={ariaLabelledby}\n            onKeyDown={handleKeyDown(index)}\n            onPointerDown={handleThumbPointerDown(index)}\n            onMouseEnter={() => setHoveringThumb(index)}\n            onMouseLeave={() => setHoveringThumb(null)}\n            className={cn(\n              'absolute h-7 w-7 cursor-grab',\n              'border-3 border-foreground bg-background',\n              'shadow-[4px_4px_0px_hsl(var(--shadow-color))]',\n              'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n              'transition-shadow duration-150',\n              activeThumb === index && 'cursor-grabbing shadow-[2px_2px_0px_hsl(var(--shadow-color))] z-10',\n              hoveringThumb === index && activeThumb !== index && 'shadow-[5px_5px_0px_hsl(var(--shadow-color))]'\n            )}\n            style={\n              orientation === 'vertical'\n                ? {\n                    bottom: `calc(${spring.position}% - 14px)`,\n                    left: '50%',\n                    transform: `\n                      translateX(-50%)\n                      scaleX(${squishes[index]?.scaleX ?? 1})\n                      scaleY(${squishes[index]?.scaleY ?? 1})\n                      rotate(${((squishes[index]?.scaleX ?? 1) - 1) * 8}deg)\n                    `,\n                  }\n                : {\n                    left: `calc(${spring.position}% - 14px)`,\n                    top: '50%',\n                    transform: `\n                      translateY(-50%)\n                      scaleX(${squishes[index]?.scaleX ?? 1})\n                      scaleY(${squishes[index]?.scaleY ?? 1})\n                      rotate(${((squishes[index]?.scaleX ?? 1) - 1) * 8}deg)\n                    `,\n                  }\n            }\n          >\n            {/* Inner border detail */}\n            <div className=\"absolute inset-1 border-2 border-foreground/20\" />\n            {/* Grip lines */}\n            <div className=\"absolute inset-0 flex items-center justify-center\">\n              <div className={cn(\n                'flex gap-0.5',\n                orientation === 'vertical' ? 'flex-row' : 'flex-col'\n              )}>\n                <div className={cn(\n                  'bg-foreground/30',\n                  orientation === 'vertical' ? 'w-0.5 h-3' : 'w-3 h-0.5'\n                )} />\n                <div className={cn(\n                  'bg-foreground/30',\n                  orientation === 'vertical' ? 'w-0.5 h-3' : 'w-3 h-0.5'\n                )} />\n                <div className={cn(\n                  'bg-foreground/30',\n                  orientation === 'vertical' ? 'w-0.5 h-3' : 'w-3 h-0.5'\n                )} />\n              </div>\n            </div>\n          </div>\n        ))}\n      </div>\n    )\n  }\n)\nSlider.displayName = 'Slider'\n\nexport { Slider }\n",
      "type": "registry:ui",
      "target": "components/ui/slider.tsx"
    }
  ],
  "type": "registry:ui"
}