{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SquishSwitch-JS-CSS",
	"title": "SquishSwitch",
	"description": "Drag-scrubbable switch whose thumb stretches by how fast it moves, flips at the midpoint and squashes against the track end on a flick.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "SquishSwitch.css",
			"target": "@components/SquishSwitch.css",
			"content": ".squish-switch-root {\n  display: inline-flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.squish-switch {\n  --ss-w: 76px;\n  --ss-h: 38px;\n  --ss-inset: 4px;\n  --ss-thumb: 30px;\n  --ss-r: 19px;\n  --ss-thumb-r: 15px;\n  --ss-track: #27272a;\n  --ss-track-on: #f5f5f5;\n  --ss-thumb-color: #4e4e51;\n  --ss-thumb-on: #27272a;\n  --ss-fade: 320ms;\n\n  position: relative;\n  display: inline-block;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  background: none;\n  cursor: pointer;\n  outline: none;\n  touch-action: pan-y;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.squish-switch::after {\n  content: '';\n  position: absolute;\n  inset: -8px;\n}\n\n.squish-switch[data-held] {\n  cursor: grabbing;\n}\n\n.squish-switch[aria-disabled='true'] {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.squish-switch__track {\n  position: relative;\n  display: block;\n  width: var(--ss-w);\n  height: var(--ss-h);\n  border-radius: var(--ss-r);\n  background: var(--ss-track);\n  transition: background-color var(--ss-fade) ease;\n}\n\n.squish-switch[data-on] .squish-switch__track {\n  background: var(--ss-track-on);\n}\n\n.squish-switch__thumb {\n  position: absolute;\n  top: var(--ss-inset);\n  left: 0;\n  width: var(--ss-thumb);\n  height: var(--ss-thumb);\n  border-radius: var(--ss-thumb-r);\n  background: var(--ss-thumb-color);\n  transition: background-color var(--ss-fade) ease;\n}\n\n.squish-switch[data-on] .squish-switch__thumb {\n  background: var(--ss-thumb-on);\n}\n\n.squish-switch__label {\n  cursor: pointer;\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .squish-switch__track,\n  .squish-switch__thumb {\n    transition-duration: 1ms;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "SquishSwitch.jsx",
			"content": "'use client';\n\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from 'motion/react';\nimport './SquishSwitch.css';\n\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value));\n\nconst FLOW_SPRING = { stiffness: 320, damping: 40, mass: 0.6 };\nconst SWELL_SPRING = { stiffness: 520, damping: 34, mass: 0.6 };\nconst MAX_STRETCH = 0.4;\nconst STRETCH_SPEED = 600;\nconst TAP_SLOP = { fine: 4, coarse: 8 };\n\nexport default function SquishSwitch({\n  checked,\n  defaultChecked = false,\n  onChange,\n  label = '',\n  disabled = false,\n  trackColor = '#27272a',\n  trackOnColor = '#f5f5f5',\n  thumbColor = '',\n  thumbOnColor = '',\n  width = 76,\n  height = 38,\n  radius = 19,\n  speed = 50,\n  stretch = 36,\n  hoverScale = 1.035,\n  colorDuration = 320,\n  ariaLabel,\n  className = '',\n  id\n}) {\n  const reduce = useReducedMotion();\n  const inset = Math.max(3, Math.round(height * 0.11));\n  const thumb = height - inset * 2;\n  const min = inset;\n  const max = width - inset - thumb;\n  const mid = (min + max) / 2;\n  const trackRadius = Math.min(radius, height / 2);\n  const thumbRadius = Math.max(2, trackRadius - inset);\n\n  const isControlled = checked !== undefined;\n  const [inner, setInner] = useState(defaultChecked);\n  const on = isControlled ? checked : inner;\n  const [dragging, setDragging] = useState(false);\n  const trackRef = useRef(null);\n  const grip = useRef(null);\n  const onRef = useRef(on);\n  onRef.current = on;\n  const skipClick = useRef(false);\n  const autoId = useId();\n  const buttonId = id ?? autoId;\n\n  const x = useMotionValue(on ? max : min);\n  const flow = useSpring(useVelocity(x), FLOW_SPRING);\n  const swell = useSpring(1, SWELL_SPRING);\n  const gain = reduce ? 0 : clamp(stretch, 0, 100) / 100;\n  const stretchOf = v => 1 + Math.min(MAX_STRETCH, Math.abs(v) / STRETCH_SPEED) * gain;\n  const scaleX = useTransform([flow, swell], ([v, h]) => stretchOf(v) * h);\n  const scaleY = useTransform([flow, swell], ([v, h]) => h / stretchOf(v));\n\n  const commit = next => {\n    if (next === onRef.current) return;\n    onRef.current = next;\n    if (!isControlled) setInner(next);\n    onChange?.(next);\n  };\n\n  useEffect(() => {\n    if (dragging) return undefined;\n    const target = on ? max : min;\n    if (reduce) {\n      x.jump(target);\n      return undefined;\n    }\n    const controls = animate(x, target, {\n      type: 'spring',\n      stiffness: 170 - (50 - clamp(speed, 0, 100)) * 1.1,\n      damping: 21.5,\n      mass: 0.9,\n      restDelta: 0.001,\n      restSpeed: 0.01\n    });\n    return () => controls.stop();\n  }, [on, dragging, min, max, speed, reduce, x]);\n\n  const localX = clientX => {\n    const el = trackRef.current;\n    if (!el) return 0;\n    const rect = el.getBoundingClientRect();\n    const scale = rect.width / (el.offsetWidth || rect.width) || 1;\n    return (clientX - rect.left) / scale;\n  };\n  const down = e => {\n    if (disabled || grip.current || e.button !== 0) return;\n    grip.current = {\n      id: e.pointerId,\n      grab: null,\n      moved: false,\n      startX: e.clientX,\n      onAtPress: onRef.current,\n      slop: e.pointerType === 'touch' ? TAP_SLOP.coarse : TAP_SLOP.fine\n    };\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n    setDragging(true);\n  };\n  const move = e => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    const lx = localX(e.clientX);\n    if (g.grab === null) {\n      g.grab = lx - x.get();\n      return;\n    }\n    if (!g.moved && Math.abs(e.clientX - g.startX) > g.slop) g.moved = true;\n    if (!g.moved) return;\n    const nx = clamp(lx - g.grab, min, max);\n    x.set(nx);\n    commit(nx > mid);\n  };\n  const up = (e, cancelled) => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    grip.current = null;\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n    if (cancelled) commit(g.onAtPress);\n    else if (!g.moved) commit(!onRef.current);\n    skipClick.current = true;\n    setTimeout(() => {\n      skipClick.current = false;\n    }, 0);\n    setDragging(false);\n  };\n  const click = () => {\n    if (skipClick.current) {\n      skipClick.current = false;\n      return;\n    }\n    if (!disabled) commit(!onRef.current);\n  };\n\n  return (\n    <span className={`squish-switch-root${className ? ` ${className}` : ''}`}>\n      <button\n        id={buttonId}\n        type=\"button\"\n        role=\"switch\"\n        aria-checked={on}\n        aria-disabled={disabled || undefined}\n        aria-label={ariaLabel}\n        className=\"squish-switch\"\n        data-on={on ? '' : undefined}\n        data-held={dragging ? '' : undefined}\n        style={{\n          '--ss-w': `${width}px`,\n          '--ss-h': `${height}px`,\n          '--ss-inset': `${inset}px`,\n          '--ss-thumb': `${thumb}px`,\n          '--ss-r': `${trackRadius}px`,\n          '--ss-thumb-r': `${thumbRadius}px`,\n          '--ss-track': trackColor,\n          '--ss-track-on': trackOnColor,\n          '--ss-thumb-color': thumbColor || `color-mix(in srgb, ${trackOnColor} 19%, ${trackColor})`,\n          '--ss-thumb-on': thumbOnColor || trackColor,\n          '--ss-fade': `${colorDuration}ms`\n        }}\n        onPointerDown={down}\n        onPointerMove={move}\n        onPointerUp={e => up(e, false)}\n        onPointerCancel={e => up(e, true)}\n        onPointerEnter={e => {\n          if (e.pointerType === 'mouse' && !disabled) swell.set(hoverScale);\n        }}\n        onPointerLeave={() => swell.set(1)}\n        onKeyDown={e => {\n          if (e.key === 'Escape' && grip.current)\n            up({ pointerId: grip.current.id, currentTarget: e.currentTarget }, true);\n        }}\n        onClick={click}\n      >\n        <span ref={trackRef} className=\"squish-switch__track\">\n          <motion.span className=\"squish-switch__thumb\" aria-hidden=\"true\" style={{ x, scaleX, scaleY }} />\n        </span>\n      </button>\n      {label ? (\n        <label htmlFor={buttonId} className=\"squish-switch__label\">\n          {label}\n        </label>\n      ) : null}\n    </span>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}