{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [
    "@base-ui/react@1.8.0",
    "@liquefy-ui/core@^1.1.0"
  ],
  "description": "A range input with a luminous track and a dimensional thumb.",
  "docs": "Two steps after installing: import the copied stylesheet once at your app entry — it lands at `styles/liquefy-ui.css` — or `@import` it from your global CSS *before* `tailwindcss`, so Tailwind utilities keep winning over component styles. Then wrap the tree in `<LiquefyProvider>`, which is where every component reads its tokens from. Every copied file already carries its own 'use client' directive. Full documentation: https://liquefy-ui.com/#/docs/installation",
  "files": [
    {
      "content": "'use client'\n\nimport { Slider } from '@base-ui/react/slider'\nimport { SpringValue } from '@liquefy-ui/core'\nimport { forwardRef, useEffect, useRef, type HTMLAttributes, type ReactNode } from 'react'\nimport { useLiquefyConfig } from '@/lib/provider'\nimport { useLiquidStyles, type LiquidStyleProps } from '@/lib/styles-prop'\n\n// This wrapped a bare `input[type=range]` and painted it through the vendor\n// pseudo-elements, which is why the filled part of the track was a fixed\n// gradient rather than the value: `::-webkit-slider-runnable-track` cannot know\n// where the thumb is. Firefox got a plain grey track instead, because\n// `::-moz-range-track` cannot take the gradient at all. Base UI draws the track,\n// the filled indicator and the thumb as real elements, so the fill follows the\n// value and both browsers get the same slider.\n\nexport type LiquidSliderProps = Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange'> & LiquidStyleProps & {\n  defaultValue?: number\n  disabled?: boolean\n  endAdornment?: ReactNode\n  label?: string\n  max?: number\n  min?: number\n  name?: string\n  onValueChange?: (value: number) => void\n  startAdornment?: ReactNode\n  step?: number\n  value?: number\n}\n\n/** A slider carries one value here; Base UI's array form is for ranged sliders. */\nconst single = (value: number | number[]): number => Array.isArray(value) ? value[0] ?? 0 : value\n\nexport const LiquidSlider = forwardRef<HTMLInputElement, LiquidSliderProps>(({\n  'aria-label': ariaLabel,\n  className,\n  defaultValue,\n  disabled,\n  endAdornment,\n  label,\n  max = 100,\n  min = 0,\n  name,\n  onValueChange,\n  startAdornment,\n  step,\n  style,\n  styles,\n  value,\n  ...props\n}, forwardedRef) => {\n  const config = useLiquefyConfig()\n  const thumbRef = useRef<HTMLDivElement>(null)\n  const lastValue = useRef(single(value ?? defaultValue ?? min))\n  // The springs live in an effect, but the value changes arrive as React\n  // callbacks, so the two talk through these.\n  const kick = useRef<(moved: number) => void>(() => {})\n  const settleTo = useRef<(target: number) => void>(() => {})\n\n  // Spring-driven thumb squish: every value change kicks a horizontal stretch\n  // that springs back, so the thumb wobbles like jelly while you drag it.\n  useEffect(() => {\n    const thumb = thumbRef.current\n    if (!thumb || config.motion === false) return undefined\n\n    const wobbliness = config.wobbliness\n    const scaleX = new SpringValue(1, { damping: 13, mass: 0.9, stiffness: 340 })\n    const scaleY = new SpringValue(1, { damping: 13, mass: 0.9, stiffness: 340 })\n    let frame = 0\n    let lastTime = performance.now()\n\n    const apply = () => {\n      thumb.style.setProperty('--lq-thumb-sx', scaleX.current.toFixed(4))\n      thumb.style.setProperty('--lq-thumb-sy', scaleY.current.toFixed(4))\n    }\n\n    const tick = (time: number) => {\n      const delta = (time - lastTime) / 1000\n      lastTime = time\n      const movingX = scaleX.step(delta)\n      const movingY = scaleY.step(delta)\n      apply()\n      frame = movingX || movingY ? requestAnimationFrame(tick) : 0\n    }\n\n    const start = () => {\n      if (frame) return\n      lastTime = performance.now()\n      frame = requestAnimationFrame(tick)\n    }\n\n    kick.current = (moved) => {\n      const amount = Math.min(0.35 + moved * 9 * wobbliness, 1.25)\n      scaleX.velocity += amount\n      scaleY.velocity -= amount\n      start()\n    }\n\n    settleTo.current = (target) => {\n      scaleX.setTarget(target)\n      scaleY.setTarget(target)\n      start()\n    }\n\n    // A drag that ends anywhere on the page still ends the drag.\n    const settle = () => settleTo.current(1)\n    window.addEventListener('pointerup', settle)\n    window.addEventListener('pointercancel', settle)\n\n    return () => {\n      cancelAnimationFrame(frame)\n      window.removeEventListener('pointerup', settle)\n      window.removeEventListener('pointercancel', settle)\n      kick.current = () => {}\n      settleTo.current = () => {}\n      thumb.style.removeProperty('--lq-thumb-sx')\n      thumb.style.removeProperty('--lq-thumb-sy')\n    }\n  }, [config.motion, config.wobbliness])\n\n  // The slider is the layout box, so it owns className, style and styles alike.\n  const root = useLiquidStyles('lq-slider', { className, style, styles })\n\n  return (\n    <Slider.Root\n      aria-label={ariaLabel}\n      className={root.className}\n      defaultValue={defaultValue}\n      disabled={disabled}\n      max={max}\n      min={min}\n      name={name}\n      onValueChange={(next) => {\n        const nextValue = single(next)\n        const moved = Math.abs(nextValue - lastValue.current) / Math.max(max - min, 1)\n        lastValue.current = nextValue\n        kick.current(moved)\n        onValueChange?.(nextValue)\n      }}\n      step={step}\n      style={root.style}\n      value={value}\n      {...props}\n    >\n      {label && <Slider.Label className=\"lq-control-label\">{label}</Slider.Label>}\n      <span className=\"lq-slider__row\">\n        {startAdornment && <span className=\"lq-slider__adornment\">{startAdornment}</span>}\n        <Slider.Control\n          className=\"lq-slider__control\"\n          onPointerDown={() => settleTo.current(1.16)}\n        >\n          <Slider.Track className=\"lq-slider__track\">\n            <Slider.Indicator className=\"lq-slider__indicator\" />\n            <Slider.Thumb\n              className=\"lq-slider__thumb\"\n              // The label names the group; the input inside the thumb is what a\n              // screen reader lands on, and it needs the name too.\n              getAriaLabel={ariaLabel ? () => ariaLabel : undefined}\n              inputRef={forwardedRef}\n              onBlur={() => settleTo.current(1)}\n              onFocus={() => settleTo.current(1.16)}\n              ref={thumbRef}\n            />\n          </Slider.Track>\n        </Slider.Control>\n        {endAdornment && <span className=\"lq-slider__adornment\">{endAdornment}</span>}\n      </span>\n    </Slider.Root>\n  )\n})\n\nLiquidSlider.displayName = 'LiquidSlider'\n",
      "path": "registry/liquid-slider.tsx",
      "type": "registry:ui"
    }
  ],
  "name": "liquid-slider",
  "registryDependencies": [
    "https://liquefy-ui.com/r/liquefy-ui.json",
    "https://liquefy-ui.com/r/provider.json",
    "https://liquefy-ui.com/r/styles-prop.json"
  ],
  "title": "LiquidSlider",
  "type": "registry:ui"
}
