{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-toggle",
  "type": "registry:ui",
  "title": "Liquid Toggle",
  "description": "Organic liquid goo physics switch with fluid meta-ball fusion and spring dynamics.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/liquid-toggle.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type PointerEvent,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type LiquidToggleSize = \"sm\" | \"md\" | \"lg\";\nexport type LiquidToggleColor =\n  | \"monochrome\"\n  | \"emerald\"\n  | \"violet\"\n  | \"amber\"\n  | \"cyan\";\nexport type LiquidToggleViscosity = \"fluid\" | \"jelly\";\n\nexport interface LiquidToggleProps {\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  size?: LiquidToggleSize;\n  color?: LiquidToggleColor;\n  viscosity?: LiquidToggleViscosity;\n  label?: string;\n  className?: string;\n  id?: string;\n}\n\nconst SIZES = {\n  sm: {\n    trackWidth: 42,\n    trackHeight: 24,\n    thumb: 16,\n    drop: 11,\n    off: 4,\n    on: 42 - 16 - 4,\n    blur: 2.6,\n  },\n  md: {\n    trackWidth: 50,\n    trackHeight: 28,\n    thumb: 18,\n    drop: 13,\n    off: 5,\n    on: 50 - 18 - 5,\n    blur: 3.2,\n  },\n  lg: {\n    trackWidth: 64,\n    trackHeight: 36,\n    thumb: 24,\n    drop: 17,\n    off: 6,\n    on: 64 - 24 - 6,\n    blur: 4.2,\n  },\n} as const;\n\nconst TUNING = {\n  fluid: {\n    thumbStiffness: 260,\n    thumbDamping: 20,\n    dropStiffness: 125,\n    dropDamping: 15,\n    stretch: 0.003,\n  },\n  jelly: {\n    thumbStiffness: 340,\n    thumbDamping: 14,\n    dropStiffness: 160,\n    dropDamping: 12,\n    stretch: 0.005,\n  },\n} as const;\n\nconst COLOR_STYLES = {\n  monochrome: {\n    activeBlob: \"bg-zinc-900 dark:bg-[#f4f4f5]\",\n    inactiveBlob: \"bg-zinc-400 dark:bg-[#71717a]\",\n    trackOn:\n      \"border-zinc-300 bg-zinc-200/80 shadow-[0_0_20px_rgba(0,0,0,0.06)] dark:border-white/20 dark:bg-white/10 dark:shadow-[0_0_20px_rgba(255,255,255,0.1)]\",\n    trackOff: \"border-zinc-200 bg-zinc-100 dark:border-white/8 dark:bg-white/4\",\n    aura: \"bg-zinc-300/40 dark:bg-white/10\",\n  },\n  emerald: {\n    activeBlob: \"bg-[#059669] dark:bg-[#34d399]\",\n    inactiveBlob: \"bg-zinc-400 dark:bg-[#71717a]\",\n    trackOn:\n      \"border-emerald-500/40 bg-emerald-500/20 shadow-[0_0_24px_rgba(16,185,129,0.2)] dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:shadow-[0_0_24px_rgba(52,211,153,0.2)]\",\n    trackOff: \"border-zinc-200 bg-zinc-100 dark:border-white/8 dark:bg-white/4\",\n    aura: \"bg-emerald-400/20\",\n  },\n  violet: {\n    activeBlob: \"bg-[#7c3aed] dark:bg-[#a78bfa]\",\n    inactiveBlob: \"bg-zinc-400 dark:bg-[#71717a]\",\n    trackOn:\n      \"border-violet-500/40 bg-violet-500/20 shadow-[0_0_24px_rgba(139,92,246,0.2)] dark:border-violet-500/30 dark:bg-violet-500/10 dark:shadow-[0_0_24px_rgba(167,139,250,0.2)]\",\n    trackOff: \"border-zinc-200 bg-zinc-100 dark:border-white/8 dark:bg-white/4\",\n    aura: \"bg-violet-400/20\",\n  },\n  amber: {\n    activeBlob: \"bg-[#d97706] dark:bg-[#fbbf24]\",\n    inactiveBlob: \"bg-zinc-400 dark:bg-[#71717a]\",\n    trackOn:\n      \"border-amber-500/40 bg-amber-500/20 shadow-[0_0_24px_rgba(245,158,11,0.2)] dark:border-amber-500/30 dark:bg-amber-500/10 dark:shadow-[0_0_24px_rgba(251,191,36,0.2)]\",\n    trackOff: \"border-zinc-200 bg-zinc-100 dark:border-white/8 dark:bg-white/4\",\n    aura: \"bg-amber-400/20\",\n  },\n  cyan: {\n    activeBlob: \"bg-[#0891b2] dark:bg-[#22d3ee]\",\n    inactiveBlob: \"bg-zinc-400 dark:bg-[#71717a]\",\n    trackOn:\n      \"border-cyan-500/40 bg-cyan-500/20 shadow-[0_0_24px_rgba(6,182,212,0.2)] dark:border-cyan-500/30 dark:bg-cyan-500/10 dark:shadow-[0_0_24px_rgba(34,211,238,0.2)]\",\n    trackOff: \"border-zinc-200 bg-zinc-100 dark:border-white/8 dark:bg-white/4\",\n    aura: \"bg-cyan-400/20\",\n  },\n} as const;\n\nexport function LiquidToggle({\n  checked: controlledChecked,\n  defaultChecked = false,\n  onChange,\n  disabled = false,\n  size = \"md\",\n  color = \"monochrome\",\n  viscosity = \"fluid\",\n  label = \"Toggle switch\",\n  className,\n  id: customId,\n}: LiquidToggleProps) {\n  const generatedId = useId();\n  const toggleId = customId ?? `liquid-toggle-${generatedId}`;\n  const filterId = `liquid-goo-${generatedId.replace(/:/g, \"\")}`;\n\n  const isControlled = controlledChecked !== undefined;\n  const [internalChecked, setInternalChecked] = useState(defaultChecked);\n  const isChecked = isControlled ? controlledChecked : internalChecked;\n\n  const config = SIZES[size];\n  const tune = TUNING[viscosity];\n  const colorTheme = COLOR_STYLES[color];\n\n  const thumbRef = useRef<HTMLSpanElement>(null);\n  const dropRef = useRef<HTMLSpanElement>(null);\n  const auraRef = useRef<HTMLSpanElement>(null);\n\n  const initialTarget = isChecked ? config.on : config.off;\n  const motion = useRef({\n    x: initialTarget,\n    v: 0,\n    dropX: initialTarget,\n    dropV: 0,\n    target: initialTarget,\n    dragging: false,\n    raf: 0,\n    last: 0,\n  });\n\n  const press = useRef<{\n    startX: number;\n    from: number;\n    moved: boolean;\n  } | null>(null);\n\n  const paint = useCallback(() => {\n    const m = motion.current;\n    const direction = m.v >= 0 ? 1 : -1;\n    const stretch = Math.min(0.35, Math.abs(m.v) * tune.stretch);\n    const scaleX = 1 + stretch;\n    const scaleY = 1 / Math.sqrt(scaleX);\n\n    if (thumbRef.current) {\n      thumbRef.current.style.transform = `translate3d(${m.x}px, 0, 0) scale(${scaleX}, ${scaleY})`;\n    }\n    if (dropRef.current) {\n      const dropOffset = (config.thumb - config.drop) / 2;\n      const trailLag = direction * -0.5 * stretch * config.drop;\n      dropRef.current.style.transform = `translate3d(${m.dropX + dropOffset + trailLag}px, 0, 0)`;\n    }\n    if (auraRef.current) {\n      const progress = (m.x - config.off) / (config.on - config.off || 1);\n      auraRef.current.style.opacity = `${Math.max(0, Math.min(1, progress))}`;\n    }\n  }, [config.drop, config.off, config.on, config.thumb, tune.stretch]);\n\n  const run = useCallback(() => {\n    const m = motion.current;\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      cancelAnimationFrame(m.raf);\n      m.raf = 0;\n      if (!m.dragging) m.x = m.target;\n      m.dropX = m.x;\n      m.v = 0;\n      m.dropV = 0;\n      paint();\n      return;\n    }\n    if (m.raf) return;\n    m.last = performance.now();\n\n    const tick = (now: number) => {\n      const dt = Math.min(0.032, (now - m.last) / 1000);\n      m.last = now;\n\n      if (!m.dragging) {\n        const springForce = tune.thumbStiffness * (m.target - m.x);\n        const dampingForce = tune.thumbDamping * m.v;\n        m.v += (springForce - dampingForce) * dt;\n        m.x += m.v * dt;\n      }\n\n      const dropSpringForce = tune.dropStiffness * (m.x - m.dropX);\n      const dropDampingForce = tune.dropDamping * m.dropV;\n      m.dropV += (dropSpringForce - dropDampingForce) * dt;\n      m.dropX += m.dropV * dt;\n\n      paint();\n\n      const settled =\n        !m.dragging &&\n        Math.abs(m.target - m.x) < 0.05 &&\n        Math.abs(m.v) < 0.5 &&\n        Math.abs(m.x - m.dropX) < 0.05 &&\n        Math.abs(m.dropV) < 0.5;\n\n      if (settled) {\n        m.x = m.target;\n        m.dropX = m.target;\n        m.v = 0;\n        m.dropV = 0;\n        paint();\n        m.raf = 0;\n      } else {\n        m.raf = requestAnimationFrame(tick);\n      }\n    };\n\n    m.raf = requestAnimationFrame(tick);\n  }, [\n    paint,\n    tune.dropDamping,\n    tune.dropStiffness,\n    tune.thumbDamping,\n    tune.thumbStiffness,\n  ]);\n\n  useEffect(() => {\n    motion.current.target = isChecked ? config.on : config.off;\n    run();\n  }, [isChecked, config.on, config.off, run]);\n\n  useEffect(() => {\n    paint();\n    const m = motion.current;\n    return () => {\n      if (m.raf) {\n        cancelAnimationFrame(m.raf);\n        m.raf = 0;\n      }\n    };\n  }, [paint]);\n\n  const handlePointerDown = (e: PointerEvent<HTMLButtonElement>) => {\n    if (disabled) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    press.current = {\n      startX: e.clientX,\n      from: motion.current.x,\n      moved: false,\n    };\n  };\n\n  const handlePointerMove = (e: PointerEvent<HTMLButtonElement>) => {\n    const p = press.current;\n    if (!p || disabled) return;\n    const by = e.clientX - p.startX;\n    if (Math.abs(by) > 3) p.moved = true;\n    if (!p.moved) return;\n\n    const m = motion.current;\n    m.dragging = true;\n    m.x = Math.max(config.off, Math.min(config.on, p.from + by));\n    m.v = 0;\n    run();\n  };\n\n  const handlePointerUp = () => {\n    const p = press.current;\n    press.current = null;\n    if (!p || disabled) return;\n\n    const m = motion.current;\n    m.dragging = false;\n\n    const nextState = p.moved ? m.x > (config.off + config.on) / 2 : !isChecked;\n    m.target = nextState ? config.on : config.off;\n    run();\n\n    if (!isControlled) {\n      setInternalChecked(nextState);\n    }\n    onChange?.(nextState);\n  };\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n    if (disabled) return;\n    if (e.key === \" \" || e.key === \"Enter\") {\n      e.preventDefault();\n      const nextState = !isChecked;\n      motion.current.target = nextState ? config.on : config.off;\n      run();\n      if (!isControlled) {\n        setInternalChecked(nextState);\n      }\n      onChange?.(nextState);\n    }\n  };\n\n  return (\n    <button\n      id={toggleId}\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={isChecked}\n      aria-label={label}\n      disabled={disabled}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      onPointerCancel={handlePointerUp}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"group relative shrink-0 cursor-pointer select-none touch-none rounded-full border outline-none transition-all duration-300 ease-out focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-zinc-500 dark:focus-visible:outline-white/60 active:scale-95 motion-reduce:transition-none motion-reduce:active:scale-100 disabled:pointer-events-none disabled:opacity-40\",\n        isChecked ? colorTheme.trackOn : colorTheme.trackOff,\n        className,\n      )}\n      style={{\n        width: config.trackWidth,\n        height: config.trackHeight,\n      }}\n    >\n      <span\n        ref={auraRef}\n        className={cn(\n          \"pointer-events-none absolute -inset-1 rounded-full blur-md transition-opacity duration-300 motion-reduce:transition-none\",\n          colorTheme.aura,\n        )}\n        style={{ opacity: isChecked ? 1 : 0 }}\n        aria-hidden=\"true\"\n      />\n\n      <svg\n        width=\"0\"\n        height=\"0\"\n        className=\"pointer-events-none absolute\"\n        aria-hidden=\"true\"\n      >\n        <defs>\n          <filter id={filterId} colorInterpolationFilters=\"sRGB\">\n            <feGaussianBlur\n              in=\"SourceGraphic\"\n              stdDeviation={config.blur}\n              result=\"blur\"\n            />\n            <feColorMatrix\n              in=\"blur\"\n              type=\"matrix\"\n              values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -8\"\n            />\n          </filter>\n        </defs>\n      </svg>\n\n      <span\n        className=\"pointer-events-none absolute inset-0 overflow-visible\"\n        style={{ filter: `url(#${filterId})` }}\n      >\n        <span\n          ref={dropRef}\n          className={cn(\n            \"absolute left-0 rounded-full transition-colors duration-300\",\n            isChecked ? colorTheme.activeBlob : colorTheme.inactiveBlob,\n          )}\n          style={{\n            width: config.drop,\n            height: config.drop,\n            top: (config.trackHeight - config.drop) / 2,\n          }}\n        />\n        <span\n          ref={thumbRef}\n          className={cn(\n            \"absolute left-0 rounded-full shadow-xs transition-colors duration-300\",\n            isChecked ? colorTheme.activeBlob : colorTheme.inactiveBlob,\n          )}\n          style={{\n            width: config.thumb,\n            height: config.thumb,\n            top: (config.trackHeight - config.thumb) / 2,\n          }}\n        />\n      </span>\n    </button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/liquid-toggle.tsx"
    }
  ]
}
