{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mac-switch",
  "type": "registry:ui",
  "title": "Mac Switch",
  "description": "Apple-inspired macOS liquid glass switch with refractive lip bezel optics, spring-loaded tactile thumb kinematics, and customizable color themes.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/mac-switch.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useId, useState } from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  mix,\n  type MotionValue,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MacSwitchColor =\n  | \"green\"\n  | \"blue\"\n  | \"purple\"\n  | \"orange\"\n  | \"pink\"\n  | \"amber\"\n  | \"monochrome\";\n\nexport type MacSwitchSize = \"sm\" | \"md\" | \"lg\";\nexport type MacSwitchMaterial = \"liquid\" | \"frosted\" | \"clear\";\n\nexport interface SwitchFilterProps {\n  id?: string;\n  blur?: MotionValue<number> | number;\n  scaleRatio?: MotionValue<number> | number;\n  specularOpacity?: MotionValue<number> | number;\n  specularSaturation?: MotionValue<number> | number;\n  width?: number;\n  height?: number;\n  radius?: number;\n  bezelWidth?: number;\n}\n\nconst SWITCH_COLORS: Record<\n  MacSwitchColor,\n  { fill: string; dot: string; label: string; glow: string }\n> = {\n  green: {\n    fill: \"#34C759\",\n    dot: \"bg-emerald-500\",\n    label: \"Green\",\n    glow: \"rgba(52, 199, 89, 0.4)\",\n  },\n  blue: {\n    fill: \"#007AFF\",\n    dot: \"bg-blue-500\",\n    label: \"Blue\",\n    glow: \"rgba(0, 122, 255, 0.4)\",\n  },\n  purple: {\n    fill: \"#AF52DE\",\n    dot: \"bg-purple-500\",\n    label: \"Purple\",\n    glow: \"rgba(175, 82, 222, 0.4)\",\n  },\n  orange: {\n    fill: \"#FF9500\",\n    dot: \"bg-orange-500\",\n    label: \"Orange\",\n    glow: \"rgba(255, 149, 0, 0.4)\",\n  },\n  pink: {\n    fill: \"#FF2D55\",\n    dot: \"bg-pink-500\",\n    label: \"Pink\",\n    glow: \"rgba(255, 45, 85, 0.4)\",\n  },\n  amber: {\n    fill: \"#FFCC00\",\n    dot: \"bg-amber-400\",\n    label: \"Amber\",\n    glow: \"rgba(255, 204, 0, 0.4)\",\n  },\n  monochrome: {\n    fill: \"#8E8E93\",\n    dot: \"bg-zinc-300\",\n    label: \"Graphite\",\n    glow: \"rgba(142, 142, 147, 0.3)\",\n  },\n};\n\nconst SWITCH_SIZES: Record<\n  MacSwitchSize,\n  {\n    sliderWidth: number;\n    sliderHeight: number;\n    thumbWidth: number;\n    thumbHeight: number;\n    bezelWidth: number;\n  }\n> = {\n  sm: {\n    sliderWidth: 120,\n    sliderHeight: 50,\n    thumbWidth: 110,\n    thumbHeight: 69,\n    bezelWidth: 14,\n  },\n  md: {\n    sliderWidth: 160,\n    sliderHeight: 67,\n    thumbWidth: 146,\n    thumbHeight: 92,\n    bezelWidth: 19,\n  },\n  lg: {\n    sliderWidth: 200,\n    sliderHeight: 84,\n    thumbWidth: 182,\n    thumbHeight: 115,\n    bezelWidth: 24,\n  },\n};\n\nfunction isMotionValue(val: unknown): val is MotionValue<number> {\n  return typeof val === \"object\" && val !== null && \"get\" in val && \"on\" in val;\n}\n\nfunction getNumericValue(\n  val?: MotionValue<number> | number,\n  fallback = 0,\n): number {\n  if (val === undefined) return fallback;\n  if (isMotionValue(val)) return Number(val.get()) || fallback;\n  return Number(val) || fallback;\n}\n\nexport const SwitchFilter: React.FC<SwitchFilterProps> = ({\n  id = \"thumb-filter-switch\",\n  blur,\n  scaleRatio,\n  specularOpacity,\n  specularSaturation,\n  width = 146,\n  height = 92,\n  radius = 46,\n  bezelWidth = 19,\n}) => {\n  const feImageRef = useRef<SVGFEImageElement>(null);\n  const blurRef = useRef<SVGFEGaussianBlurElement>(null);\n  const dispRef = useRef<SVGFEDisplacementMapElement>(null);\n  const specRef = useRef<SVGFESpecularLightingElement>(null);\n  const satRef = useRef<SVGFEColorMatrixElement>(null);\n\n  useEffect(() => {\n    if (typeof document === \"undefined\") return;\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = width;\n    canvas.height = height;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const imgData = ctx.createImageData(width, height);\n    const data = imgData.data;\n    let pos = 0;\n\n    for (let y = 0; y < height; y++) {\n      for (let x = 0; x < width; x++) {\n        const segX = Math.max(radius, Math.min(width - radius, x));\n        const segY = height / 2;\n        const dx = x - segX;\n        const dy = y - segY;\n        const dist = Math.sqrt(dx * dx + dy * dy);\n        const distEdge = radius - dist;\n\n        if (distEdge <= 0 || distEdge >= bezelWidth) {\n          data[pos++] = 128;\n          data[pos++] = 128;\n          data[pos++] = 255;\n          data[pos++] = 255;\n        } else {\n          const u = distEdge / bezelWidth;\n          const slope = Math.sin(u * Math.PI) * (1 - 0.25 * u);\n          const dirX = dist > 0.001 ? dx / dist : 0;\n          const dirY = dist > 0.001 ? dy / dist : 0;\n          const nx = -dirX * slope * 0.9;\n          const ny = -dirY * slope * 0.9;\n          const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));\n\n          data[pos++] = Math.round(128 + 127 * nx);\n          data[pos++] = Math.round(128 + 127 * ny);\n          data[pos++] = Math.round(128 + 127 * nz);\n          data[pos++] = 255;\n        }\n      }\n    }\n\n    ctx.putImageData(imgData, 0, 0);\n    const dataUrl = canvas.toDataURL(\"image/png\");\n    feImageRef.current?.setAttribute(\"href\", dataUrl);\n  }, [width, height, radius, bezelWidth]);\n\n  useEffect(() => {\n    const unsubBlur = isMotionValue(blur)\n      ? blur.on(\"change\", (v) => {\n          blurRef.current?.setAttribute(\"stdDeviation\", String(v));\n        })\n      : undefined;\n\n    const unsubDisp = isMotionValue(scaleRatio)\n      ? scaleRatio.on(\"change\", (v) => {\n          dispRef.current?.setAttribute(\"scale\", String(v * 24));\n        })\n      : undefined;\n\n    const unsubSpec = isMotionValue(specularOpacity)\n      ? specularOpacity.on(\"change\", (v) => {\n          specRef.current?.setAttribute(\"specularConstant\", String(v * 1.5));\n        })\n      : undefined;\n\n    const unsubSat = isMotionValue(specularSaturation)\n      ? specularSaturation.on(\"change\", (v) => {\n          satRef.current?.setAttribute(\"values\", String(1 + v * 0.15));\n        })\n      : undefined;\n\n    const currentBlur = getNumericValue(blur, 0.2);\n    const currentScale = getNumericValue(scaleRatio, 0.4);\n    const currentSpec = getNumericValue(specularOpacity, 0.5);\n    const currentSat = getNumericValue(specularSaturation, 6);\n\n    blurRef.current?.setAttribute(\"stdDeviation\", String(currentBlur));\n    dispRef.current?.setAttribute(\"scale\", String(currentScale * 24));\n    specRef.current?.setAttribute(\n      \"specularConstant\",\n      String(currentSpec * 1.5),\n    );\n    satRef.current?.setAttribute(\"values\", String(1 + currentSat * 0.15));\n\n    return () => {\n      unsubBlur?.();\n      unsubDisp?.();\n      unsubSpec?.();\n      unsubSat?.();\n    };\n  }, [blur, scaleRatio, specularOpacity, specularSaturation]);\n\n  const initialBlur = getNumericValue(blur, 0.2);\n  const initialDisp = getNumericValue(scaleRatio, 0.4) * 24;\n  const initialSpec = getNumericValue(specularOpacity, 0.5) * 1.5;\n  const initialSat = 1 + getNumericValue(specularSaturation, 6) * 0.15;\n\n  return (\n    <svg\n      width=\"0\"\n      height=\"0\"\n      className=\"pointer-events-none absolute -z-10 opacity-0\"\n      aria-hidden=\"true\"\n    >\n      <defs>\n        <filter\n          id={id}\n          x=\"-20%\"\n          y=\"-20%\"\n          width=\"140%\"\n          height=\"140%\"\n          colorInterpolationFilters=\"sRGB\"\n        >\n          <feImage\n            ref={feImageRef}\n            result=\"normalMap\"\n            x=\"0\"\n            y=\"0\"\n            width={width}\n            height={height}\n            preserveAspectRatio=\"none\"\n          />\n          <feGaussianBlur\n            ref={blurRef}\n            in=\"SourceGraphic\"\n            stdDeviation={initialBlur}\n            result=\"blurred\"\n          />\n          <feDisplacementMap\n            ref={dispRef}\n            in=\"blurred\"\n            in2=\"normalMap\"\n            scale={initialDisp}\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n            result=\"refracted\"\n          />\n          <feSpecularLighting\n            ref={specRef}\n            in=\"normalMap\"\n            surfaceScale=\"2\"\n            specularConstant={initialSpec}\n            specularExponent=\"24\"\n            result=\"specular\"\n            lightingColor=\"#ffffff\"\n          >\n            <feDistantLight azimuth=\"235\" elevation=\"55\" />\n          </feSpecularLighting>\n          <feColorMatrix\n            ref={satRef}\n            in=\"refracted\"\n            type=\"saturate\"\n            values={String(initialSat)}\n            result=\"saturated\"\n          />\n          <feBlend in=\"saturated\" in2=\"specular\" mode=\"screen\" />\n        </filter>\n      </defs>\n    </svg>\n  );\n};\n\nexport const Filter = SwitchFilter;\n\ntype SpringConfig = Parameters<typeof useSpring>[1];\n\nfunction useReducibleSpring(\n  source: MotionValue<number>,\n  config: SpringConfig,\n  instant: boolean,\n): MotionValue<number> {\n  const spring = useSpring(source, config);\n  return instant ? source : spring;\n}\n\nexport interface MacSwitchProps {\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  size?: MacSwitchSize;\n  color?: MacSwitchColor;\n  material?: MacSwitchMaterial;\n  forceActive?: boolean;\n  specularOpacity?: number;\n  specularSaturation?: number;\n  refractionLevel?: number;\n  blurLevel?: number;\n  className?: string;\n  label?: string;\n}\n\nexport const MacSwitch: React.FC<MacSwitchProps> = ({\n  checked: controlledChecked,\n  defaultChecked = false,\n  onChange,\n  disabled = false,\n  size = \"md\",\n  color = \"green\",\n  material = \"liquid\",\n  forceActive = false,\n  specularOpacity: specularOpacityProp = 0.5,\n  specularSaturation: specularSaturationProp = 6,\n  refractionLevel = 1.0,\n  blurLevel = 0.2,\n  className,\n  label = \"Toggle switch\",\n}) => {\n  const generatedId = useId();\n  const reduceMotion = useReducedMotion() ?? false;\n  const filterId = `mac-switch-filter-${generatedId.replace(/:/g, \"\")}`;\n\n  const currentTheme = SWITCH_COLORS[color] ?? SWITCH_COLORS.green;\n  const currentSize = SWITCH_SIZES[size] ?? SWITCH_SIZES.md;\n  const { sliderWidth, sliderHeight, thumbWidth, thumbHeight, bezelWidth } =\n    currentSize;\n\n  const thumbRadius = thumbHeight / 2;\n  const THUMB_REST_SCALE = 0.65;\n  const THUMB_ACTIVE_SCALE = 0.9;\n  const THUMB_REST_OFFSET = ((1 - THUMB_REST_SCALE) * thumbWidth) / 2;\n  const TRAVEL =\n    sliderWidth - sliderHeight - (thumbWidth - thumbHeight) * THUMB_REST_SCALE;\n  const thumbMarginLeft =\n    -THUMB_REST_OFFSET + (sliderHeight - thumbHeight * THUMB_REST_SCALE) / 2;\n\n  const isControlled = controlledChecked !== undefined;\n  const [uncontrolledChecked, setUncontrolledChecked] =\n    useState(defaultChecked);\n  const isChecked = isControlled ? controlledChecked : uncontrolledChecked;\n\n  const checked = useMotionValue(isChecked ? 1 : 0);\n  const pointerDown = useMotionValue(0);\n  const forceActiveMotion = useMotionValue(forceActive ? 1 : 0);\n  const xDragRatio = useMotionValue(isChecked ? 1 : 0);\n  const initialPointerX = useMotionValue(0);\n\n  const isDraggingRef = useRef(false);\n\n  useEffect(() => {\n    forceActiveMotion.set(forceActive ? 1 : 0);\n  }, [forceActive, forceActiveMotion]);\n\n  useEffect(() => {\n    if (controlledChecked !== undefined) {\n      const nextVal = controlledChecked ? 1 : 0;\n      checked.set(nextVal);\n      xDragRatio.set(nextVal);\n    }\n  }, [controlledChecked, checked, xDragRatio]);\n\n  const active = useTransform((): number =>\n    forceActiveMotion.get() > 0.5 || pointerDown.get() > 0.5 ? 1 : 0,\n  );\n\n  const blur = useMotionValue(blurLevel);\n  const specularOpacity = useMotionValue(specularOpacityProp);\n  const specularSaturation = useMotionValue(specularSaturationProp);\n  const refractionBase = useMotionValue(refractionLevel);\n\n  useEffect(() => {\n    blur.set(blurLevel);\n  }, [blurLevel, blur]);\n\n  useEffect(() => {\n    specularOpacity.set(specularOpacityProp);\n  }, [specularOpacityProp, specularOpacity]);\n\n  useEffect(() => {\n    specularSaturation.set(specularSaturationProp);\n  }, [specularSaturationProp, specularSaturation]);\n\n  useEffect(() => {\n    refractionBase.set(refractionLevel);\n  }, [refractionLevel, refractionBase]);\n\n  const xRatio = useReducibleSpring(\n    useTransform((): number => {\n      const c = checked.get();\n      const dragRatio = xDragRatio.get();\n      if (pointerDown.get() > 0.5) {\n        return dragRatio;\n      }\n      return c > 0.5 ? 1 : 0;\n    }),\n    { damping: 42, stiffness: 520, mass: 0.8 },\n    reduceMotion,\n  );\n\n  const backgroundOpacity = useReducibleSpring(\n    useTransform(active, (v) => 1 - 0.9 * v),\n    { damping: 42, stiffness: 600, mass: 0.8 },\n    reduceMotion,\n  );\n\n  const thumbScale = useReducibleSpring(\n    useTransform(\n      active,\n      (v) => THUMB_REST_SCALE + (THUMB_ACTIVE_SCALE - THUMB_REST_SCALE) * v,\n    ),\n    { damping: 42, stiffness: 600, mass: 0.8 },\n    reduceMotion,\n  );\n\n  const scaleRatio = useReducibleSpring(\n    useTransform(() => (0.4 + 0.5 * active.get()) * refractionBase.get()),\n    { damping: 42, stiffness: 600, mass: 0.8 },\n    reduceMotion,\n  );\n\n  const considerChecked = useTransform((): number => {\n    const x = xDragRatio.get();\n    const c = checked.get();\n    return pointerDown.get() > 0.5 ? (x > 0.5 ? 1 : 0) : c > 0.5 ? 1 : 0;\n  });\n\n  const backgroundColor = useTransform(\n    useReducibleSpring(\n      considerChecked,\n      { damping: 42, stiffness: 520 },\n      reduceMotion,\n    ),\n    mix(\"#94949F55\", currentTheme.fill),\n  );\n\n  const thumbX = useTransform(xRatio, (r) => r * TRAVEL);\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled) return;\n    isDraggingRef.current = true;\n    pointerDown.set(1);\n    initialPointerX.set(e.clientX);\n    xDragRatio.set(checked.get());\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n  };\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!isDraggingRef.current || disabled) return;\n    const baseRatio = checked.get();\n    const displacementX = e.clientX - initialPointerX.get();\n    const ratio = baseRatio + displacementX / TRAVEL;\n    const overflow = ratio < 0 ? -ratio : ratio > 1 ? ratio - 1 : 0;\n    const overflowSign = ratio < 0 ? -1 : 1;\n    const dampedOverflow = (overflowSign * overflow) / 22;\n    xDragRatio.set(Math.min(1, Math.max(0, ratio)) + dampedOverflow);\n  };\n\n  const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!isDraggingRef.current) return;\n    isDraggingRef.current = false;\n    pointerDown.set(0);\n\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n\n    const displacement = Math.abs(e.clientX - initialPointerX.get());\n    let nextChecked: boolean;\n    if (displacement < 4) {\n      nextChecked = checked.get() < 0.5;\n    } else {\n      nextChecked = xDragRatio.get() > 0.5;\n    }\n\n    const nextVal = nextChecked ? 1 : 0;\n    checked.set(nextVal);\n    xDragRatio.set(nextVal);\n    if (!isControlled) {\n      setUncontrolledChecked(nextChecked);\n    }\n    onChange?.(nextChecked);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (disabled) return;\n    if (e.key === \" \" || e.key === \"Enter\") {\n      e.preventDefault();\n      const nextChecked = checked.get() < 0.5;\n      const nextVal = nextChecked ? 1 : 0;\n      checked.set(nextVal);\n      xDragRatio.set(nextVal);\n      if (!isControlled) {\n        setUncontrolledChecked(nextChecked);\n      }\n      onChange?.(nextChecked);\n    } else if (e.key === \"ArrowLeft\") {\n      e.preventDefault();\n      checked.set(0);\n      xDragRatio.set(0);\n      if (!isControlled) {\n        setUncontrolledChecked(false);\n      }\n      onChange?.(false);\n    } else if (e.key === \"ArrowRight\") {\n      e.preventDefault();\n      checked.set(1);\n      xDragRatio.set(1);\n      if (!isControlled) {\n        setUncontrolledChecked(true);\n      }\n      onChange?.(true);\n    }\n  };\n\n  const backdropStyle =\n    material === \"liquid\"\n      ? `url(#${filterId}) blur(14px)`\n      : material === \"frosted\"\n        ? \"blur(24px) saturate(180%)\"\n        : \"blur(8px) saturate(120%)\";\n\n  return (\n    <div\n      className={cn(\n        \"relative flex items-center justify-center select-none py-4\",\n        disabled && \"opacity-50 pointer-events-none\",\n        className,\n      )}\n    >\n      {material === \"liquid\" && (\n        <SwitchFilter\n          id={filterId}\n          blur={blur}\n          scaleRatio={scaleRatio}\n          specularOpacity={specularOpacity}\n          specularSaturation={specularSaturation}\n          width={thumbWidth}\n          height={thumbHeight}\n          radius={thumbRadius}\n          bezelWidth={bezelWidth}\n        />\n      )}\n\n      <motion.div\n        role=\"switch\"\n        aria-checked={isChecked}\n        aria-label={label}\n        aria-disabled={disabled || undefined}\n        tabIndex={disabled ? -1 : 0}\n        onKeyDown={handleKeyDown}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerUp}\n        className=\"relative inline-block touch-none select-none cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50\"\n        style={{\n          width: sliderWidth,\n          height: sliderHeight,\n          backgroundColor: backgroundColor,\n          borderRadius: sliderHeight / 2,\n          boxShadow: \"inset 0 1px 3px rgba(0, 0, 0, 0.15)\",\n        }}\n      >\n        <motion.div\n          className=\"absolute pointer-events-none\"\n          style={{\n            height: thumbHeight,\n            width: thumbWidth,\n            marginLeft: thumbMarginLeft,\n            x: thumbX,\n            y: \"-50%\",\n            borderRadius: thumbRadius,\n            top: sliderHeight / 2,\n            backdropFilter: backdropStyle,\n            WebkitBackdropFilter: backdropStyle,\n            scale: thumbScale,\n            willChange: \"transform\",\n            backgroundColor: useTransform(\n              backgroundOpacity,\n              (op) => `rgba(255, 255, 255, ${op})`,\n            ),\n            boxShadow: useTransform(() => {\n              const isPressed = pointerDown.get() > 0.5;\n              return (\n                \"0 4px 22px rgba(0, 0, 0, 0.14), inset 0 1px 1.5px rgba(255, 255, 255, 0.85)\" +\n                (isPressed\n                  ? \", inset 2px 7px 24px rgba(0,0,0,0.09), inset -2px -7px 24px rgba(255,255,255,0.09)\"\n                  : \"\")\n              );\n            }),\n            border: \"0.5px solid rgba(255, 255, 255, 0.4)\",\n          }}\n        >\n          <div\n            className=\"pointer-events-none absolute inset-x-0 top-0 h-1/2 opacity-35\"\n            style={{\n              borderRadius: `${thumbRadius}px ${thumbRadius}px 0 0`,\n              background:\n                \"linear-gradient(180deg, rgba(255,255,255,0.65) 0%, rgba(255,255,255,0.05) 100%)\",\n            }}\n          />\n        </motion.div>\n      </motion.div>\n    </div>\n  );\n};\n\nexport const Switch = MacSwitch;\n",
      "type": "registry:ui",
      "target": "@ui/mac-switch.tsx"
    }
  ]
}
