{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mercury-surface",
  "type": "registry:ui",
  "title": "Mercury Surface",
  "description": "Headless gooey-surface engine: an SVG metaball filter whose border never swells, because every working blur carries its own solved pair of alpha thresholds, plus the grab gesture that pulls a liquid finger out of any surface and whips it back on a spring. Renders one filter and hands back a controller - no UI of its own.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/crafterui/ui/mercury-surface.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n// The engine behind the mercury family: a metaball filter whose border never\n// swells, and the grab gesture that pulls a liquid finger out of any surface.\n//\n// Headless. It renders one <filter> and hands back a controller; the shapes,\n// the choreography and the chrome all belong to whatever is built on top of it\n// (see mercury-dial and mercury-menu). Nothing here draws a UI.\n//\n// Inspired by liquid-taffy by arknow91 (https://github.com/arknow91/liquid-taffy)\n// - MIT licensed. Rewritten here on Motion springs and theme tokens.\nimport * as React from \"react\"\nimport { animate } from \"motion/react\"\n\n/* ── The rim ──────────────────────────────────────────────────────────────\n   A goo border is not a stroke. It is the sliver between two iso-alpha\n   contours of the SAME blurred alpha, so how far apart those contours land in\n   PIXELS is decided by the blur: one fixed threshold pair draws a different\n   border at every σ, which is why gooey buttons in the wild visibly inflate\n   the moment they start moving.\n\n   Every working σ therefore carries its own pair, solved by rasterising the\n   exact filter over a 32px disc so the rim's outer edge lands on the crisp\n   border's outer edge and its weight stays 1px. Blur and thresholds are ONE\n   setting - never change one without the other, and never in a later frame. */\nexport const GOO_THRESHOLDS = {\n  1: [-14.5146, -24.6721],\n  5: [-12.7296, -15.063],\n  7: [-11.6925, -13.245],\n} as const\n\n/** The σ values the rim is solved for. Anything else draws a wrong border. */\nexport type GooBlur = keyof typeof GOO_THRESHOLDS\n\n/* Alpha-only threshold: the RGB rows stay identity so the interior keeps the\n   blurred source colours, which is what lets two fills blend into a gradient\n   right at the neck where they merge. */\nconst thresholdMatrix = (offset: number) =>\n  `1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 30 ${offset}`\n\nexport interface GooFilterProps {\n  /** Unique per instance - two filters sharing an id collide. */\n  id: string\n  /** Filter region, in the host <svg>'s user units. Must cover the whole canvas. */\n  width: number\n  height: number\n  /** Resting blur. Non-zero, or the threshold has no soft edge to bite on. @default 1 */\n  blur?: GooBlur\n}\n\n/**\n * The metaball filter, ready to drop in a host <svg>'s <defs>.\n *\n * The region is fixed in user units rather than a percentage of the group's\n * bounding box: a percentage region is measured off whatever the blobs occupy\n * right now, so it crops the blur's tail at rest and then crawls outward as a\n * finger extends.\n */\nexport function GooFilter({ id, width, height, blur = 1 }: GooFilterProps) {\n  const [outer, inner] = GOO_THRESHOLDS[blur]\n  return (\n    <filter\n      id={id}\n      filterUnits=\"userSpaceOnUse\"\n      x=\"0\"\n      y=\"0\"\n      width={width}\n      height={height}\n      colorInterpolationFilters=\"sRGB\"\n    >\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation={blur} result=\"blur\" />\n      {/* Outer contour: set below the half-way point by just enough to undo the\n          curvature shrink a blur puts on a disc, so the liquid's edge lands\n          where the crisp border's outer edge was. */}\n      <feColorMatrix in=\"blur\" type=\"matrix\" values={thresholdMatrix(outer)} result=\"outer\" />\n      {/* Inner contour: a second, higher threshold of the same blur carves the\n          interior out, and the rim is the sliver left between the two. */}\n      <feColorMatrix in=\"blur\" type=\"matrix\" values={thresholdMatrix(inner)} result=\"inner\" />\n      {/* Painted in the same order a CSS border is: the surface fills the whole\n          outer contour, the border colour lies over it, and the interior covers\n          both everywhere except the sliver. Stacking it this way is what lets a\n          translucent border token - which is what `--border` is in dark mode -\n          read at exactly the weight it does on the crisp bodies. Flooding the\n          rim alone would leave it lying on the page instead. */}\n      <feFlood style={{ floodColor: \"var(--background)\" }} result=\"surface\" />\n      <feComposite in=\"surface\" in2=\"outer\" operator=\"in\" result=\"surfaceShape\" />\n      <feFlood style={{ floodColor: \"var(--border)\" }} result=\"rim\" />\n      <feComposite in=\"rim\" in2=\"outer\" operator=\"in\" result=\"rimShape\" />\n      <feMerge>\n        <feMergeNode in=\"surfaceShape\" />\n        <feMergeNode in=\"rimShape\" />\n        <feMergeNode in=\"inner\" />\n      </feMerge>\n    </filter>\n  )\n}\n\n/**\n * Switch the blur and its matching thresholds together, in one frame. Reads the\n * primitives off the host <svg> rather than through three refs per consumer -\n * this runs a handful of times per gesture, never per frame.\n */\nexport function setGooBlur(host: SVGSVGElement | null, sigma: GooBlur) {\n  const filter = host?.querySelector(\"filter\")\n  if (!filter) return\n  const [outer, inner] = GOO_THRESHOLDS[sigma]\n  filter.querySelector(\"feGaussianBlur\")?.setAttribute(\"stdDeviation\", String(sigma))\n  const matrices = filter.querySelectorAll(\"feColorMatrix\")\n  matrices[0]?.setAttribute(\"values\", thresholdMatrix(outer))\n  matrices[1]?.setAttribute(\"values\", thresholdMatrix(inner))\n}\n\n/* ── Springs ──────────────────────────────────────────────────────────────\n   The family's two curves, as physics rather than sampled polylines: at unit\n   mass a spring of ζ, ω is stiffness ω², damping 2ζω. */\n/** ζ=0.434, ω=22.46 - 22% overshoot. Everything that pops in or springs back. */\nexport const SPRING_HOUSE = { type: \"spring\", stiffness: 505, damping: 19.5 } as const\n/** ζ=0.479, ω=18.09 - 18% overshoot. The louder curve that carries a leap. */\nexport const SPRING_POP = { type: \"spring\", stiffness: 327, damping: 17.3 } as const\n/** Micro state and the press squash. */\nexport const EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const\n/** Exits wind up ~10% the wrong way before they collapse. */\nexport const EASE_ANTICIPATE = [0.36, 0, 0.66, -0.56] as const\n\n/**\n * Apple's continuous corner (the iOS squircle), as a path. A panel draws its\n * crisp body and its goo blob from this same call, so the two pictures share\n * one silhouette and the handoff between them is invisible.\n */\nexport function squirclePath(x: number, y: number, w: number, h: number, r: number) {\n  const s = Math.min(r * 1.528665, w / 2, h / 2)\n  const u = (k: number) => s * (k / 1.528665)\n  const [c0, c1, c2, c3, c4, c5, c6] = [\n    1.528665, 1.08849, 0.8684, 0.63149, 0.37283, 0.16906, 0.07491,\n  ].map(u)\n  return [\n    `M ${x + c0} ${y}`,\n    `L ${x + w - c0} ${y}`,\n    `C ${x + w - c1} ${y} ${x + w - c2} ${y} ${x + w - c3} ${y + c6}`,\n    `C ${x + w - c4} ${y + c5} ${x + w - c5} ${y + c4} ${x + w - c6} ${y + c3}`,\n    `C ${x + w} ${y + c2} ${x + w} ${y + c1} ${x + w} ${y + c0}`,\n    `L ${x + w} ${y + h - c0}`,\n    `C ${x + w} ${y + h - c1} ${x + w} ${y + h - c2} ${x + w - c6} ${y + h - c3}`,\n    `C ${x + w - c5} ${y + h - c4} ${x + w - c4} ${y + h - c5} ${x + w - c3} ${y + h - c6}`,\n    `C ${x + w - c2} ${y + h} ${x + w - c1} ${y + h} ${x + w - c0} ${y + h}`,\n    `L ${x + c0} ${y + h}`,\n    `C ${x + c1} ${y + h} ${x + c2} ${y + h} ${x + c3} ${y + h - c6}`,\n    `C ${x + c4} ${y + h - c5} ${x + c5} ${y + h - c4} ${x + c6} ${y + h - c3}`,\n    `C ${x} ${y + h - c2} ${x} ${y + h - c1} ${x} ${y + h - c0}`,\n    `L ${x} ${y + c0}`,\n    `C ${x} ${y + c1} ${x} ${y + c2} ${x + c6} ${y + c3}`,\n    `C ${x + c5} ${y + c4} ${x + c4} ${y + c5} ${x + c3} ${y + c6}`,\n    `C ${x + c2} ${y} ${x + c1} ${y} ${x + c0} ${y}`,\n    \"Z\",\n  ].join(\" \")\n}\n\n/* ── The grab ─────────────────────────────────────────────────────────────\n   Press any body and drag: a chain of beads is drawn out of its rim as a\n   liquid finger, the grabbed body leans after it, and the release whips the\n   whole thing home on the house spring. */\n\n/** How far from the grabbed body's centre the edge gives before the sponge stops. */\nexport const GRAB_MAX = 44\n\n/* Four beads shaped like a real finger: THICK at the root so it melts into the\n   body it is pulled from, THINNEST through the middle, a modest bulb at the\n   head. `thin` is how much a bead narrows at full tension; `lag` grows down the\n   chain so the beads trail the head and keep the bridge unbroken. */\nexport const GRAB_CHAIN = [\n  { follow: 1, size: 0.85, thin: 0.06, lag: 0.16 },\n  { follow: 0.76, size: 0.62, thin: 0.2, lag: 0.19 },\n  { follow: 0.52, size: 0.66, thin: 0.18, lag: 0.22 },\n  { follow: 0.28, size: 0.82, thin: 0.1, lag: 0.25 },\n] as const\n\n/** The trigger, or the index of one of the host's auxiliary bodies. */\nexport type StretchTarget = \"trigger\" | number\n\ntype Bit = Element | null\n\n/** The slice of a pointer event the controller needs - React's synthetic event\n    satisfies it structurally, so nothing here is bound to React. */\nexport interface StretchPointerEvent {\n  button: number\n  pointerId: number\n  clientX: number\n  clientY: number\n  currentTarget: { setPointerCapture(pointerId: number): void }\n}\n\n/**\n * What a component must tell the controller about its own picture. The\n * controller decides WHEN things move; the host knows HOW its goo is wired.\n */\nexport interface StretchHost {\n  /** Diameter of the trigger, in the units the geometry is authored in. */\n  buttonSize: number\n  /** Rendered size ÷ authored size. Pointer deltas arrive in rendered pixels\n      while every length here is an authored unit, so a host that scales itself\n      has to say by how much or the pull runs long and off-axis. @default 1 */\n  scale?: number\n  /** How far a pulled aux body leans after the finger. 0.22 for a droplet,\n      0.14 for a heavier panel. */\n  auxLean: number\n  /** The positioned root every pull is measured against. */\n  root(): HTMLElement | null\n  /** Blob, crisp body and glyph - everything that squashes together, or the\n      trigger visibly tears apart. */\n  triggerBits(): Bit[]\n  /** Blob and crisp body only: these additionally wear the rotated directional\n      stretch, which must not reach the glyph. */\n  triggerStretchBits(): Bit[]\n  triggerIcon(): Bit\n  chain(): (SVGCircleElement | null)[]\n  /** A pulled aux body's blob, crisp body and hit area. */\n  auxBits(index: number): Bit[]\n  /** Go liquid at the grab blur. Also the host's chance to hide any mass parked\n      inside the trigger: a hidden body that cannot ride the trigger's lean pokes\n      out of the moving silhouette as a hump. */\n  liquidOn(target: StretchTarget): void\n  /** Hand the picture back to the crisp bodies, `delay` seconds from now. */\n  handoff(delay: number): void\n}\n\nexport interface MercuryStretch {\n  /** Begin a grab. `base` is the grabbed body's centre, in the root's space. */\n  beginGrab(target: StretchTarget, event: StretchPointerEvent, base: { x: number; y: number }): void\n  pointerMove(event: { clientX: number; clientY: number }): void\n  release(): void\n  /** For click handlers: reports whether this click is the tail of a stretch\n      and must be swallowed instead of toggling anything. */\n  consumeClick(): boolean\n  /** Kill the release choreography - call before any full open/close run. */\n  kill(): void\n}\n\n/* A pull under this many px is an un-press, not a snap-back. */\nconst STRETCH_FLOOR = 12\n/* Dead zone before the finger starts to follow. */\nconst GRAB_DEADZONE = 6\n\n/**\n * The grab gesture. One controller per component instance; it owns the maths,\n * the tweens and the press bookkeeping, and calls back into the host for\n * everything that is that component's own picture.\n */\nexport function useMercuryStretch(host: StretchHost): MercuryStretch {\n  const hostRef = React.useRef(host)\n  hostRef.current = host\n\n  const ref = React.useRef<MercuryStretch | null>(null)\n  if (ref.current === null) ref.current = createStretch(() => hostRef.current)\n\n  React.useEffect(() => {\n    const stretch = ref.current\n    return () => stretch?.kill()\n  }, [])\n\n  return ref.current\n}\n\nfunction createStretch(getHost: () => StretchHost): MercuryStretch {\n  let pressed = false\n  /* Cursor distance from the grab base during a hold - decides whether the\n     release is a plain un-press or a sponge snap-back. */\n  let stretchDist = 0\n  let suppressClick = false\n  let target: StretchTarget | null = null\n  let base = { x: 0, y: 0 }\n  /* The release runs on its own list: killing a component's open/close from a\n     mere release would freeze its drops mid-flight. */\n  let running: { stop(): void }[] = []\n\n  const stop = () => {\n    running.forEach((animation) => animation.stop())\n    running = []\n  }\n\n  const play = (...animations: { stop(): void }[]) => {\n    running.push(...animations)\n  }\n\n  const reduced = () =>\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n  const release = () => {\n    if (!pressed) return\n    pressed = false\n\n    const host = getHost()\n    const grabbed = target\n    target = null\n    const home = base\n    const wasStretched = stretchDist > STRETCH_FLOOR\n    suppressClick = wasStretched\n    stretchDist = 0\n\n    stop()\n\n    if (wasStretched) {\n      /* The chain whips home head-first, each bead a breath behind, and\n         dissolves into the rim as it lands. */\n      GRAB_CHAIN.forEach((_link, index) => {\n        const bead = host.chain()[index]\n        if (!bead) return\n        play(\n          animate(bead, { x: home.x, y: home.y }, { ...SPRING_HOUSE, delay: index * 0.025 }),\n          animate(bead, { scale: 0 }, { duration: 0.2, ease: \"easeIn\", delay: 0.16 + index * 0.025 }),\n        )\n      })\n    }\n\n    if (typeof grabbed === \"number\") {\n      /* A pulled aux body rides its position spring home - the ring IS the\n         shake. (A scale wobble would orbit its remote transform origin.) */\n      play(animate(host.auxBits(grabbed).filter(isEl), { x: 0, y: 0 }, SPRING_HOUSE))\n    } else {\n      /* Rotation springs home with the position rather than being zeroed after\n         the handoff: the circle hides its own rotation but its shadow does not,\n         and a set() made that shadow visibly jump as the crisp body took over. */\n      play(\n        animate(\n          host.triggerStretchBits().filter(isEl),\n          { x: 0, y: 0, rotate: 0 },\n          SPRING_HOUSE,\n        ),\n      )\n      const icon = host.triggerIcon()\n      if (icon) play(animate(icon, { x: 0, y: 0 }, SPRING_HOUSE))\n      const bits = host.triggerBits().filter(isEl)\n      play(\n        wasStretched\n          ? animate(\n              bits,\n              { scaleX: [1.2, 0.93, 1], scaleY: [0.82, 1.09, 1] },\n              { duration: 0.6, times: [0.15, 0.33, 1], ease: \"easeOut\", delay: 0.12 },\n            )\n          : animate(bits, { scaleX: 1, scaleY: 1 }, SPRING_HOUSE),\n      )\n    }\n\n    host.handoff(wasStretched ? 0.45 : 0.3)\n  }\n\n  const beginGrab = (\n    next: StretchTarget,\n    event: StretchPointerEvent,\n    from: { x: number; y: number },\n  ) => {\n    if (reduced() || event.button !== 0) return\n    try {\n      event.currentTarget.setPointerCapture(event.pointerId)\n    } catch {\n      // The pointer may already be gone; the stretch just will not follow.\n    }\n    const host = getHost()\n    pressed = true\n    stretchDist = 0\n    target = next\n    base = { x: from.x, y: from.y }\n    /* Clear a stale suppression, or this gesture's click eats the last one's. */\n    suppressClick = false\n\n    host.liquidOn(next)\n    host.chain().forEach((bead) => {\n      if (bead) animate(bead, { x: from.x, y: from.y, scale: 0.4 }, { duration: 0 })\n    })\n    /* Capture can fail silently; a window-level backstop guarantees the release\n       lands even if the pointer lets go far outside the button. */\n    window.addEventListener(\"pointerup\", release, { once: true })\n\n    if (next === \"trigger\") {\n      stop()\n      play(\n        animate(host.triggerBits().filter(isEl), { scaleX: 0.85, scaleY: 0.85 }, {\n          duration: 0.1,\n          ease: EASE_OUT_STRONG,\n        }),\n      )\n    }\n  }\n\n  /* Hold and drag: only the grabbed PIECE of the edge stretches. The head\n     chases the cursor a clamped distance, the beads trail at fractions of the\n     pull, and the goo renders the lot as one finger drawn out of the rim. */\n  const pointerMove = (event: { clientX: number; clientY: number }) => {\n    if (!pressed || target === null || reduced()) return\n    const host = getHost()\n    const rect = host.root()?.getBoundingClientRect()\n    if (!rect) return\n\n    const scale = host.scale ?? 1\n    const half = host.buttonSize / 2\n    const dx = (event.clientX - rect.left) / scale - (half + base.x)\n    const dy = (event.clientY - rect.top) / scale - (half + base.y)\n    const dist = Math.hypot(dx, dy)\n    stretchDist = dist\n    const pull = Math.min(Math.max(0, dist - GRAB_DEADZONE) * 0.7, GRAB_MAX)\n    const tension = pull / GRAB_MAX\n    const ux = dist > 0 ? dx / dist : 0\n    const uy = dist > 0 ? dy / dist : 0\n\n    GRAB_CHAIN.forEach((link, index) => {\n      const bead = host.chain()[index]\n      if (!bead) return\n      animate(\n        bead,\n        {\n          x: base.x + ux * pull * link.follow,\n          y: base.y + uy * pull * link.follow,\n          scale: link.size * (1 - tension * link.thin),\n        },\n        { duration: link.lag, ease: \"easeOut\" },\n      )\n    })\n\n    if (typeof target === \"number\") {\n      /* The pulled body leans after the finger; its shape stays put, because a\n         scale would orbit its remote transform origin. */\n      animate(host.auxBits(target).filter(isEl), {\n        x: ux * pull * host.auxLean,\n        y: uy * pull * host.auxLean,\n      }, { duration: 0.25, ease: \"easeOut\" })\n      return\n    }\n\n    /* The trigger leans into the pull and stretches a touch along it, so the\n       mass visibly follows the grabbed piece. */\n    animate(\n      host.triggerStretchBits().filter(isEl),\n      {\n        x: ux * pull * 0.18,\n        y: uy * pull * 0.18,\n        rotate: (Math.atan2(dy, dx) * 180) / Math.PI,\n        scaleX: (1 + tension * 0.12) * 0.85,\n        scaleY: (1 - tension * 0.06) * 0.85,\n      },\n      { duration: 0.25, ease: \"easeOut\" },\n    )\n    const icon = host.triggerIcon()\n    if (icon) {\n      animate(icon, { x: ux * pull * 0.28, y: uy * pull * 0.28 }, { duration: 0.25, ease: \"easeOut\" })\n    }\n  }\n\n  const consumeClick = () => {\n    pressed = false\n    if (!suppressClick) return false\n    suppressClick = false\n    return true\n  }\n\n  return { beginGrab, pointerMove, release, consumeClick, kill: stop }\n}\n\nconst isEl = (el: Bit): el is Element => el !== null\n"
    }
  ]
}
