{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-switch",
  "title": "Glass Switch",
  "description": "An iOS-style liquid-glass switch: the thumb rests as an opaque platter and lifts into a clear warping lens on press, with velocity-driven gel deformation, rubber-band overdrag, and overshoot travel tweens.",
  "registryDependencies": [
    "https://loreglasses.com/r/glass.json"
  ],
  "files": [
    {
      "path": "registry/default/glass-switch/glass-switch.tsx",
      "content": "\"use client\";\n\nimport { Glass, useGlassDark } from \"@/components/ui/glass\";\nimport {\n  glassEase,\n  MotionValue,\n  PRESS_DURATION,\n  prefersReducedMotion,\n  RELEASE_DURATION,\n  rubberBand,\n  SpringDriver,\n  TRAVEL_DURATION,\n  tween,\n} from \"@/components/ui/glass-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nconst TRACK_W = 74;\nconst TRACK_H = 28;\nconst THUMB_W = Math.round(0.6 * TRACK_W);\nconst THUMB_H = TRACK_H - 6;\nconst INSET = 3;\nconst TRAVEL = TRACK_W - THUMB_W - 6;\nconst TRACK_R = TRACK_H / 2;\nconst LENS_HALF_W = THUMB_W / 2;\nconst LENS_HALF_H = THUMB_H / 2;\nconst LENS_R = THUMB_H / 2;\nconst FILL_H = Math.round(0.75 * TRACK_H);\nconst RUBBER_OVERSHOOT = TRACK_W * 0.1;\nconst RUBBER_DAMPENING = 30;\nconst BLEED =\n  Math.ceil(0.5 * Math.max(LENS_HALF_W, LENS_HALF_H) + RUBBER_OVERSHOOT) + 2;\nconst HOLD_MS = 200;\nconst DRAG_THRESHOLD = 3;\nconst TAP_COLLAPSE_MS = 330;\nconst PRESS_GROW = 1.5;\nconst TINT_BLUR = 4;\nconst REST_TINT = 1;\nconst DEFORM_STIFFNESS = 180;\nconst DEFORM_DAMPING = 14;\nconst DEFORM_CAP = 0.35;\nconst DEFORM_GAIN = 0.012;\nconst DEFORM_EXP = 0.75;\nconst HOLD_BOOST = 0.175;\n\nconst PRIMARY = \"#0a84ff\";\nconst BG4_LIGHT = \"#e1dfdf\";\nconst BG4_DARK = \"#2a2828\";\n\ninterface GlassSwitchProps {\n  ariaLabel?: string;\n  checked?: boolean;\n  className?: string;\n  defaultChecked?: boolean;\n  disabled?: boolean;\n  name?: string;\n  onCheckedChange?: (checked: boolean) => void;\n  value?: string;\n}\n\nfunction GlassSwitch({\n  checked,\n  defaultChecked = false,\n  onCheckedChange,\n  disabled = false,\n  name,\n  value,\n  ariaLabel,\n  className,\n}: GlassSwitchProps) {\n  const dark = useGlassDark();\n  const [internal, setInternal] = useState(defaultChecked);\n  const isOn = checked ?? internal;\n  const isOnRef = useRef(isOn);\n  isOnRef.current = isOn;\n\n  const markerRef = useRef<HTMLDivElement | null>(null);\n  const hitRef = useRef<HTMLDivElement | null>(null);\n  const trackRef = useRef<HTMLDivElement | null>(null);\n  const fillRef = useRef<HTMLDivElement | null>(null);\n  const brightnessRef = useRef<HTMLDivElement | null>(null);\n  const tintRef = useRef<HTMLDivElement | null>(null);\n  const shadowRef = useRef<HTMLDivElement | null>(null);\n  const inputRef = useRef<HTMLInputElement | null>(null);\n\n  const x = useRef(new MotionValue(isOn ? TRAVEL : 0));\n  const lensHalfW = useRef(new MotionValue(LENS_HALF_W));\n  const lensHalfH = useRef(new MotionValue(LENS_HALF_H));\n  const lensR = useRef(new MotionValue(LENS_R));\n  const tintOpacity = useRef(new MotionValue(REST_TINT));\n  const tintBlur = useRef(new MotionValue(TINT_BLUR));\n  const fillScaleX = useRef(new MotionValue(0.85));\n  const fillScaleY = useRef(new MotionValue(0.525));\n  const shadowOpacity = useRef(new MotionValue(0));\n  const deform = useRef(new MotionValue(0));\n  const heldRef = useRef(false);\n  const frame = useRef(0);\n\n  const deformDriver = useRef<SpringDriver | null>(null);\n  if (!deformDriver.current) {\n    deformDriver.current = new SpringDriver(\n      deform.current,\n      { stiffness: DEFORM_STIFFNESS, damping: DEFORM_DAMPING },\n      () => {\n        const v = Math.abs(x.current.getVelocity());\n        const fromVelocity = DEFORM_GAIN * v ** DEFORM_EXP;\n        const boost = heldRef.current ? HOLD_BOOST : 0;\n        return Math.min(DEFORM_CAP, Math.max(fromVelocity, boost));\n      },\n      () => !heldRef.current && Math.abs(x.current.getVelocity()) < 0.005\n    );\n  }\n  const startDeform = useCallback(() => {\n    if (!prefersReducedMotion()) {\n      deformDriver.current?.start();\n    }\n  }, []);\n\n  const apply = useCallback(() => {\n    frame.current = 0;\n    const marker = markerRef.current;\n    if (!marker) {\n      return;\n    }\n    const xv = Math.min(\n      Math.max(x.current.get(), -RUBBER_OVERSHOOT),\n      TRAVEL + RUBBER_OVERSHOOT\n    );\n    const q = deform.current.get();\n    const w = 2 * lensHalfW.current.get() * (1 - 0.2 * q);\n    const h = 2 * lensHalfH.current.get() * (1 + 0.4 * q);\n    const r = lensR.current.get();\n    const centerX = BLEED + INSET + THUMB_W / 2 + xv;\n    const centerY = BLEED + TRACK_H / 2;\n    marker.style.width = `${w}px`;\n    marker.style.height = `${h}px`;\n    marker.style.borderRadius = `${r}px`;\n    marker.style.left = `${centerX - w / 2}px`;\n    marker.style.top = `${centerY - h / 2}px`;\n    if (tintRef.current) {\n      const t = tintOpacity.current.get();\n      tintRef.current.style.background = `rgba(255,255,255,${t})`;\n      const b = tintBlur.current.get();\n      const filter = b > 0.05 ? `blur(${b}px)` : \"none\";\n      tintRef.current.style.backdropFilter = filter;\n      tintRef.current.style.setProperty(\"-webkit-backdrop-filter\", filter);\n    }\n    if (shadowRef.current) {\n      shadowRef.current.style.opacity = String(shadowOpacity.current.get());\n    }\n    if (hitRef.current) {\n      hitRef.current.style.transform = `translateX(${xv}px)`;\n    }\n    const progress = TRAVEL > 0 ? Math.max(0, Math.min(1, xv / TRAVEL)) : 0;\n    const fillBg = `color-mix(in srgb, ${dark ? BG4_DARK : BG4_LIGHT}, ${PRIMARY} ${progress * 100}%)`;\n    if (trackRef.current) {\n      trackRef.current.style.background = fillBg;\n    }\n    if (fillRef.current) {\n      fillRef.current.style.background = fillBg;\n      fillRef.current.style.transform = `scale(${fillScaleX.current.get()}, ${fillScaleY.current.get()})`;\n    }\n  }, [dark]);\n\n  const schedule = useCallback(() => {\n    if (frame.current === 0) {\n      frame.current = requestAnimationFrame(apply);\n    }\n  }, [apply]);\n\n  useEffect(() => {\n    const values = [\n      x.current,\n      lensHalfW.current,\n      lensHalfH.current,\n      lensR.current,\n      tintOpacity.current,\n      tintBlur.current,\n      fillScaleX.current,\n      fillScaleY.current,\n      shadowOpacity.current,\n      deform.current,\n    ];\n    const subs = values.map((v) => v.on(schedule));\n    apply();\n    return () => {\n      for (const off of subs) {\n        off();\n      }\n      cancelAnimationFrame(frame.current);\n      deformDriver.current?.stop();\n    };\n  }, [apply, schedule]);\n\n  const press = useCallback(() => {\n    const d = prefersReducedMotion() ? 0 : PRESS_DURATION;\n    tween(lensHalfW.current, LENS_HALF_W * PRESS_GROW, d, glassEase);\n    tween(lensHalfH.current, LENS_HALF_H * PRESS_GROW, d, glassEase);\n    tween(lensR.current, LENS_R * PRESS_GROW, d, glassEase);\n    tween(tintOpacity.current, 0, d, glassEase);\n    tween(tintBlur.current, 0, d, glassEase);\n    tween(fillScaleX.current, 0.95, d, glassEase);\n    tween(fillScaleY.current, 0.975, d, glassEase);\n    tween(shadowOpacity.current, 1, d, glassEase);\n  }, []);\n\n  const release = useCallback(() => {\n    const d = prefersReducedMotion() ? 0 : RELEASE_DURATION;\n    tween(lensHalfW.current, LENS_HALF_W, d, glassEase);\n    tween(lensHalfH.current, LENS_HALF_H, d, glassEase);\n    tween(lensR.current, LENS_R, d, glassEase);\n    tween(tintOpacity.current, REST_TINT, d, glassEase);\n    tween(tintBlur.current, TINT_BLUR, d, glassEase);\n    tween(fillScaleX.current, 0.85, d, glassEase);\n    tween(fillScaleY.current, 0.525, d, glassEase);\n    tween(shadowOpacity.current, 0, d, glassEase);\n  }, []);\n\n  const commit = useCallback(\n    (next: boolean) => {\n      if (next !== isOnRef.current) {\n        if (checked === undefined) {\n          setInternal(next);\n        }\n        onCheckedChange?.(next);\n      }\n    },\n    [checked, onCheckedChange]\n  );\n\n  const travelTo = useCallback(\n    (on: boolean, onComplete?: () => void) => {\n      const d = prefersReducedMotion() ? 0 : TRAVEL_DURATION;\n      tween(x.current, on ? TRAVEL : 0, d, glassEase, onComplete);\n      startDeform();\n    },\n    [startDeform]\n  );\n\n  const stateRef = useRef<\"idle\" | \"pending\" | \"hold\" | \"tap\">(\"idle\");\n  const holdTimer = useRef(0);\n  const tapTimer = useRef(0);\n  const activePointer = useRef<number | null>(null);\n  const dragStartX = useRef(0);\n  const grabX = useRef(0);\n  const dragging = useRef(false);\n  const suppressClick = useRef(false);\n  const alive = useRef(true);\n\n  useEffect(\n    () => () => {\n      alive.current = false;\n      clearTimeout(holdTimer.current);\n      clearTimeout(tapTimer.current);\n    },\n    []\n  );\n\n  useEffect(() => {\n    if (!heldRef.current && stateRef.current !== \"tap\") {\n      const target = isOn ? TRAVEL : 0;\n      if (Math.abs(x.current.get() - target) > 0.5) {\n        travelTo(isOn);\n      }\n    }\n  }, [isOn, travelTo]);\n\n  const tap = useCallback(\n    (next: boolean) => {\n      if (suppressClick.current) {\n        return;\n      }\n      commit(next);\n      if (stateRef.current === \"idle\") {\n        stateRef.current = \"tap\";\n        press();\n        heldRef.current = true;\n        startDeform();\n        clearTimeout(tapTimer.current);\n        tapTimer.current = window.setTimeout(() => {\n          heldRef.current = false;\n          release();\n        }, TAP_COLLAPSE_MS);\n        travelTo(next, () => {\n          if (alive.current && stateRef.current === \"tap\") {\n            stateRef.current = \"idle\";\n          }\n        });\n      }\n    },\n    [commit, press, release, startDeform, travelTo]\n  );\n\n  const onPointerDown = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (disabled || activePointer.current !== null) {\n        return;\n      }\n      activePointer.current = e.pointerId;\n      e.currentTarget.setPointerCapture(e.pointerId);\n      dragStartX.current = e.clientX;\n      grabX.current = x.current.get();\n      dragging.current = false;\n      suppressClick.current = true;\n      clearTimeout(holdTimer.current);\n      clearTimeout(tapTimer.current);\n      stateRef.current = \"pending\";\n      holdTimer.current = window.setTimeout(() => {\n        if (stateRef.current === \"pending\") {\n          stateRef.current = \"hold\";\n          x.current.stop();\n          press();\n          heldRef.current = true;\n          startDeform();\n        }\n      }, HOLD_MS);\n    },\n    [disabled, press, startDeform]\n  );\n\n  const onPointerMove = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.pointerId !== activePointer.current) {\n        return;\n      }\n      const delta = e.clientX - dragStartX.current;\n      if (!dragging.current) {\n        if (Math.abs(delta) < DRAG_THRESHOLD) {\n          return;\n        }\n        dragging.current = true;\n        x.current.stop();\n        grabX.current = x.current.get();\n        dragStartX.current = e.clientX;\n        clearTimeout(holdTimer.current);\n        heldRef.current = true;\n        startDeform();\n        if (stateRef.current !== \"hold\") {\n          stateRef.current = \"hold\";\n          press();\n        }\n      }\n      let next = grabX.current + (e.clientX - dragStartX.current);\n      if (next < 0) {\n        next = -rubberBand(-next, RUBBER_OVERSHOOT, RUBBER_DAMPENING);\n      } else if (next > TRAVEL) {\n        next =\n          TRAVEL + rubberBand(next - TRAVEL, RUBBER_OVERSHOOT, RUBBER_DAMPENING);\n      }\n      x.current.set(next);\n    },\n    [press, startDeform]\n  );\n\n  const onPointerUp = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.pointerId !== activePointer.current) {\n        return;\n      }\n      activePointer.current = null;\n      clearTimeout(holdTimer.current);\n      if (dragging.current) {\n        stateRef.current = \"idle\";\n        heldRef.current = false;\n        release();\n        const next = Math.max(0, Math.min(TRAVEL, x.current.get())) > TRAVEL / 2;\n        travelTo(next);\n        commit(next);\n        requestAnimationFrame(() => {\n          suppressClick.current = false;\n        });\n      } else if (\n        stateRef.current === \"pending\" ||\n        stateRef.current === \"tap\"\n      ) {\n        stateRef.current = \"tap\";\n        suppressClick.current = false;\n        press();\n        heldRef.current = true;\n        startDeform();\n        clearTimeout(tapTimer.current);\n        tapTimer.current = window.setTimeout(() => {\n          heldRef.current = false;\n          release();\n        }, TAP_COLLAPSE_MS);\n        const next = !isOnRef.current;\n        commit(next);\n        suppressClick.current = true;\n        travelTo(next, () => {\n          if (alive.current && stateRef.current === \"tap\") {\n            stateRef.current = \"idle\";\n          }\n        });\n        requestAnimationFrame(() => {\n          suppressClick.current = false;\n        });\n      } else if (stateRef.current === \"hold\") {\n        stateRef.current = \"idle\";\n        heldRef.current = false;\n        release();\n        travelTo(isOnRef.current);\n        requestAnimationFrame(() => {\n          suppressClick.current = false;\n        });\n      } else {\n        suppressClick.current = false;\n      }\n    },\n    [commit, press, release, startDeform, travelTo]\n  );\n\n  const onPointerCancel = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.pointerId !== activePointer.current) {\n        return;\n      }\n      activePointer.current = null;\n      clearTimeout(holdTimer.current);\n      heldRef.current = false;\n      stateRef.current = \"idle\";\n      release();\n      travelTo(isOnRef.current);\n      requestAnimationFrame(() => {\n        suppressClick.current = false;\n      });\n    },\n    [release, travelTo]\n  );\n\n  const config = dark\n    ? {\n        brightness: 0.12,\n        specularAngle: 45,\n        specularStrength: 1,\n        glowExponent: 1.5,\n        edgeWidth: 2,\n        edgeExponent: 1.5,\n        specularDark: false,\n      }\n    : {\n        brightness: -0.02,\n        specularAngle: 30,\n        specularStrength: 1.5,\n        glowExponent: 2,\n        edgeWidth: 1.5,\n        edgeExponent: 1,\n        specularDark: true,\n      };\n\n  return (\n    <label\n      className={cn(\n        \"relative block cursor-pointer rounded-full has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-40 has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-[#0a84ff] has-[input:focus-visible]:outline-offset-3\",\n        className\n      )}\n      style={{ flexShrink: 0, width: TRACK_W, height: TRACK_H, overflow: \"visible\" }}\n    >\n      <input\n        aria-label={ariaLabel}\n        checked={isOn}\n        className=\"sr-only\"\n        disabled={disabled}\n        name={name}\n        onChange={(e) => tap(e.target.checked)}\n        onClick={(e) => {\n          if (suppressClick.current) {\n            e.preventDefault();\n          }\n        }}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\") {\n            e.preventDefault();\n            tap(!isOn);\n          }\n        }}\n        ref={inputRef}\n        role=\"switch\"\n        type=\"checkbox\"\n        value={value}\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute\"\n        style={{\n          left: 0,\n          top: 0,\n          width: TRACK_W,\n          height: TRACK_H,\n          borderRadius: TRACK_R,\n        }}\n        ref={trackRef}\n      />\n      <Glass\n        chroma={0}\n        depth={2}\n        domeDepth={6}\n        edgeHighlight={0.5}\n        edgeWidth={config.edgeWidth}\n        edgeExponent={config.edgeExponent}\n        glow={0.4}\n        glowExponent={config.glowExponent}\n        glowSpread={0.5}\n        lens={\n          <div className=\"absolute\" data-glass-lens ref={markerRef}>\n            <div\n              className=\"absolute inset-0 rounded-[inherit]\"\n              ref={brightnessRef}\n              style={{\n                background: config.brightness >= 0 ? \"#fff\" : \"#000\",\n                opacity: Math.abs(config.brightness),\n              }}\n            />\n            <div className=\"absolute inset-0 rounded-[inherit]\" ref={tintRef} />\n            <div\n              className=\"absolute inset-0 rounded-[inherit]\"\n              ref={shadowRef}\n              style={{\n                opacity: 0,\n                boxShadow:\n                  \"0 2px 6px rgba(0, 0, 0, 0.16), inset 0 -4px 10px rgba(0, 0, 0, 0.12)\",\n              }}\n            />\n          </div>\n        }\n        resolution={2}\n        reveal\n        scaleX={0.25}\n        scaleY={0.25}\n        specularAngle={config.specularAngle}\n        specularDark={config.specularDark}\n        specularStrength={config.specularStrength}\n        splay={0.4}\n        style={{\n          position: \"absolute\",\n          left: -BLEED,\n          top: -BLEED,\n          width: TRACK_W + 2 * BLEED,\n          height: TRACK_H + 2 * BLEED,\n          overflow: \"visible\",\n          pointerEvents: \"none\",\n        }}\n      >\n        <div\n          className=\"absolute\"\n          style={{\n            left: BLEED,\n            top: BLEED + (TRACK_H - FILL_H) / 2,\n            width: TRACK_W,\n            height: FILL_H,\n            borderRadius: FILL_H / 2,\n            transform: \"scale(0.85, 0.525)\",\n          }}\n          ref={fillRef}\n        />\n      </Glass>\n      <div\n        className=\"absolute touch-none select-none\"\n        onPointerCancel={onPointerCancel}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        ref={hitRef}\n        style={{\n          left: INSET,\n          top: INSET,\n          width: THUMB_W,\n          height: THUMB_H,\n          willChange: \"transform\",\n        }}\n      />\n    </label>\n  );\n}\n\nexport { GlassSwitch };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Import { GlassSwitch } from \"@/components/ui/glass-switch\" and use it as a controlled toggle (checked / onCheckedChange). The thumb rests as a solid platter and lifts into a refracting lens while pressed. Takes no tint prop.",
  "type": "registry:ui"
}