{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SquishSwitch-TS-TW",
	"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:component",
			"path": "SquishSwitch/SquishSwitch.tsx",
			"content": "'use client';\n\nimport React, {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent\n} from 'react';\nimport { animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from 'motion/react';\nexport interface SquishSwitchProps {\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onChange?: (checked: boolean) => void;\n  label?: string;\n  disabled?: boolean;\n  trackColor?: string;\n  trackOnColor?: string;\n  thumbColor?: string;\n  thumbOnColor?: string;\n  width?: number;\n  height?: number;\n  radius?: number;\n  speed?: number;\n  stretch?: number;\n  hoverScale?: number;\n  colorDuration?: number;\n  ariaLabel?: string;\n  className?: string;\n  id?: string;\n}\n\ninterface Grip {\n  id: number;\n  grab: number | null;\n  moved: boolean;\n  startX: number;\n  onAtPress: boolean;\n  slop: number;\n}\n\nconst clamp = (value: number, min: number, max: number) => 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\nconst SquishSwitch: React.FC<SquishSwitchProps> = ({\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<HTMLSpanElement>(null);\n  const grip = useRef<Grip | null>(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: number) => 1 + Math.min(MAX_STRETCH, Math.abs(v) / STRETCH_SPEED) * gain;\n  const scaleX = useTransform([flow, swell], ([v, h]: number[]) => stretchOf(v) * h);\n  const scaleY = useTransform([flow, swell], ([v, h]: number[]) => h / stretchOf(v));\n\n  const commit = (next: boolean) => {\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: number) => {\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: PointerEvent<HTMLButtonElement>) => {\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: PointerEvent<HTMLButtonElement>) => {\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: { pointerId: number; currentTarget: HTMLButtonElement }, cancelled: boolean) => {\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={`inline-flex items-center gap-2.5${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=\"group relative m-0 inline-block cursor-pointer touch-pan-y border-0 bg-transparent p-0 outline-none select-none [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] after:absolute after:-inset-2 after:content-[''] data-[held]:cursor-grabbing aria-disabled:cursor-not-allowed aria-disabled:opacity-50\"\n        data-on={on ? '' : undefined}\n        data-held={dragging ? '' : undefined}\n        style={\n          {\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          } as CSSProperties\n        }\n        onPointerDown={down}\n        onPointerMove={move}\n        onPointerUp={e => up(e, false)}\n        onPointerCancel={e => up(e, true)}\n        onPointerEnter={(e: PointerEvent<HTMLButtonElement>) => {\n          if (e.pointerType === 'mouse' && !disabled) swell.set(hoverScale);\n        }}\n        onPointerLeave={() => swell.set(1)}\n        onKeyDown={(e: KeyboardEvent<HTMLButtonElement>) => {\n          if (e.key === 'Escape' && grip.current)\n            up({ pointerId: grip.current.id, currentTarget: e.currentTarget }, true);\n        }}\n        onClick={click}\n      >\n        <span\n          ref={trackRef}\n          className=\"relative block [width:var(--ss-w)] [height:var(--ss-h)] [border-radius:var(--ss-r)] [background:var(--ss-track)] [transition:background-color_var(--ss-fade)_ease] group-data-[on]:[background:var(--ss-track-on)] motion-reduce:[transition-duration:1ms]\"\n        >\n          <motion.span\n            className=\"absolute left-0 [top:var(--ss-inset)] [width:var(--ss-thumb)] [height:var(--ss-thumb)] [border-radius:var(--ss-thumb-r)] [background:var(--ss-thumb-color)] [transition:background-color_var(--ss-fade)_ease] group-data-[on]:[background:var(--ss-thumb-on)] motion-reduce:[transition-duration:1ms]\"\n            aria-hidden=\"true\"\n            style={{ x, scaleX, scaleY }}\n          />\n        </span>\n      </button>\n      {label ? (\n        <label htmlFor={buttonId} className=\"cursor-pointer select-none\">\n          {label}\n        </label>\n      ) : null}\n    </span>\n  );\n};\n\nexport default SquishSwitch;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}