{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ElasticSlider-TS-CSS",
	"title": "ElasticSlider",
	"description": "Slider handle stretches elastically then snaps with spring physics.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "ElasticSlider.css",
			"target": "@components/ElasticSlider.css",
			"content": ".slider-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n  width: 12rem;\n}\n\n.slider-wrapper {\n  display: flex;\n  width: 100%;\n  touch-action: none;\n  user-select: none;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n}\n\n.slider-root {\n  position: relative;\n  display: flex;\n  width: 100%;\n  max-width: 200px;\n  flex-grow: 1;\n  cursor: grab;\n  touch-action: none;\n  user-select: none;\n  align-items: center;\n  padding: 1rem 0;\n}\n\n.slider-root:active {\n  cursor: grabbing;\n}\n\n.slider-track-wrapper {\n  display: flex;\n  flex-grow: 1;\n}\n\n.slider-track {\n  position: relative;\n  height: 100%;\n  flex-grow: 1;\n  overflow: hidden;\n  border-radius: 9999px;\n  background-color: rgba(128, 128, 128, 0.4);\n}\n\n.slider-range {\n  position: absolute;\n  height: 100%;\n  background-color: #888;\n  border-radius: 9999px;\n}\n\n.value-indicator {\n  color: #808080;\n  position: absolute;\n  transform: translateY(-1rem);\n  font-size: 0.75rem;\n  font-weight: 500;\n  letter-spacing: 0.05em;\n}\n\n.icon {\n  width: 24px;\n  height: 24px;\n  color: #888;\n}\n\n.icon.dark {\n  color: #ddd;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "ElasticSlider.tsx",
			"content": "import React, { useEffect, useRef, useState } from 'react';\nimport { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion/react';\nimport { Icon } from '@chakra-ui/react';\nimport { RiVolumeDownFill, RiVolumeUpFill } from 'react-icons/ri';\n\nimport './ElasticSlider.css';\n\nconst MAX_OVERFLOW = 50;\n\ninterface ElasticSliderProps {\n  defaultValue?: number;\n  startingValue?: number;\n  maxValue?: number;\n  className?: string;\n  isStepped?: boolean;\n  stepSize?: number;\n  leftIcon?: React.ReactNode;\n  rightIcon?: React.ReactNode;\n}\n\nconst ElasticSlider: React.FC<ElasticSliderProps> = ({\n  defaultValue = 50,\n  startingValue = 0,\n  maxValue = 100,\n  className = '',\n  isStepped = false,\n  stepSize = 1,\n  leftIcon = <Icon as={RiVolumeDownFill} />,\n  rightIcon = <Icon as={RiVolumeUpFill} />\n}) => {\n  return (\n    <div className={`slider-container ${className}`}>\n      <Slider\n        defaultValue={defaultValue}\n        startingValue={startingValue}\n        maxValue={maxValue}\n        isStepped={isStepped}\n        stepSize={stepSize}\n        leftIcon={leftIcon}\n        rightIcon={rightIcon}\n      />\n    </div>\n  );\n};\n\ninterface SliderProps {\n  defaultValue: number;\n  startingValue: number;\n  maxValue: number;\n  isStepped: boolean;\n  stepSize: number;\n  leftIcon: React.ReactNode;\n  rightIcon: React.ReactNode;\n}\n\nconst Slider: React.FC<SliderProps> = ({\n  defaultValue,\n  startingValue,\n  maxValue,\n  isStepped,\n  stepSize,\n  leftIcon,\n  rightIcon\n}) => {\n  const [value, setValue] = useState<number>(defaultValue);\n  const sliderRef = useRef<HTMLDivElement>(null);\n  const [region, setRegion] = useState<'left' | 'middle' | 'right'>('middle');\n  const clientX = useMotionValue(0);\n  const overflow = useMotionValue(0);\n  const scale = useMotionValue(1);\n\n  useEffect(() => {\n    setValue(defaultValue);\n  }, [defaultValue]);\n\n  useMotionValueEvent(clientX, 'change', (latest: number) => {\n    if (sliderRef.current) {\n      const { left, right } = sliderRef.current.getBoundingClientRect();\n      let newValue: number;\n      if (latest < left) {\n        setRegion('left');\n        newValue = left - latest;\n      } else if (latest > right) {\n        setRegion('right');\n        newValue = latest - right;\n      } else {\n        setRegion('middle');\n        newValue = 0;\n      }\n      overflow.jump(decay(newValue, MAX_OVERFLOW));\n    }\n  });\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.buttons > 0 && sliderRef.current) {\n      const { left, width } = sliderRef.current.getBoundingClientRect();\n      let newValue = startingValue + ((e.clientX - left) / width) * (maxValue - startingValue);\n      if (isStepped) {\n        newValue = Math.round(newValue / stepSize) * stepSize;\n      }\n      newValue = Math.min(Math.max(newValue, startingValue), maxValue);\n      setValue(newValue);\n      clientX.jump(e.clientX);\n    }\n  };\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    handlePointerMove(e);\n    e.currentTarget.setPointerCapture(e.pointerId);\n  };\n\n  const handlePointerUp = () => {\n    animate(overflow, 0, { type: 'spring', bounce: 0.5 });\n  };\n\n  const getRangePercentage = (): number => {\n    const totalRange = maxValue - startingValue;\n    if (totalRange === 0) return 0;\n    return ((value - startingValue) / totalRange) * 100;\n  };\n\n  return (\n    <>\n      <motion.div\n        onHoverStart={() => animate(scale, 1.2)}\n        onHoverEnd={() => animate(scale, 1)}\n        onTouchStart={() => animate(scale, 1.2)}\n        onTouchEnd={() => animate(scale, 1)}\n        style={{\n          scale,\n          opacity: useTransform(scale, [1, 1.2], [0.7, 1])\n        }}\n        className=\"slider-wrapper\"\n      >\n        <motion.div\n          animate={{\n            scale: region === 'left' ? [1, 1.4, 1] : 1,\n            transition: { duration: 0.25 }\n          }}\n          style={{\n            x: useTransform(() => (region === 'left' ? -overflow.get() / scale.get() : 0))\n          }}\n        >\n          {leftIcon}\n        </motion.div>\n\n        <div\n          ref={sliderRef}\n          className=\"slider-root\"\n          onPointerMove={handlePointerMove}\n          onPointerDown={handlePointerDown}\n          onPointerUp={handlePointerUp}\n          onPointerCancel={handlePointerUp}\n          onLostPointerCapture={handlePointerUp}\n        >\n          <motion.div\n            style={{\n              scaleX: useTransform(() => {\n                if (sliderRef.current) {\n                  const { width } = sliderRef.current.getBoundingClientRect();\n                  return 1 + overflow.get() / width;\n                }\n                return 1;\n              }),\n              scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.8]),\n              transformOrigin: useTransform(() => {\n                if (sliderRef.current) {\n                  const { left, width } = sliderRef.current.getBoundingClientRect();\n                  return clientX.get() < left + width / 2 ? 'right' : 'left';\n                }\n                return 'center';\n              }),\n              height: useTransform(scale, [1, 1.2], [6, 12]),\n              marginTop: useTransform(scale, [1, 1.2], [0, -3]),\n              marginBottom: useTransform(scale, [1, 1.2], [0, -3])\n            }}\n            className=\"slider-track-wrapper\"\n          >\n            <div className=\"slider-track\">\n              <div className=\"slider-range\" style={{ width: `${getRangePercentage()}%` }} />\n            </div>\n          </motion.div>\n        </div>\n\n        <motion.div\n          animate={{\n            scale: region === 'right' ? [1, 1.4, 1] : 1,\n            transition: { duration: 0.25 }\n          }}\n          style={{\n            x: useTransform(() => (region === 'right' ? overflow.get() / scale.get() : 0))\n          }}\n        >\n          {rightIcon}\n        </motion.div>\n      </motion.div>\n      <p className=\"value-indicator\">{Math.round(value)}</p>\n    </>\n  );\n};\n\nfunction decay(value: number, max: number): number {\n  if (max === 0) {\n    return 0;\n  }\n  const entry = value / max;\n  const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);\n  return sigmoid * max;\n}\n\nexport default ElasticSlider;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@chakra-ui/react@^3.20.0",
		"react-icons@^5.5.0"
	]
}