{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realistic-switch",
  "type": "registry:ui",
  "title": "RealisticSwitch",
  "description": "A latching push switch, a raised box that sinks into its socket when pressed, stays down and glows while on, and springs back up on the next press.",
  "files": [
    {
      "path": "components/evil-buttons/realistic-switch.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/realistic-switch.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useState, type CSSProperties } from \"react\"\nimport {\n  motion,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype RealisticSwitchSize = \"sm\" | \"default\" | \"lg\"\ntype RealisticSwitchTone = \"auto\" | \"light\" | \"dark\"\n\ntype RealisticSwitchProps = {\n  checked?: boolean\n  defaultChecked?: boolean\n  onCheckedChange?: (checked: boolean) => void\n  /** Cap color. Any CSS color; its walls, shading, and glow are mixed from it. */\n  color?: string\n  /** Color of the power mark on the cap. */\n  markColor?: string\n  /** Socket finish: follow the theme, or force ceramic white or graphite. */\n  tone?: RealisticSwitchTone\n  /** Light the cap from inside while it is latched on. */\n  illuminated?: boolean\n  /** Play a short mechanical click when it latches or releases. */\n  sound?: boolean\n  size?: RealisticSwitchSize\n  disabled?: boolean\n  className?: string\n  \"aria-label\"?: string\n}\n\n// Everything is sized in em, so one font size scales the whole switch.\nconst FONT_SIZE: Record<RealisticSwitchSize, number> = {\n  sm: 11,\n  default: 15,\n  lg: 19,\n}\n\n// Geometry, in em. The cap is a box standing on its own walls inside a\n// recessed socket; its height above the socket floor is the one thing that\n// moves. Cap and walls slide together on the same transform, and the walls are\n// clipped at the floor, so nothing is resized and nothing snaps to a pixel.\nconst SOCKET = 4.4\nconst CAP = 3.3\nconst GAP = (SOCKET - CAP) / 2\n\n// Cap height in each state: up, dipped under hover, fully pressed, and\n// latched down while on.\nconst HEIGHT = {\n  up: 0.95,\n  hover: 0.82,\n  pressed: 0.1,\n  latched: 0.34,\n  latchedHover: 0.3,\n}\n\n// A touch of bounce, so a released cap settles like a spring without wobbling\n// through the small hover dip.\nconst SPRING = { stiffness: 520, damping: 30, mass: 0.6 }\n\nconst TONES: Record<RealisticSwitchTone, string> = {\n  light:\n    \"[--hi:#ffffff] [--mid:#efeff2] [--lo:#d9d9df] [--well:#c9c9d1] [--well-lo:#aeaeb8] [--floor:rgb(20_20_40/0.35)] [--spill:0.35]\",\n  dark: \"[--hi:#3a3a40] [--mid:#26262b] [--lo:#18181b] [--well:#0d0d10] [--well-lo:#050506] [--floor:rgb(0_0_0/0.8)] [--spill:0.6]\",\n  auto: \"[--hi:#ffffff] [--mid:#efeff2] [--lo:#d9d9df] [--well:#c9c9d1] [--well-lo:#aeaeb8] [--floor:rgb(20_20_40/0.35)] [--spill:0.35] dark:[--hi:#3a3a40] dark:[--mid:#26262b] dark:[--lo:#18181b] dark:[--well:#0d0d10] dark:[--well-lo:#050506] dark:[--floor:rgb(0_0_0/0.8)] dark:[--spill:0.6]\",\n}\n\nconst mix = (amount: number, toward: \"white\" | \"black\") =>\n  `color-mix(in oklab, var(--cap) ${amount}%, ${toward})`\n\nconst em = (value: number) => `${value}em`\n\nlet audio: AudioContext | null = null\n\n// A switch click is a very short, bright burst of noise. Latching is pitched a\n// little higher than releasing, like a real push switch's two stops.\nfunction playClick(on: boolean) {\n  try {\n    audio ??= new AudioContext()\n    const length = Math.floor(audio.sampleRate * 0.03)\n    const buffer = audio.createBuffer(1, length, audio.sampleRate)\n    const data = buffer.getChannelData(0)\n    for (let i = 0; i < length; i++) {\n      data[i] = (Math.random() * 2 - 1) * (1 - i / length) ** 6\n    }\n    const source = audio.createBufferSource()\n    source.buffer = buffer\n    const filter = audio.createBiquadFilter()\n    filter.type = \"bandpass\"\n    filter.frequency.value = on ? 3400 : 2500\n    filter.Q.value = 1.1\n    const gain = audio.createGain()\n    gain.gain.value = 0.7\n    source.connect(filter).connect(gain).connect(audio.destination)\n    source.start()\n  } catch {\n    // No audio available: the switch still works silently.\n  }\n}\n\nfunction RealisticSwitch({\n  checked: checkedProp,\n  defaultChecked = false,\n  onCheckedChange,\n  color = \"#e1261c\",\n  markColor = \"rgb(255 255 255 / 0.92)\",\n  tone = \"auto\",\n  illuminated = true,\n  sound = false,\n  size = \"default\",\n  disabled = false,\n  className,\n  \"aria-label\": ariaLabel = \"Power\",\n}: RealisticSwitchProps) {\n  const [internalChecked, setInternalChecked] = useState(defaultChecked)\n  const isControlled = checkedProp !== undefined\n  const checked = isControlled ? checkedProp : internalChecked\n  const [pressing, setPressing] = useState(false)\n  const [hovering, setHovering] = useState(false)\n  const reduceMotion = useReducedMotion()\n\n  const target = pressing\n    ? HEIGHT.pressed\n    : checked\n      ? hovering\n        ? HEIGHT.latchedHover\n        : HEIGHT.latched\n      : hovering\n        ? HEIGHT.hover\n        : HEIGHT.up\n  const height = useSpring(target, SPRING)\n\n  useEffect(() => {\n    if (reduceMotion) height.jump(target)\n    else height.set(target)\n  }, [height, target, reduceMotion])\n\n  // The walls fill the space under the cap, and the cast shadow spreads and\n  // fades as the cap rises away from the socket floor.\n  const capLift = useTransform(height, (h) => em(-h))\n  const shadowOpacity = useTransform(height, [0, HEIGHT.up], [0.35, 1])\n  const shadowScale = useTransform(height, [0, HEIGHT.up], [0.92, 1.06])\n  const shadowDrop = useTransform(height, (h) => em(0.05 + h * 0.35))\n\n  const toggle = () => {\n    const next = !checked\n    if (!isControlled) {\n      setInternalChecked(next)\n    }\n    onCheckedChange?.(next)\n    if (sound) {\n      playClick(next)\n    }\n  }\n\n  const lit = illuminated && checked\n\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={checked}\n      aria-label={ariaLabel}\n      disabled={disabled}\n      onClick={toggle}\n      onPointerDown={(e) => {\n        if (e.button === 0) setPressing(true)\n      }}\n      onPointerUp={() => setPressing(false)}\n      onPointerEnter={(e) => {\n        if (e.pointerType === \"mouse\") setHovering(true)\n      }}\n      onPointerLeave={() => {\n        setPressing(false)\n        setHovering(false)\n      }}\n      onPointerCancel={() => setPressing(false)}\n      onKeyDown={(e) => {\n        if (e.key === \" \" || e.key === \"Enter\") setPressing(true)\n      }}\n      onKeyUp={() => setPressing(false)}\n      onBlur={() => setPressing(false)}\n      style={{ fontSize: FONT_SIZE[size], \"--cap\": color } as CSSProperties}\n      className={cn(\n        \"group relative inline-flex cursor-pointer rounded-[1.1em] p-[0.5em] pt-[1.4em] outline-none select-none [-webkit-tap-highlight-color:transparent]\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        \"disabled:cursor-not-allowed disabled:opacity-50\",\n        TONES[tone],\n        className\n      )}\n    >\n      <span\n        aria-hidden\n        className=\"relative block\"\n        style={{ width: em(SOCKET), height: em(SOCKET) }}\n      >\n        {/* Socket: a moulded plate with a lit top edge and a soft drop shadow. */}\n        <span\n          className=\"absolute -inset-[0.3em] rounded-[1.05em] shadow-[0_0.08em_0.1em_rgb(0_0_0/0.18),0_0.5em_1em_-0.3em_var(--floor),inset_0_0.05em_0_rgb(255_255_255/0.6),inset_0_-0.06em_0_rgb(0_0_0/0.18)]\"\n          style={{ background: \"linear-gradient(to bottom, var(--hi), var(--mid) 50%, var(--lo))\" }}\n        />\n        {/* The well the cap travels in: darker the deeper it goes. */}\n        <span\n          className=\"absolute inset-0 rounded-[0.85em] shadow-[inset_0_0.14em_0.3em_rgb(0_0_0/0.5),inset_0_-0.04em_0_rgb(255_255_255/0.25),0_0.04em_0_rgb(255_255_255/0.5)]\"\n          style={{ background: \"linear-gradient(to bottom, var(--well-lo), var(--well))\" }}\n        />\n        {/* Light from a latched cap pooling in the well. */}\n        <span\n          className=\"absolute inset-[0.15em] rounded-[0.75em] blur-[0.35em] transition-opacity duration-500\"\n          style={{ background: \"var(--cap)\", opacity: lit ? \"var(--spill)\" : 0 }}\n        />\n        {/* Shadow the cap casts on the well floor. */}\n        <motion.span\n          className=\"absolute rounded-[0.7em] bg-black/70 blur-[0.3em]\"\n          style={{\n            inset: em(GAP),\n            opacity: shadowOpacity,\n            scale: shadowScale,\n            y: shadowDrop,\n          }}\n        />\n        {/* Walls: a block that rides up with the cap, cut off at the floor. */}\n        <span\n          className=\"absolute overflow-hidden rounded-b-[0.7em]\"\n          style={{ left: em(GAP), right: em(GAP), bottom: em(GAP), top: em(-HEIGHT.up) }}\n        >\n          <motion.span\n            className=\"absolute inset-x-0 overflow-hidden rounded-[0.7em] will-change-transform\"\n            style={{\n              // Starts level with the cap's top and reaches the floor even at full lift.\n              top: em(GAP + HEIGHT.up),\n              height: em(CAP + HEIGHT.up),\n              y: capLift,\n              background: `linear-gradient(to bottom, ${mix(62, \"black\")}, ${mix(40, \"black\")})`,\n            }}\n          >\n            {/* Rounded corners turn away from the light at both sides. */}\n            <span className=\"absolute inset-0 bg-[linear-gradient(to_right,rgb(0_0_0/0.35),transparent_18%,rgb(255_255_255/0.08)_50%,transparent_82%,rgb(0_0_0/0.35))]\" />\n          </motion.span>\n        </span>\n        {/* Cap: the top face, carried up by the walls. */}\n        <motion.span\n          className=\"absolute overflow-hidden rounded-[0.7em] will-change-transform\"\n          style={{\n            left: em(GAP),\n            top: em(GAP),\n            width: em(CAP),\n            height: em(CAP),\n            y: capLift,\n            background: `linear-gradient(to bottom, ${mix(80, \"white\")}, var(--cap) 45%, ${mix(86, \"black\")})`,\n            boxShadow: `inset 0 0.06em 0 ${mix(45, \"white\")}, inset 0 -0.08em 0.05em ${mix(60, \"black\")}`,\n          }}\n        >\n          {/* A shallow dish in the middle of the cap, lit from above. */}\n          <span\n            className=\"absolute inset-[0.34em] rounded-[0.5em]\"\n            style={{\n              background: `linear-gradient(to bottom, ${mix(82, \"black\")}, ${mix(92, \"white\")})`,\n              boxShadow: `inset 0 0.07em 0.12em ${mix(45, \"black\")}, 0 0.04em 0 ${mix(55, \"white\")}`,\n            }}\n          />\n          {/* Inner lamp: the cap glows through when latched on. */}\n          <span\n            className=\"absolute inset-0 transition-opacity duration-500\"\n            style={{\n              opacity: lit ? 1 : 0,\n              background:\n                \"radial-gradient(circle at 50% 50%, rgb(255 240 225 / 0.55), rgb(255 200 170 / 0.18) 50%, transparent 80%)\",\n            }}\n          />\n          <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            strokeWidth={2.4}\n            strokeLinecap=\"round\"\n            className=\"absolute top-1/2 left-1/2 size-[1.25em] -translate-x-1/2 -translate-y-1/2 transition-[filter] duration-500\"\n            style={{\n              stroke: markColor,\n              filter: lit\n                ? \"drop-shadow(0 0 0.12em rgb(255 255 255 / 0.9))\"\n                : \"drop-shadow(0 0.04em 0 rgb(0 0 0 / 0.35))\",\n            }}\n          >\n            <path d=\"M12 3v8\" />\n            <path d=\"M7.05 6.4a7 7 0 1 0 9.9 0\" />\n          </svg>\n        </motion.span>\n      </span>\n    </button>\n  )\n}\n\nexport { RealisticSwitch }\nexport type { RealisticSwitchProps, RealisticSwitchSize, RealisticSwitchTone }\n"
    }
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ]
}
