{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"trail-range","type":"registry:ui","title":"Trail Range","description":"A range slider whose thumb drags a glowing wake behind it and squashes when flicked fast. Built on a native range input, so keyboard and forms just work.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/trail-range.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useState, type ChangeEvent } from \"react\";\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\";\n\nexport interface TrailRangeProps {\n  min?: number;\n  max?: number;\n  step?: number;\n  /** Controlled value. */\n  value?: number;\n  defaultValue?: number;\n  onChange?: (value: number) => void;\n  label?: string;\n  showValue?: boolean;\n  color?: string;\n  /** How long the wake lingers behind the thumb, 0-1. */\n  trail?: number;\n  disabled?: boolean;\n  className?: string;\n}\n\nconst THUMB = 22;\n// The native range is invisible but sized so its thumb lands exactly where ours is drawn.\nconst css = `.wake-slider-input{-webkit-appearance:none;appearance:none;background:transparent}\n.wake-slider-input:focus-visible{outline:none}\n.wake-slider-input::-webkit-slider-thumb{-webkit-appearance:none;width:${THUMB}px;height:${THUMB}px}\n.wake-slider-input::-moz-range-thumb{width:${THUMB}px;height:${THUMB}px;border:0}`;\nconst at = (p: number) => `calc(${p * 100}% + ${THUMB / 2 - p * THUMB}px)`;\n\nexport function TrailRange({\n  min = 0,\n  max = 100,\n  step = 1,\n  value,\n  defaultValue = 40,\n  onChange,\n  label = \"Volume\",\n  showValue = true,\n  color = \"#38bdf8\",\n  trail = 0.6,\n  disabled = false,\n  className,\n}: TrailRangeProps) {\n  const id = useId();\n  const reduce = useReducedMotion();\n  const [inner, setInner] = useState(defaultValue);\n  const v = value ?? inner;\n  const pct = max > min ? (Math.min(max, Math.max(min, v)) - min) / (max - min) : 0;\n\n  const target = useMotionValue(pct);\n  useEffect(() => target.set(pct), [pct, target]);\n  const head = useSpring(target, { stiffness: 900, damping: 60 });\n  const k = 10 + 400 * (1 - trail) ** 2;\n  const soft = useSpring(target, { stiffness: k, damping: 1.8 * Math.sqrt(k) });\n  const tail = reduce ? head : soft;\n\n  const lo = useTransform(() => Math.min(head.get(), tail.get()));\n  const span = useTransform(() => Math.abs(head.get() - tail.get()));\n  const wakeLeft = useTransform(lo, at);\n  const wakeWidth = useTransform(span, (s) => `calc(${s * 100}% - ${s * THUMB}px)`);\n  const wakeBg = useTransform(\n    () =>\n      `linear-gradient(${head.get() >= tail.get() ? 90 : 270}deg, transparent, ${color} 55%, color-mix(in oklab, ${color} 45%, white))`,\n  );\n  const fillWidth = useTransform(head, at);\n  const thumbLeft = useTransform(head, at);\n  // Squash-and-stretch from the gap between thumb and wake: fast moves flatten the thumb.\n  const squash = useTransform(span, (s) => Math.min(s * 2.2, 0.35));\n  const scaleX = useTransform(squash, (s) => 1 + s);\n  const scaleY = useTransform(squash, (s) => 1 - s * 0.6);\n  const glow = useTransform(squash, (s) => `0 0 ${8 + s * 60}px ${2 + s * 12}px color-mix(in oklab, ${color} ${40 + s * 150}%, transparent)`);\n\n  const change = (e: ChangeEvent<HTMLInputElement>) => {\n    const n = Number(e.target.value);\n    setInner(n);\n    onChange?.(n);\n  };\n\n  return (\n    <div className={`w-full max-w-sm ${disabled ? \"opacity-50\" : \"\"} ${className ?? \"\"}`}>\n      <style href=\"trail-range\" precedence=\"default\">\n        {css}\n      </style>\n      {(label || showValue) && (\n        <div className=\"mb-2 flex items-baseline justify-between text-sm\">\n          <label htmlFor={id} className=\"font-medium text-foreground\">\n            {label}\n          </label>\n          {showValue && <output htmlFor={id} className=\"text-muted-foreground tabular-nums\">{v}</output>}\n        </div>\n      )}\n      <div className=\"relative h-8\">\n        <input\n          id={id}\n          type=\"range\"\n          min={min}\n          max={max}\n          step={step}\n          value={v}\n          disabled={disabled}\n          onChange={change}\n          className=\"wake-slider-input peer absolute inset-0 z-10 m-0 size-full cursor-pointer outline-none disabled:cursor-not-allowed\"\n        />\n        <span aria-hidden className=\"absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted\" />\n        <motion.span\n          aria-hidden\n          className=\"absolute top-1/2 left-0 h-1.5 -translate-y-1/2 rounded-full\"\n          style={{ width: fillWidth, background: `color-mix(in oklab, ${color} 35%, transparent)` }}\n        />\n        {/* The wake: a blurred glow plus a crisp core, stretching from the lagging tail to the thumb. */}\n        <motion.span\n          aria-hidden\n          className=\"absolute top-1/2 h-4 -translate-y-1/2 rounded-full opacity-90 blur-md\"\n          style={{ left: wakeLeft, width: wakeWidth, background: wakeBg }}\n        />\n        <motion.span\n          aria-hidden\n          className=\"absolute top-1/2 h-2 -translate-y-1/2 rounded-full\"\n          style={{ left: wakeLeft, width: wakeWidth, background: wakeBg }}\n        />\n        <motion.span\n          aria-hidden\n          className=\"pointer-events-none absolute top-1/2 rounded-full border-[3px] bg-background peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring\"\n          style={{\n            left: thumbLeft,\n            width: THUMB,\n            height: THUMB,\n            marginLeft: -THUMB / 2,\n            marginTop: -THUMB / 2,\n            borderColor: color,\n            scaleX,\n            scaleY,\n            boxShadow: glow,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n"}]}