{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bead-button","type":"registry:component","title":"Bead Button","description":"A single bead of water on dark hydrophobic glass is the button. It magnifies the etched plate behind it and shows it upside down, the way real water does, while the label stays upright and crisp. The pointer's approach leans it, a press flattens it and pinches off a satellite droplet that re-merges, and it never runs away.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/bead-button.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useRef } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties } from \"react\";\n\n/**\n * BeadButton: a single bead of water on dark hydrophobic glass IS the button.\n * It is a real <button>. Nothing about the button is wrong except what it is\n * made of.\n *\n * HOW IT IS MADE. CSS and SVG only, no canvas, no dependencies.\n *   1. The component owns its backdrop: a dark etched plate (hairline grid,\n *      one pool of light, a ruler, a few mono marks, static grain). The same\n *      <Backdrop> is drawn twice.\n *   2. Inside the bead the second copy is the lens. It is rotated 180 degrees\n *      about the bead centre and scaled by \"magnify\", so the backdrop seen\n *      through the water is inverted and enlarged, which is the physically\n *      correct cue that this is water. The bead clips it.\n *   3. An SVG filter bends it at the rim: a displacement map whose strength\n *      rises steeply towards the edge (barrel), run three times at slightly\n *      different strengths so the edge fringes.\n *   4. The bead's own optics are layered on top: a thin dark total-internal-\n *      reflection band, a bright crescent on the lit rim, a bounce on the far\n *      rim, one specular and one speck, a body tint that deepens at the rim.\n *      The plate gets a contact shadow and a bright caustic crescent on the\n *      side away from the one light (top left). The label is upright and\n *      crisp above the water.\n *\n * ALIVE. A small spring system runs ONLY while something is moving; at rest\n * no frame loop exists. The pointer's approach leans the bead (time constant\n * ~0.35 s); pressing flattens and widens it while a satellite droplet pinches\n * off and re-merges within ~380 ms; sweeping across the plate shoves it a few\n * pixels and it springs back (non-wetting). It never moves away from the\n * pointer: the click target stays where it is. Touch and pen respond on a TAP\n * only. Under prefers-reduced-motion the bead is static and the pressed state\n * is instant.\n *\n * Needs SVG filters on HTML (all current browsers). Where the displacement\n * is not supported the bead still shows the inverted, magnified lens, just\n * without the rim bend.\n *\n * Part of the Attention kit: a style of anomalies, familiar things made wrong\n * (fish in dry air, a stone that breaks upward, a shadow that runs toward its\n * lamp). One subject, one motivated light, one broken rule, and everything\n * else calm. Cut byte for byte from the kit's one site (AttentionSite, a\n * demo studio called Aerium). Paste it as its own file: it carries its own\n * copy of every helper it uses and has no dependencies.\n */\n\nconst HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\nfunction clamp(v: number, lo: number, hi: number, fallback: number) {\n  return Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;\n}\n\nconst f = (n: number) => String(Math.round(n * 1000) / 1000);\n\nexport type BeadButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport type BeadButtonTint = \"clear\" | \"koi\" | \"cobalt\";\n\nexport type BeadButtonProps = Omit<ComponentPropsWithoutRef<\"button\">, \"type\" | \"children\" | \"className\" | \"style\" | \"color\"> & {\n  /** The words on the bead. */\n  label?: string;\n  size?: BeadButtonSize;\n  /** A subtle body tint: clear water, koi (warm) or cobalt. */\n  tint?: BeadButtonTint;\n  /** How strongly the bead magnifies the plate behind it, 1.1 to 1.7. */\n  magnify?: number;\n  /** Plate ground colour, a hex. */\n  ground?: string;\n  type?: \"button\" | \"submit\" | \"reset\";\n  /** Goes on the plate (the root), which centres the bead. */\n  className?: string;\n  style?: CSSProperties;\n};\n\nconst SIZES: Record<BeadButtonSize, { h: number; font: number }> = {\n  sm: { h: 60, font: 13 },\n  md: { h: 84, font: 16 },\n  lg: { h: 116, font: 20 },\n};\n\nconst BEAD_TINTS: Record<BeadButtonTint, { core: string; rim: string; caustic: string; sat: string }> = {\n  clear: { core: \"rgba(217,236,248,0)\", rim: \"rgba(150,190,215,0.07)\", caustic: \"217,236,248\", sat: \"rgba(120,160,185,0.20)\" },\n  koi: { core: \"rgba(255,127,30,0.03)\", rim: \"rgba(255,120,30,0.20)\", caustic: \"255,176,112\", sat: \"rgba(255,127,30,0.26)\" },\n  cobalt: { core: \"rgba(43,99,255,0.04)\", rim: \"rgba(43,99,255,0.26)\", caustic: \"150,184,255\", sat: \"rgba(43,99,255,0.30)\" },\n};\n\n/* The bead is almost an ellipse, not quite: a drop on glass is never perfect. */\nconst SHAPE = \"31% 33% 35% 32% / 46% 44% 56% 54%\";\n\n/* ---- The lens ----------------------------------------------------------\n * Displacement map: red rises left to right, green top to bottom. A curve\n * on each channel (|t|^2.4) leaves the middle flat and bends the rim hard,\n * which is what a barrel lens does. 17 samples are plenty.\n */\nconst MAP_URI =\n  \"data:image/svg+xml,\" +\n  encodeURIComponent(\n    [\n      \"<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'>\",\n      \"<defs>\",\n      \"<linearGradient id='a' x1='0' y1='0' x2='1' y2='0'><stop offset='0' stop-color='#000'/><stop offset='1' stop-color='#f00'/></linearGradient>\",\n      \"<linearGradient id='b' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='#000'/><stop offset='1' stop-color='#0f0'/></linearGradient>\",\n      \"</defs>\",\n      \"<rect width='128' height='128' fill='url(#a)'/>\",\n      \"<rect width='128' height='128' fill='url(#b)' style='mix-blend-mode:screen'/>\",\n      \"</svg>\",\n    ].join(\"\"),\n  );\n\nconst BARREL = Array.from({ length: 17 }, (_, i) => {\n  const t = (i / 16) * 2 - 1;\n  return (0.5 + 0.5 * Math.sign(t) * Math.pow(Math.abs(t), 2.4)).toFixed(3);\n}).join(\" \");\n\n/* ---- The plate ---------------------------------------------------------\n * The one thing the lens looks at. Everything is in plate pixels, anchored to\n * the top-left corner or (for the marks) to the centre, so the lens copy,\n * which has the same size, lines up exactly with the plate beneath it.\n */\nfunction Backdrop({ uid, ground, k }: { uid: string; ground: string; k: number }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      style={{\n        display: \"block\",\n        position: \"absolute\",\n        inset: 0,\n        pointerEvents: \"none\",\n        backgroundColor: ground,\n        backgroundImage: [\n          \"radial-gradient(ellipse 27% 46% at 41% 40%, rgba(217,236,248,0.36), rgba(217,236,248,0.12) 48%, rgba(217,236,248,0) 80%)\",\n          \"linear-gradient(135deg, rgba(27,37,45,0.9), rgba(4,6,10,0) 72%)\",\n        ].join(\",\"),\n      }}\n    >\n      <svg width=\"100%\" height=\"100%\" style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}>\n        <defs>\n          <pattern id={uid + \"g1\"} width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n            <path d=\"M16 0H0V16\" fill=\"none\" stroke=\"rgba(214,232,245,0.09)\" strokeWidth=\"1\" />\n          </pattern>\n          <pattern id={uid + \"g2\"} width=\"80\" height=\"80\" patternUnits=\"userSpaceOnUse\">\n            <path d=\"M80 0H0V80\" fill=\"none\" stroke=\"rgba(214,232,245,0.19)\" strokeWidth=\"1\" />\n          </pattern>\n          <filter id={uid + \"n\"} x=\"0\" y=\"0\" width=\"100%\" height=\"100%\">\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.85\" numOctaves=\"2\" seed=\"7\" stitchTiles=\"stitch\" />\n            <feColorMatrix values=\"0 0 0 0 0.85  0 0 0 0 0.92  0 0 0 0 0.97  1.6 0 0 0 -0.5\" />\n          </filter>\n        </defs>\n        <rect width=\"100%\" height=\"100%\" fill={\"url(#\" + uid + \"g1)\"} />\n        <rect width=\"100%\" height=\"100%\" fill={\"url(#\" + uid + \"g2)\"} />\n        <rect width=\"100%\" height=\"100%\" filter={\"url(#\" + uid + \"n)\"} opacity=\"0.06\" />\n        {/* Marks, placed from the plate centre. They read as etched into the glass. */}\n        <svg x=\"50%\" y=\"50%\" overflow=\"visible\">\n          <g transform={\"scale(\" + k + \")\"}>\n          <g fill=\"none\" stroke=\"rgba(234,241,246,0.6)\" strokeWidth=\"1\">\n            <path d=\"M-96 21H96\" />\n            {Array.from({ length: 20 }, (_, i) => {\n              const x = -95 + i * 10;\n              const tall = i % 5 === 0;\n              return <path key={i} d={\"M\" + x + \" 21V\" + (tall ? 13 : 17)} />;\n            })}\n            <path d=\"M-60 -22H-30M-45 -29V-15\" />\n            <circle cx=\"62\" cy=\"-14\" r=\"5\" />\n            <path d=\"M62 -22V-6M54 -14H70\" strokeOpacity=\"0.6\" />\n          </g>\n          <g fill=\"rgba(234,241,246,0.7)\" fontFamily=\"ui-monospace, SFMono-Regular, Menlo, monospace\" fontSize=\"8\" letterSpacing=\"1\">\n            <text x=\"-95\" y=\"33\">\n              0\n            </text>\n            <text x=\"-45\" y=\"33\">\n              50\n            </text>\n            <text x=\"5\" y=\"33\">\n              100\n            </text>\n            <text x=\"55\" y=\"33\">\n              150\n            </text>\n            <text x=\"-26\" y=\"-20\">\n              B 01\n            </text>\n            <text x=\"-12\" y=\"-4\" fillOpacity=\"0.7\">\n              112°\n            </text>\n          </g>\n          </g>\n        </svg>\n      </svg>\n    </span>\n  );\n}\n\nconst STYLE_CSS = [\n  \".bdb-btn{appearance:none;-webkit-appearance:none;border:0;padding:0;margin:0;background:transparent;font:inherit;color:inherit;cursor:pointer;position:relative;display:block;touch-action:manipulation;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none;will-change:transform}\",\n  \".bdb-btn:focus{outline:none}\",\n  \".bdb-btn:focus-visible{outline:2px solid #d9ecf8!important;outline-offset:6px!important;border-radius:\" + SHAPE + \"!important}\",\n  \".bdb-btn:disabled{cursor:not-allowed}\",\n  \".bdb-btn:disabled .bdb-body{opacity:.5;filter:saturate(.5)}\",\n  \".bdb-btn:disabled .bdb-label{opacity:.62}\",\n  \"@keyframes bdb-breathe{0%,100%{opacity:.74}50%{opacity:1}}\",\n  \".bdb-caustic{animation:bdb-breathe 9.7s ease-in-out infinite}\",\n  \"@media (prefers-reduced-motion:reduce){.bdb-caustic{animation:none}}\",\n].join(\"\\n\");\n\ntype Live = { disabled: boolean; magnify: number; h: number };\n\nexport function BeadButton({\n  label = \"Commission a room\",\n  size = \"md\",\n  tint = \"clear\",\n  magnify = 1.35,\n  ground = \"#0a1015\",\n  type = \"button\",\n  disabled = false,\n  className = \"\",\n  style,\n  ...rest\n}: BeadButtonProps) {\n  const sz = SIZES[size] ?? SIZES.md;\n  const tn = BEAD_TINTS[tint] ?? BEAD_TINTS.clear;\n  const mag = clamp(magnify, 1.1, 1.7, 1.35);\n  const groundHex = HEX.test(ground) ? ground : \"#0a1015\";\n  const text = String(label ?? \"\").slice(0, 60);\n  const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n  const fid = uid + \"d\";\n  const H = sz.h;\n  const D = Math.round(H * 0.2);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const slotRef = useRef<HTMLDivElement>(null);\n  const btnRef = useRef<HTMLButtonElement>(null);\n  const bodyRef = useRef<HTMLSpanElement>(null);\n  const lensRef = useRef<HTMLSpanElement>(null);\n  const specRef = useRef<HTMLSpanElement>(null);\n  const labelRef = useRef<HTMLSpanElement>(null);\n  const causticRef = useRef<HTMLDivElement>(null);\n  const satRef = useRef<HTMLDivElement>(null);\n  const neckRef = useRef<HTMLDivElement>(null);\n  const opts = useRef<Live>({ disabled, magnify: mag, h: H });\n  const refresh = useRef<() => void>(() => {});\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const slot = slotRef.current;\n    const btn = btnRef.current;\n    const body = bodyRef.current;\n    const lens = lensRef.current;\n    const spec = specRef.current;\n    const lab = labelRef.current;\n    const caustic = causticRef.current;\n    const sat = satRef.current;\n    const neck = neckRef.current;\n    if (!neck || !root || !slot || !btn || !body || !lens || !spec || !lab || !caustic || !sat) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\n    const s = {\n      x: 0, y: 0, vx: 0, vy: 0, // shove spring\n      lx: 0, ly: 0, tlx: 0, tly: 0, // lean (smoothed) and its target\n      p: 0, pv: 0, pressed: false, // press spring\n      satT: -1, // satellite clock in seconds, -1 idle\n      px: 0, py: 0, pt: 0, // last pointer sample for velocity\n    };\n    const g = { cx: 0, cy: 0, a: 1, b: 1, k: 1 };\n    let raf = 0;\n    let last = 0;\n    let releaseTimer = 0;\n    let tap: { x: number; y: number; t: number } | null = null;\n\n    const measure = () => {\n      g.cx = slot.offsetLeft + slot.offsetWidth / 2;\n      g.cy = slot.offsetTop + slot.offsetHeight / 2;\n      g.a = Math.max(1, slot.offsetWidth / 2);\n      g.b = Math.max(1, slot.offsetHeight / 2);\n      lens.style.width = root.clientWidth + \"px\";\n      lens.style.height = root.clientHeight + \"px\";\n    };\n\n    const ease = (w: number) => w * w * (3 - 2 * w);\n\n    const paint = () => {\n      const H0 = opts.current.h;\n      const tx = s.x + s.lx * H0 * 0.03;\n      const ty = s.y + s.ly * H0 * 0.025;\n      btn.style.transform = \"translate(\" + f(tx) + \"px,\" + f(ty) + \"px)\";\n      const sx = 1 + 0.08 * s.p + 0.025 * Math.abs(s.lx);\n      const sy = 1 - 0.14 * s.p + 0.02 * Math.abs(s.ly);\n      body.style.transform = \"scale(\" + f(sx) + \",\" + f(sy) + \")\";\n      const m = opts.current.magnify * (1 - 0.04 * s.p);\n      lens.style.transform =\n        \"rotate(180deg) scale(\" + f(m) + \") translate(\" + f(-(g.cx + tx + s.lx * H0 * 0.06)) + \"px,\" + f(-(g.cy + ty + s.ly * H0 * 0.05)) + \"px)\";\n      spec.style.transform = \"rotate(-16deg) translate(\" + f(-s.lx * H0 * 0.04) + \"px,\" + f(-s.ly * H0 * 0.035) + \"px)\";\n      lab.style.transform = \"translate(\" + f(s.lx * H0 * 0.02) + \"px,\" + f(s.ly * H0 * 0.015 + s.p * H0 * 0.025) + \"px)\";\n      caustic.style.transform = \"translate(\" + f(tx + H0 * 0.08) + \"px,\" + f(ty + H0 * 0.11) + \"px) scale(\" + f(1 + 0.05 * s.p) + \",\" + f(1 - 0.06 * s.p) + \")\";\n      if (s.satT >= 0) {\n        const u = Math.min(1, s.satT / 0.38);\n        let e = 0;\n        if (u < 0.4) e = ease(u / 0.4);\n        else if (u < 0.55) e = 1;\n        else e = 1 - ease((u - 0.55) / 0.45);\n        const th = 0.66;\n        const nx0 = Math.cos(th) / g.a;\n        const ny0 = Math.sin(th) / g.b;\n        const nl = Math.hypot(nx0, ny0) || 1;\n        const nx = nx0 / nl;\n        const ny = ny0 / nl;\n        const dd = Math.round(H0 * 0.2);\n        const dist = -dd * 0.3 + e * dd * 1.95;\n        const ox = g.a * Math.cos(th) + nx * dist + tx;\n        const oy = g.b * Math.sin(th) + ny * dist + ty;\n        const along = 1 + 1.7 * Math.pow(1 - e, 1.6) * (u < 0.55 ? 1 : 0.6);\n        const across = 1 / Math.sqrt(along);\n        const grow = 0.55 + 0.45 * Math.min(1, e * 1.6);\n        sat.style.opacity = \"1\";\n        const tk = u < 0.42 ? 1 - u / 0.42 : u > 0.8 ? (u - 0.8) / 0.2 : 0;\n        if (tk > 0.03) {\n          const len = Math.max(2, dist + dd * 0.6);\n          neck.style.opacity = \"1\";\n          neck.style.width = f(len) + \"px\";\n          neck.style.height = f(dd * 0.75) + \"px\";\n          neck.style.marginTop = f(-dd * 0.375) + \"px\";\n          neck.style.transform =\n            \"translate(\" + f(g.a * Math.cos(th) + nx * -dd * 0.6 + tx) + \"px,\" + f(g.b * Math.sin(th) + ny * -dd * 0.6 + ty) + \"px) rotate(\" + f((Math.atan2(ny, nx) * 180) / Math.PI) + \"deg) scaleY(\" + f(tk) + \")\";\n        } else neck.style.opacity = \"0\";\n        sat.style.transform =\n          \"translate(\" + f(ox) + \"px,\" + f(oy) + \"px) rotate(\" + f((Math.atan2(ny, nx) * 180) / Math.PI) + \"deg) scale(\" + f(along * grow) + \",\" + f(across * grow) + \")\";\n      } else {\n        sat.style.opacity = \"0\";\n        neck.style.opacity = \"0\";\n      }\n    };\n\n    const active = () =>\n      s.satT >= 0 ||\n      Math.abs(s.lx - s.tlx) > 0.002 ||\n      Math.abs(s.ly - s.tly) > 0.002 ||\n      Math.abs(s.x) > 0.02 ||\n      Math.abs(s.y) > 0.02 ||\n      Math.abs(s.vx) > 0.05 ||\n      Math.abs(s.vy) > 0.05 ||\n      Math.abs(s.p - (s.pressed ? 1 : 0)) > 0.002 ||\n      Math.abs(s.pv) > 0.01;\n\n    const frame = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;\n      last = now;\n      const k = 1 - Math.exp(-dt / 0.35);\n      s.lx += (s.tlx - s.lx) * k;\n      s.ly += (s.tly - s.ly) * k;\n      // Shove: a soft spring pulls the bead home (non-wetting).\n      s.vx += (-150 * s.x - 15 * s.vx) * dt;\n      s.vy += (-150 * s.y - 15 * s.vy) * dt;\n      s.x = Math.max(-6, Math.min(6, s.x + s.vx * dt));\n      s.y = Math.max(-6, Math.min(6, s.y + s.vy * dt));\n      // Press: underdamped, so it settles with one small overshoot.\n      s.pv += (260 * ((s.pressed ? 1 : 0) - s.p) - 19 * s.pv) * dt;\n      s.p += s.pv * dt;\n      if (s.satT >= 0) {\n        s.satT += dt;\n        if (s.satT > 0.38) s.satT = -1;\n      }\n      if (active()) {\n        paint();\n        raf = requestAnimationFrame(frame);\n      } else {\n        last = 0;\n        s.x = s.y = s.vx = s.vy = 0;\n        s.lx = s.tlx;\n        s.ly = s.tly;\n        s.p = s.pressed ? 1 : 0;\n        s.pv = 0;\n        paint();\n      }\n    };\n\n    const kick = () => {\n      if (reduced.matches) {\n        paint();\n        return;\n      }\n      if (!raf) {\n        last = 0;\n        raf = requestAnimationFrame(frame);\n      }\n    };\n\n    const setPressed = (on: boolean, withSatellite: boolean) => {\n      const next = on && !opts.current.disabled;\n      s.pressed = next;\n      if (next && withSatellite && !reduced.matches && s.satT < 0) s.satT = 0;\n      if (reduced.matches) {\n        s.p = next ? 1 : 0;\n        s.pv = 0;\n      }\n      kick();\n    };\n\n    const toPlate = (e: PointerEvent) => {\n      const r = root.getBoundingClientRect();\n      g.k = r.width / (root.offsetWidth || 1) || 1;\n      return { x: (e.clientX - r.left) / g.k, y: (e.clientY - r.top) / g.k };\n    };\n\n    const onMove = (e: PointerEvent) => {\n      if (e.pointerType === \"touch\" || reduced.matches || opts.current.disabled) return;\n      const p = toPlate(e);\n      const dx = p.x - (g.cx + s.x);\n      const dy = p.y - (g.cy + s.y);\n      const dist = Math.hypot(dx / g.a, dy / g.b);\n      const prox = dist <= 1 ? 1 : Math.max(0, 1 - (dist - 1) / 2.2);\n      const sm = prox * prox * (3 - 2 * prox);\n      s.tlx = Math.max(-1, Math.min(1, dx / (g.a * 1.1))) * sm;\n      s.tly = Math.max(-1, Math.min(1, dy / (g.b * 1.5))) * sm;\n      const dt = (e.timeStamp - s.pt) / 1000;\n      if (dist < 1.15 && dt > 0 && dt < 0.12) {\n        const vx = (p.x - s.px) / dt;\n        const vy = (p.y - s.py) / dt;\n        s.vx = Math.max(-80, Math.min(80, s.vx + Math.max(-40, Math.min(40, vx * 0.05))));\n        s.vy = Math.max(-80, Math.min(80, s.vy + Math.max(-40, Math.min(40, vy * 0.05))));\n      }\n      s.px = p.x;\n      s.py = p.y;\n      s.pt = e.timeStamp;\n      kick();\n    };\n\n    const onLeave = () => {\n      s.tlx = 0;\n      s.tly = 0;\n      kick();\n    };\n\n    const onDown = (e: PointerEvent) => {\n      if (opts.current.disabled || !(e.target instanceof Node) || !btn.contains(e.target)) return;\n      if (e.pointerType === \"mouse\") setPressed(true, true);\n      else tap = { x: e.clientX, y: e.clientY, t: e.timeStamp };\n    };\n\n    const onUp = (e: PointerEvent) => {\n      if (e.pointerType === \"mouse\") {\n        if (s.pressed) setPressed(false, false);\n        return;\n      }\n      const t0 = tap;\n      tap = null;\n      if (!t0 || e.type !== \"pointerup\") return;\n      if (Math.hypot(e.clientX - t0.x, e.clientY - t0.y) < 8 && e.timeStamp - t0.t < 300 && e.target instanceof Node && btn.contains(e.target)) {\n        setPressed(true, true);\n        window.clearTimeout(releaseTimer);\n        releaseTimer = window.setTimeout(() => setPressed(false, false), 140);\n      }\n    };\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      if ((e.key === \" \" || e.key === \"Enter\") && !e.repeat) setPressed(true, true);\n    };\n    const onKeyUp = () => {\n      if (s.pressed) setPressed(false, false);\n    };\n\n    measure();\n    paint();\n    const ro = new ResizeObserver(() => {\n      measure();\n      paint();\n    });\n    ro.observe(root);\n    ro.observe(slot);\n    root.addEventListener(\"pointermove\", onMove);\n    root.addEventListener(\"pointerleave\", onLeave);\n    root.addEventListener(\"pointerdown\", onDown);\n    window.addEventListener(\"pointerup\", onUp);\n    window.addEventListener(\"pointercancel\", onUp);\n    btn.addEventListener(\"keydown\", onKeyDown);\n    btn.addEventListener(\"keyup\", onKeyUp);\n    btn.addEventListener(\"blur\", onKeyUp);\n    refresh.current = () => {\n      measure();\n      if (opts.current.disabled && s.pressed) s.pressed = false;\n      kick();\n      paint();\n    };\n    return () => {\n      refresh.current = () => {};\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n      window.clearTimeout(releaseTimer);\n      ro.disconnect();\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      root.removeEventListener(\"pointerdown\", onDown);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onUp);\n      btn.removeEventListener(\"keydown\", onKeyDown);\n      btn.removeEventListener(\"keyup\", onKeyUp);\n      btn.removeEventListener(\"blur\", onKeyUp);\n    };\n  }, []);\n\n  useEffect(() => {\n    opts.current = { disabled, magnify: mag, h: H };\n    refresh.current();\n  }, [disabled, mag, H, text]);\n\n  const padX = Math.round(H * 0.5);\n  const S = Math.round(H * 0.5);\n  const rootStyle: CSSProperties = {\n    padding: Math.round(H * 0.8) + \"px min(\" + Math.round(H * 1.15 + 20) + \"px, 6%)\",\n    backgroundColor: groundHex,\n    ...style,\n  };\n  const layer: CSSProperties = { display: \"block\", position: \"absolute\", inset: 0 };\n\n  return (\n    <div ref={rootRef} className={\"relative isolate inline-flex items-center justify-center overflow-hidden \" + className} style={rootStyle}>\n      <style>{STYLE_CSS}</style>\n      <Backdrop uid={uid + \"p\"} ground={groundHex} k={H / 84} />\n\n      {/* The lens filter: barrel bend with a chromatic fringe. */}\n      <svg width=\"0\" height=\"0\" aria-hidden=\"true\" focusable=\"false\" style={{ position: \"absolute\", pointerEvents: \"none\" }}>\n        <filter id={fid} x=\"-10%\" y=\"-14%\" width=\"120%\" height=\"128%\" colorInterpolationFilters=\"sRGB\">\n          <feImage href={MAP_URI} preserveAspectRatio=\"none\" result=\"map\" />\n          <feComponentTransfer in=\"map\" result=\"cm\">\n            <feFuncR type=\"table\" tableValues={BARREL} />\n            <feFuncG type=\"table\" tableValues={BARREL} />\n          </feComponentTransfer>\n          <feDisplacementMap in=\"SourceGraphic\" in2=\"cm\" scale={S * 0.9} xChannelSelector=\"R\" yChannelSelector=\"G\" result=\"dr\" />\n          <feDisplacementMap in=\"SourceGraphic\" in2=\"cm\" scale={S} xChannelSelector=\"R\" yChannelSelector=\"G\" result=\"dg\" />\n          <feDisplacementMap in=\"SourceGraphic\" in2=\"cm\" scale={S * 1.1} xChannelSelector=\"R\" yChannelSelector=\"G\" result=\"db\" />\n          <feColorMatrix in=\"dr\" type=\"matrix\" values=\"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"r\" />\n          <feColorMatrix in=\"dg\" type=\"matrix\" values=\"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\" result=\"g\" />\n          <feColorMatrix in=\"db\" type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\" result=\"b\" />\n          <feBlend in=\"r\" in2=\"g\" mode=\"screen\" result=\"rg\" />\n          <feBlend in=\"rg\" in2=\"b\" mode=\"screen\" />\n        </filter>\n      </svg>\n\n      <div ref={slotRef} className=\"relative\" style={{ zIndex: 1 }}>\n        {/* Caustic: the bead focuses the one light into a crescent on the plate, away from the light. */}\n        <div\n          ref={causticRef}\n          className=\"bdb-caustic\"\n          aria-hidden=\"true\"\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: SHAPE,\n            pointerEvents: \"none\",\n                        background:\n              \"radial-gradient(ellipse 24% 26% at 80% 84%, rgba(255,255,255,1), rgba(\" + tn.caustic + \",0.85) 38%, rgba(\" + tn.caustic + \",0.22) 72%, rgba(\" + tn.caustic + \",0) 100%)\",\n            filter: \"blur(1.5px)\",\n          }}\n        />\n        {/* Satellite droplet: pinches off the lower right on a press and re-merges. */}\n        <div\n          ref={satRef}\n          aria-hidden=\"true\"\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"50%\",\n            width: D,\n            height: D,\n            marginLeft: -D / 2,\n            marginTop: -D / 2,\n            borderRadius: \"50%\",\n            opacity: 0,\n            pointerEvents: \"none\",\n            background: [\n              \"radial-gradient(circle at 34% 30%, rgba(255,255,255,0.95) 0, rgba(255,255,255,0.7) 9%, rgba(255,255,255,0) 26%)\",\n              \"radial-gradient(circle at 50% 50%, \" + tn.sat + \" 30%, rgba(3,5,8,0.92) 100%)\",\n            ].join(\",\"),\n            boxShadow: \"inset 0 0 0 1px rgba(214,232,245,0.34), inset -1px -2px 2px rgba(217,236,248,0.4), 2px 4px 5px rgba(0,0,0,0.5)\",\n          }}\n        />\n        {/* The pinch neck: a short tapered bridge that snaps as the satellite leaves and forms as it returns. */}\n        <div\n          ref={neckRef}\n          aria-hidden=\"true\"\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: \"50%\",\n            opacity: 0,\n            pointerEvents: \"none\",\n            transformOrigin: \"0 50%\",\n            clipPath: \"polygon(0 0, 45% 34%, 100% 40%, 100% 60%, 45% 66%, 0 100%)\",\n            background: \"linear-gradient(180deg, rgba(255,255,255,0.55), rgba(150,190,215,0.28) 40%, rgba(3,5,8,0.75))\",\n          }}\n        />\n        <button ref={btnRef} type={type} disabled={disabled} className=\"bdb-btn\" {...rest}>\n          <span\n            ref={bodyRef}\n            className=\"bdb-body\"\n            style={{\n              ...layer,\n              borderRadius: SHAPE,\n              overflow: \"hidden\",\n              isolation: \"isolate\",\n              backgroundColor: \"#03060a\",\n              boxShadow: \"5px 9px 16px rgba(0,0,0,0.55), 0 0 0 1px rgba(214,232,245,0.07)\",\n              transformOrigin: \"50% 50%\",\n            }}\n          >\n            {/* The inverted, magnified second copy of the plate, bent at the rim. */}\n            <span style={{ ...layer, filter: \"url(#\" + fid + \") brightness(1.32) contrast(1.04)\" }}>\n              <span ref={lensRef} style={{ display: \"block\", position: \"absolute\", left: \"50%\", top: \"50%\", transformOrigin: \"0 0\", willChange: \"transform\", overflow: \"hidden\" }}>\n                <Backdrop uid={uid + \"l\"} ground={groundHex} k={H / 84} />\n              </span>\n            </span>\n            {/* Body tint, deeper where the water is thicker (the rim). */}\n            <span style={{ ...layer, background: \"radial-gradient(ellipse 62% 62% at 46% 42%, \" + tn.core + \", \" + tn.rim + \" 100%)\" }} />\n            {/* A little shade behind the words, so the label keeps its contrast on any plate. */}\n            <span style={{ ...layer, background: \"radial-gradient(ellipse 36% 30% at 50% 50%, rgba(4,7,10,0.38), rgba(4,7,10,0) 100%)\" }} />\n            {/* Focused light inside: the far rim brightens where the light comes out. */}\n            <span style={{ ...layer, background: \"radial-gradient(ellipse 34% 30% at 72% 80%, rgba(217,236,248,0.55), rgba(217,236,248,0) 100%)\" }} />\n            {/* Rim: total internal reflection band, lit crescent, far bounce. */}\n            <span\n              style={{\n                ...layer,\n                borderRadius: SHAPE,\n                boxShadow: [\n                  \"inset 0 0 0 1px rgba(2,4,8,0.8)\",\n                  \"inset 0 0 4px 2px rgba(2,4,8,0.65)\",\n                  \"inset 2px 3px 0 -1px rgba(255,255,255,0.95)\",\n                  \"inset 4px 6px 6px -4px rgba(217,236,248,0.5)\",\n                  \"inset -3px -5px 5px -4px rgba(217,236,248,0.7)\",\n                ].join(\",\"),\n              }}\n            />\n            <span\n              style={{\n                display: \"block\",\n                position: \"absolute\",\n                left: \"30%\",\n                top: \"6%\",\n                width: \"34%\",\n                height: \"11%\",\n                borderRadius: \"50%\",\n                transform: \"rotate(-4deg)\",\n                background: \"linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.34) 40%, rgba(255,255,255,0.12) 80%, rgba(255,255,255,0))\",\n                filter: \"blur(1.5px)\",\n              }}\n            />\n            <span\n              ref={specRef}\n              style={{\n                display: \"block\",\n                position: \"absolute\",\n                left: \"13%\",\n                top: \"11%\",\n                width: Math.round(H * 0.34),\n                height: Math.round(H * 0.17),\n                borderRadius: \"50%\",\n                transform: \"rotate(-16deg)\",\n                background: \"radial-gradient(ellipse at 50% 50%, rgba(255,255,255,1) 0, rgba(255,255,255,0.8) 28%, rgba(255,255,255,0) 68%)\",\n              }}\n            />\n            <span\n              style={{\n                display: \"block\",\n                position: \"absolute\",\n                right: \"12%\",\n                bottom: \"13%\",\n                width: Math.round(H * 0.1),\n                height: Math.round(H * 0.07),\n                borderRadius: \"50%\",\n                background: \"radial-gradient(ellipse at 50% 50%, rgba(255,255,255,0.55), rgba(255,255,255,0) 70%)\",\n              }}\n            />\n          </span>\n          <span\n            ref={labelRef}\n            className=\"bdb-label\"\n            style={{\n              position: \"relative\",\n              display: \"flex\",\n              height: H,\n              minWidth: Math.round(H * 1.55),\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              padding: \"0 \" + padX + \"px\",\n              whiteSpace: \"nowrap\",\n              color: \"#eaf1f6\",\n              fontFamily: 'var(--font-instrument, \"Instrument Sans\"), \"Geist\", system-ui, sans-serif',\n              fontSize: sz.font,\n              fontWeight: 500,\n              letterSpacing: \"-0.012em\",\n              textShadow: \"0 1px 2px rgba(2,4,8,0.9), 0 0 12px rgba(2,4,8,0.8)\",\n            }}\n          >\n            {text}\n          </span>\n        </button>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/viberdy/bead-button.tsx"}],"meta":{"tier":"free","tags":["button","water","droplet","lens","refraction","css","svg","glass","interactive","anomaly","attention"],"href":"https://viberdy.dev/library/bead-button","aiPrompt":"Build `BeadButton`, a single \"use client\" React + Tailwind-compatible file with no dependencies, exporting `BeadButton` and `BeadButtonProps`. It is a real `<button>` that looks like one bead of water sitting on dark hydrophobic glass. The anomaly: the button is the only wet thing, and it behaves like real water.\n\nSTRUCTURE. A root `div` (the \"plate\", `relative isolate inline-flex items-center justify-center overflow-hidden`, className and style go here, padding about 0.8H vertical and min(1.15H+20px, 6%) horizontal) holds: the plate Backdrop, a hidden `<svg>` with the lens filter, and a slot `div` (z 1) containing, in order: a caustic div, a satellite droplet div, and the `<button>`. Inside the button: a body span (the bead; absolute inset 0; border-radius `48% 52% 51% 49% / 54% 52% 48% 46%`; overflow hidden; background #03060a; box-shadow `5px 9px 16px rgba(0,0,0,.55), 0 0 0 1px rgba(214,232,245,.07)`) and a label span above it (flex, height H, padding 0.5H, min-width 1.55H, Instrument Sans 500, `var(--font-instrument, \"Instrument Sans\"), \"Geist\", system-ui`, ink #eaf1f6, dark text-shadow halo). Sizes: sm H60/13px, md H84/16px, lg H116/20px.\n\nTHE LENS (the whole trick). `Backdrop` is a component drawing the plate: ground #0a1015, a soft light pool `radial-gradient(ellipse 27% 46% at 41% 40%, rgba(217,236,248,.36), .12 48%, 0 80%)`, a diagonal slate gradient, an SVG with a 16px hairline grid (rgba(214,232,245,.075)), an 80px grid (.15), static feTurbulence grain at 10% opacity, and marks centred on the plate and scaled by H/84: a ruler (ticks every 10px, tall every 50, mono numerals 0/50/100/150, 8px), a corner bracket, a crosshair circle, \"B 01\" and \"112°\". Render Backdrop TWICE: once as the plate, once inside the bead as the lens. The lens element is the same size as the plate (set from the root's client size), `position:absolute; left:50%; top:50%; transform-origin:0 0; transform: rotate(180deg) scale(m) translate(-(cx+tx), -(cy+ty))` where (cx,cy) is the bead centre in plate coordinates (slot offsetLeft/Top plus half its size), (tx,ty) the bead's current translation and m = `magnify` (default 1.35, clamped 1.1-1.7, 4% less while pressed). So the plate seen through the water is inverted and enlarged about the bead centre, and the bead clips it. Wrap the lens in a span with `filter:url(#id)`.\n\nFILTER. Region x -10%, y -14%, w 120%, h 128%, color-interpolation-filters sRGB. `feImage` of a 128px SVG data URI (red horizontal gradient + green vertical gradient, second rect mix-blend-mode screen) fills the region; `feComponentTransfer` with `feFuncR/G type=table` of 17 values `0.5 + 0.5*sign(t)*|t|^2.4`, t = 2i/16 - 1 (barrel: flat centre, hard bend at the rim); three `feDisplacementMap` (scale H*0.5 times 0.9, 1, 1.1) each isolated to one channel with `feColorMatrix`, recombined with two `feBlend mode=screen`: a chromatic fringe at the rim.\n\nOPTICS, layered inside the body above the lens: a 22% black shade; a body tint (`radial-gradient(ellipse 62% 62% at 46% 42%, core, rim 100%)`; clear core rgba(217,236,248,.03) rim rgba(150,190,215,.16); koi rim rgba(255,120,30,.20); cobalt rim rgba(43,99,255,.26)); a dark scrim behind the label; a bright focused-light blob at 72% 80%; a rim layer (closest-side ellipse gradient to rgba(2,4,8,.92) = total internal reflection band, plus inset shadows: thin dark line, bright crescent top left `inset 2px 3px 2px -1px rgba(255,255,255,.85)`, soft bounce bottom right `inset -4px -6px 7px -4px rgba(217,236,248,.62)`); a soft window reflection arc at the top; one specular (0.34H x 0.17H ellipse, rotated -16deg, white 1.0 to 0) and one speck at the far rim. On the plate: the caustic div (bead-shaped, offset by 0.18H, 0.3H, blurred 0.03H, `radial-gradient(ellipse 54% 46% at 76% 88%, rgba(217,236,248,.95), .3 52%, 0 80%)`, tinted by the water, opacity breathing .74-1 on a 9.7s CSS keyframe, off under reduced motion) away from the one light (top left).\n\nMOTION. No frame loop at idle: one rAF runs only while a spring is unsettled and stops itself. State: lean (smoothed, time constant 0.35s; target = direction to the pointer weighted by proximity, falling off over ~2.2 bead radii), shove (spring k=150, c=15, clamped to +-6px, impulse from pointer velocity when the pointer sweeps through the bead), press (spring k=260, c=19, overshoots once). Each frame writes: button translate (shove + lean*0.03H), body scale (1+.08p, 1-.14p), lens transform, specular counter-shift, label shift (it sinks 0.025H when pressed, never scaled), caustic transform. Satellite: on a press a droplet (0.2H) pinches off at the lower right along the rim normal and re-merges over 0.38s (out 0-0.4, hold to 0.55, back by 1; stretched along the normal while it is thin), sitting under the bead. The bead never runs from the pointer. Mouse: press on pointerdown, release on pointerup/cancel. Touch and pen: only a TAP (pointerup under 8px and 300ms) gives a 140ms press with the satellite. Keyboard: Enter/Space press while down. Reduced motion: no loop, no lean, no caustic breathing, pressed state instant. Disabled: body 50% with less saturation, label 62%, no motion, no pointer response.\n\nA11Y/CONTRACT. `<button type=\"button\">` default, forwards onClick and all other button attributes, `disabled`. Props: label, size (sm|md|lg), tint (clear|koi|cobalt), magnify, ground (hex, validated), type, disabled, className, style. Props live via an `opts` ref synced in an effect and a `refresh` ref; the main effect has [] deps. Own CSS block (joined array, no template literals): button reset, `:focus-visible{outline:2px solid #d9ecf8!important;outline-offset:6px!important;border-radius:48%!important}`, disabled rules, the caustic keyframes. Clamp every number with a NaN fallback; validate hex with a regex.","license":"https://viberdy.dev/license"}}