{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mercury-menu",
  "type": "registry:ui",
  "title": "Mercury Menu",
  "description": "A dropdown the button pours out of itself. The trigger swells, the panel oozes up under its own weight and then fires into place on a pop spring with the rows condensing in bottom-up; closing crashes it back down in one dive and the button takes the hit with a splat. Press and drag the button or the open panel and a liquid finger stretches out of its rim.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crafterui.com/r/mercury-surface.json"
  ],
  "files": [
    {
      "path": "registry/crafterui/ui/mercury-menu.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n// A dropdown the button pours out of itself.\n//\n// The trigger stays where it is. It swells, and a panel is drawn up out of it -\n// oozing to about 40% under its own weight before a pop spring fires it the\n// rest of the way to the gap above the button, overshooting and ringing still.\n// The rows condense in behind it, bottom-up, the direction the drop grew.\n//\n// Closing is not the opening reversed. The panel dives in one fast crash: it\n// necks, the rows blink out, the mass is drunk back into the circle, and the\n// button takes the hit with a splat. Around 90ms of that dive the two are one\n// merged shape - the only frames where the whole trick is visible.\n//\n// The picture is drawn twice and exactly one copy is ever on screen: real CSS\n// bodies at rest, and while anything moves a goo layer that draws its own rim\n// exactly where those borders were. Grab either body and drag - see\n// mercury-surface for the gesture.\nimport * as React from \"react\"\nimport { animate } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  EASE_OUT_STRONG,\n  GRAB_CHAIN,\n  GooFilter,\n  SPRING_HOUSE,\n  SPRING_POP,\n  setGooBlur,\n  squirclePath,\n  useMercuryStretch,\n} from \"@/registry/crafterui/ui/mercury-surface\"\n\nexport interface MercuryMenuItem {\n  /** Stable key; falls back to the label. @default undefined */\n  id?: string\n  /** Row text. */\n  label: string\n  /** Glyph before the label. @default undefined */\n  icon?: React.ReactNode\n  /** Fires when the row is chosen. The menu closes either way. @default undefined */\n  onSelect?: () => void\n}\n\nexport interface MercuryMenuProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\" | \"onSelect\"> {\n  /** Rows, top to bottom. */\n  items: MercuryMenuItem[]\n  /** Open state when controlled. Leave unset for internal state. @default undefined */\n  open?: boolean\n  /** Open on mount when uncontrolled. @default false */\n  defaultOpen?: boolean\n  /** Fires on every change, from click, Escape or an outside press. @default undefined */\n  onOpenChange?: (open: boolean) => void\n  /** Glyph on the trigger. Throws 135° into a cross as the menu opens. @default a plus */\n  icon?: React.ReactNode\n  /** Panel width in 32px-trigger units - the squircle needs a fixed one. @default 156 */\n  panelWidth?: number\n  /** Rendered width of the trigger, px. Everything scales off it. @default 32 */\n  size?: number\n  /** Names the menu for a screen reader. @default \"Menu\" */\n  label?: string\n}\n\n/* Layout is authored at 32px and scaled as a whole, so the rim calibration -\n   solved against a 32px disc - holds at every size. */\nconst BUTTON = 32\nconst HALF = BUTTON / 2\nconst ROW = 30\nconst PANEL_PAD = 7\nconst PANEL_GAP = 14\nconst PANEL_RADIUS = 16\n/* Resting scale: the shrunk panel must hide entirely inside the trigger's 16px\n   radius. Its farthest corner is ~151px from the origin, so 0.08 leaves ~12. */\nconst REST_SCALE = 0.08\n/* A panel at rest sits very slightly off-square, so the pop has a tilt to swing\n   out of rather than only a size to grow through. */\nconst REST_TILT = -3\n/* Room past any body's edge for the longest thing the liquid can do: a 44px\n   pull, plus the finger's head, plus three σ of blur tail. */\nconst GOO_PAD = 76\n\n/* The panel grows out of, and dives into, the button it overlaps - one mass, no\n   far necks - so a light blur carries the whole look. */\nconst BLUR_ACTIVE = 5\nconst BLUR_REST = 1\nconst BLUR_GRAB = 5\n\nexport function MercuryMenu({\n  items,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  icon,\n  panelWidth = 156,\n  size = BUTTON,\n  label = \"Menu\",\n  className,\n  ...props\n}: MercuryMenuProps) {\n  const reactId = React.useId()\n  const menuId = `mercury-menu-${reactId.replace(/:/g, \"\")}`\n  const gooId = `mercury-menu-goo-${reactId.replace(/:/g, \"\")}`\n\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const gooRef = React.useRef<SVGSVGElement>(null)\n  const bodiesRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n  const triggerBodyRef = React.useRef<HTMLDivElement>(null)\n  const triggerBlobRef = React.useRef<SVGCircleElement>(null)\n  const iconRef = React.useRef<HTMLSpanElement>(null)\n  const chainRefs = React.useRef<(SVGCircleElement | null)[]>([])\n  const panelBodyRef = React.useRef<SVGSVGElement>(null)\n  const panelBlobRef = React.useRef<SVGPathElement>(null)\n  const panelRef = React.useRef<HTMLDivElement>(null)\n  const rowRefs = React.useRef<(HTMLButtonElement | null)[]>([])\n\n  const [internal, setInternal] = React.useState(defaultOpen)\n  const isOpen = open ?? internal\n  /* The stretch host is built once and must not read a stale render's state. */\n  const openRef = React.useRef(isOpen)\n  openRef.current = isOpen\n\n  const geo = React.useMemo(() => {\n    const height = PANEL_PAD * 2 + items.length * ROW\n    /* The button's centre in the panel's own coordinates - scaling around it is\n       what makes the panel grow out of the button rather than out of the air. */\n    const originY = height + PANEL_GAP + HALF\n    const cx = Math.max(panelWidth / 2, HALF) + GOO_PAD\n    const cy = originY + GOO_PAD\n    return {\n      height,\n      originX: panelWidth / 2,\n      originY,\n      left: (BUTTON - panelWidth) / 2,\n      top: -(height + PANEL_GAP),\n      cx,\n      cy,\n      width: cx * 2,\n      canvasHeight: cy + HALF + GOO_PAD,\n      path: squirclePath(0, 0, panelWidth, height, PANEL_RADIUS),\n      /* The same squircle again, placed in the goo canvas. (cx, cy) there is\n         the button's CENTRE, while the panel's left/top are measured from the\n         anchor's corner - hence the half-button shift, without which the goo\n         panel sits 16 units off the crisp one it is supposed to replace. */\n      blobPath: squirclePath(\n        cx - HALF + (BUTTON - panelWidth) / 2,\n        cy - HALF - (height + PANEL_GAP),\n        panelWidth,\n        height,\n        PANEL_RADIUS,\n      ),\n    }\n  }, [items.length, panelWidth])\n\n  const run = React.useRef<{ stop(): void }[]>([])\n  const timers = React.useRef<number[]>([])\n  const killRun = React.useCallback(() => {\n    run.current.forEach((animation) => animation.stop())\n    run.current = []\n    timers.current.forEach((id) => window.clearTimeout(id))\n    timers.current = []\n  }, [])\n  const play = (...animations: ({ stop(): void } | null | undefined)[]) => {\n    animations.forEach((animation) => animation && run.current.push(animation))\n  }\n  const at = (delay: number, fn: () => void) => {\n    timers.current.push(window.setTimeout(fn, delay * 1000))\n  }\n\n  const bits = React.useCallback(\n    () => [triggerBlobRef.current, triggerBodyRef.current, iconRef.current].filter(isEl),\n    [],\n  )\n  const stretchBits = React.useCallback(\n    () => [triggerBlobRef.current, triggerBodyRef.current].filter(isEl),\n    [],\n  )\n  const panelBits = React.useCallback(\n    () => [panelBlobRef.current, panelBodyRef.current, panelRef.current].filter(isEl),\n    [],\n  )\n  const rows = React.useCallback(() => rowRefs.current.filter(isEl), [])\n\n  const clearTint = React.useCallback(() => {\n    ;[triggerBlobRef.current, panelBlobRef.current, ...chainRefs.current].forEach((blob) =>\n      blob?.style.removeProperty(\"fill\"),\n    )\n  }, [])\n\n  const goLiquid = React.useCallback((blur: 1 | 5 | 7) => {\n    rootRef.current?.setAttribute(\"data-liquid\", \"\")\n    if (gooRef.current) gooRef.current.style.opacity = \"1\"\n    if (bodiesRef.current) bodiesRef.current.style.opacity = \"0\"\n    setGooBlur(gooRef.current, blur)\n  }, [])\n\n  /* Cross-fade back to the identical crisp picture while the blur is still at\n     its working width - thinning the rim on screen blinks every border, so the\n     blur only resets once the goo is already hidden. */\n  const handoff = React.useCallback(\n    (delay: number, fade = 0.15) => {\n      /* Explicit start values, not just targets: the instant states below are\n         written straight to style.opacity, which Motion does not see - it keeps\n         its own cached value per element and would read this fade as \"already\n         at 0\" and skip it, leaving the goo painted over the crisp picture. */\n      play(\n        animate(gooRef.current!, { opacity: [1, 0] }, { duration: fade, ease: \"easeOut\", delay }),\n        animate(bodiesRef.current!, { opacity: [0, 1] }, { duration: fade, ease: \"easeOut\", delay }),\n      )\n      at(delay + fade, () => {\n        setGooBlur(gooRef.current, BLUR_REST)\n        rootRef.current?.removeAttribute(\"data-liquid\")\n        clearTint()\n      })\n    },\n    [clearTint],\n  )\n\n  const stretch = useMercuryStretch({\n    buttonSize: BUTTON,\n    scale: size / BUTTON,\n    auxLean: 0.14,\n    root: () => rootRef.current,\n    triggerBits: bits,\n    triggerStretchBits: stretchBits,\n    triggerIcon: () => iconRef.current,\n    chain: () => chainRefs.current,\n    auxBits: panelBits,\n    liquidOn: (target) => {\n      goLiquid(BLUR_GRAB)\n      /* While the menu is closed the shrunk panel is parked INSIDE the circle,\n         and it has to sit the grab out: it cannot ride the trigger's lean, so\n         left in the goo it pokes out of the moving silhouette as a hump. */\n      panelBlobRef.current?.style.setProperty(\n        \"opacity\",\n        target === \"trigger\" && !openRef.current ? \"0\" : \"1\",\n      )\n      const held = target === \"trigger\" ? triggerBlobRef.current : panelBlobRef.current\n      /* The held piece and its finger wear the button's own hover tint, mixed\n         rather than taken from a token: `--muted` is a whole step lighter than\n         the surface in dark mode, which turns a 5% wash into a grey slug. */\n      ;[held, ...chainRefs.current].forEach((blob) =>\n        blob?.style.setProperty(\n          \"fill\",\n          \"color-mix(in oklab, var(--foreground) 5%, var(--background))\",\n        ),\n      )\n    },\n    handoff: (delay) => handoff(delay),\n  })\n\n  /* Rest layout. Written through Motion rather than as a style string: Motion\n     owns each transform component separately and cannot read one back out of a\n     `transform` something else authored. */\n  const layout = React.useCallback(\n    (opened: boolean, instant: boolean) => {\n      const options = { duration: instant ? 0 : 0.2 }\n      animate(\n        panelBits(),\n        {\n          scaleX: opened ? 1 : REST_SCALE,\n          scaleY: opened ? 1 : REST_SCALE,\n          rotate: opened ? 0 : REST_TILT,\n          x: 0,\n          y: 0,\n        },\n        options,\n      )\n      animate(bits(), { scaleX: 1, scaleY: 1, x: 0, y: 0 }, options)\n      animate(rows(), { opacity: opened ? 1 : 0, y: 0 }, options)\n      animate(chainRefs.current.filter(isEl), { scale: 0, x: 0, y: 0 }, { duration: 0 })\n      if (iconRef.current) animate(iconRef.current, { rotate: opened ? 135 : 0 }, options)\n      ;[panelBodyRef.current, panelRef.current].forEach((el) => {\n        if (el) el.style.opacity = opened ? \"1\" : \"0\"\n      })\n      panelBlobRef.current?.style.setProperty(\"opacity\", \"1\")\n      if (gooRef.current) gooRef.current.style.opacity = \"0\"\n      if (bodiesRef.current) bodiesRef.current.style.opacity = \"1\"\n      rootRef.current?.removeAttribute(\"data-liquid\")\n    },\n    [bits, panelBits, rows],\n  )\n\n  /* One layout pass on mount, and again only when the row count changes - see\n     mercury-dial: keyed any finer, an incidental re-render snaps a running\n     choreography back to its rest pose. */\n  const layoutRef = React.useRef(layout)\n  layoutRef.current = layout\n  React.useEffect(() => {\n    layoutRef.current(openRef.current, true)\n  }, [items.length])\n  React.useEffect(() => killRun, [killRun])\n\n  const reduced = () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n  const commit = React.useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternal(next)\n      onOpenChange?.(next)\n    },\n    [onOpenChange, open],\n  )\n\n  const runOpen = React.useCallback(() => {\n    killRun()\n    stretch.kill()\n    commit(true)\n    if (reduced()) {\n      layout(true, true)\n      return\n    }\n\n    clearTint()\n    goLiquid(BLUR_ACTIVE)\n    ;[panelBodyRef.current, panelRef.current].forEach((el) => {\n      if (el) el.style.opacity = \"1\"\n    })\n    /* Re-arm the panel blob a closed-state grab may have hidden. */\n    panelBlobRef.current?.style.setProperty(\"opacity\", \"1\")\n\n    play(\n      /* The button swells as the drop gathers, then shakes it off. */\n      animate(bits(), { x: 0, y: 0, scaleX: 1.16, scaleY: 1.16 }, { duration: 0.13, ease: EASE_OUT_STRONG }),\n      animate(bits(), { scaleX: 1, scaleY: 1 }, { ...SPRING_HOUSE, delay: 0.15 }),\n      animate(stretchBits(), { rotate: 0 }, { duration: 0 }),\n      animate(chainRefs.current.filter(isEl), { x: 0, y: 0, scale: 0 }, { duration: 0.14, ease: EASE_OUT_STRONG }),\n      iconRef.current && animate(iconRef.current, { rotate: 135 }, { ...SPRING_HOUSE, delay: 0.02 }),\n      /* The panel oozes out, then fires on the pop spring - swinging upright\n         from its rest tilt, the two axes 30ms out of phase so it lands with a\n         jelly wobble rather than as a rectangle that grew. */\n      animate(panelBits(), { x: 0, y: 0 }, { duration: 0 }),\n      animate(panelBits(), { scaleX: 0.42, scaleY: 0.42 }, { duration: 0.14, ease: \"easeInOut\", delay: 0.03 }),\n      animate(panelBits(), { scaleY: 1, rotate: 0 }, { ...SPRING_POP, delay: 0.17 }),\n      animate(panelBits(), { scaleX: 1 }, { ...SPRING_POP, delay: 0.2 }),\n    )\n\n    /* Rows condense bottom-up - the direction the drop grew. */\n    const list = rows()\n    list.forEach((row, i) => {\n      const reverse = list.length - 1 - i\n      play(\n        animate(\n          row,\n          { opacity: [0, 1], y: [12, 0] },\n          { duration: 0.19, ease: [0.34, 1.6, 0.64, 1], delay: 0.2 + reverse * 0.03 },\n        ),\n      )\n    })\n\n    handoff(0.47, 0.16)\n  }, [bits, clearTint, commit, goLiquid, handoff, killRun, layout, panelBits, rows, stretch, stretchBits])\n\n  const runClose = React.useCallback(\n    (fromTrigger: boolean) => {\n      killRun()\n      stretch.kill()\n      commit(false)\n      if (reduced()) {\n        layout(false, true)\n        return\n      }\n\n      clearTint()\n      goLiquid(BLUR_ACTIVE)\n      panelBlobRef.current?.style.setProperty(\"opacity\", \"1\")\n\n      play(\n        iconRef.current && animate(iconRef.current, { rotate: 0 }, { ...SPRING_HOUSE, duration: 0.22 }),\n        animate(stretchBits(), { rotate: 0 }, { duration: 0 }),\n        animate(bits(), { x: 0, y: 0 }, { duration: 0.1, ease: EASE_OUT_STRONG }),\n        animate(chainRefs.current.filter(isEl), { x: 0, y: 0, scale: 0 }, { duration: 0.1, ease: EASE_OUT_STRONG }),\n        /* One fast dive: the panel necks, crashes ONTO the button - a flash of\n           merged mass around 90ms in - and is drunk straight back down. */\n        animate(panelBits(), { x: 0, y: 0 }, { duration: 0.1, ease: EASE_OUT_STRONG }),\n        animate(panelBits(), { scaleX: 0.35 }, { duration: 0.08, ease: [0.5, 0, 0.1, 1] }),\n        animate(panelBits(), { scaleY: 0.35, rotate: REST_TILT }, { duration: 0.08, ease: [0.5, 0, 0.1, 1], delay: 0.015 }),\n        animate(rows(), { opacity: [1, 0], y: [0, 4] }, { duration: 0.05, ease: \"easeIn\" }),\n        animate(\n          panelBits(),\n          { scaleX: REST_SCALE, scaleY: REST_SCALE },\n          { duration: 0.06, ease: \"easeIn\", delay: 0.1 },\n        ),\n        fromTrigger ? animate(bits(), { scaleX: 1, scaleY: 1 }, { ...SPRING_HOUSE, duration: 0.1 }) : undefined,\n        /* The splat carries the rest of the story. */\n        animate(\n          bits(),\n          { scaleX: [1.18, 0.95, 1], scaleY: [0.84, 1.06, 1] },\n          { duration: 0.36, times: [0.14, 0.33, 1], ease: \"easeOut\", delay: 0.14 },\n        ),\n      )\n\n      at(0.17, () => {\n        ;[panelBodyRef.current, panelRef.current].forEach((el) => {\n          if (el) el.style.opacity = \"0\"\n        })\n      })\n\n      handoff(0.22, 0.1)\n    },\n    [bits, clearTint, commit, goLiquid, handoff, killRun, layout, panelBits, rows, stretch, stretchBits],\n  )\n\n  const toggle = () => {\n    if (stretch.consumeClick()) return\n    if (isOpen) runClose(true)\n    else runOpen()\n  }\n\n  React.useEffect(() => {\n    if (!isOpen) return\n    const onPointerDown = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) runClose(false)\n    }\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Escape\") return\n      runClose(false)\n      triggerRef.current?.focus()\n    }\n    document.addEventListener(\"pointerdown\", onPointerDown)\n    document.addEventListener(\"keydown\", onKeyDown)\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointerDown)\n      document.removeEventListener(\"keydown\", onKeyDown)\n    }\n  }, [isOpen, runClose])\n\n  const scale = size / BUTTON\n  const panelBox: React.CSSProperties = {\n    width: panelWidth,\n    height: geo.height,\n    left: geo.left,\n    top: geo.top,\n    transformOrigin: `${geo.originX}px ${geo.originY}px`,\n  }\n\n  return (\n    <div\n      className={cn(\"relative select-none\", className)}\n      style={{ width: size, height: size }}\n      {...props}\n    >\n      <div\n        ref={rootRef}\n        className=\"relative\"\n        style={{\n          width: BUTTON,\n          height: BUTTON,\n          transform: scale === 1 ? undefined : `scale(${scale})`,\n          transformOrigin: \"0 0\",\n        }}\n      >\n        {/* Layer 1 - the resting picture: real borders, real shadows. */}\n        <div ref={bodiesRef} className=\"absolute inset-0\" aria-hidden=\"true\">\n          <div\n            ref={triggerBodyRef}\n            className=\"bg-background border-border absolute rounded-full border shadow-[0_3px_6px_-1px_rgb(0_0_0/0.10)]\"\n            style={{ width: BUTTON, height: BUTTON, left: 0, top: 0 }}\n          />\n          {/* The crisp panel is the SAME squircle the goo blob draws, so the two\n              pictures share one silhouette and the handoff has nothing to see. */}\n          <svg\n            ref={panelBodyRef}\n            className=\"absolute overflow-visible opacity-0\"\n            style={{ ...panelBox, filter: \"drop-shadow(0 3px 6px rgb(0 0 0 / 0.10))\" }}\n            width={panelWidth}\n            height={geo.height}\n            viewBox={`0 0 ${panelWidth} ${geo.height}`}\n          >\n            <path d={geo.path} className=\"fill-background stroke-border\" strokeWidth={1} />\n          </svg>\n        </div>\n\n        {/* Layer 2 - the liquid. A real <svg>, because Safari will not reliably\n            repaint a CSS `filter: url()` on an HTML element whose children\n            animate. It draws its own rim and one shadow for the whole mass. */}\n        <svg\n          ref={gooRef}\n          width={geo.width}\n          height={geo.canvasHeight}\n          viewBox={`0 0 ${geo.width} ${geo.canvasHeight}`}\n          className=\"pointer-events-none absolute overflow-visible opacity-0\"\n          style={{\n            left: HALF - geo.cx,\n            top: HALF - geo.cy,\n            filter: \"drop-shadow(0 3px 6px rgb(0 0 0 / 0.10))\",\n          }}\n          aria-hidden=\"true\"\n          focusable=\"false\"\n        >\n          <defs>\n            <GooFilter id={gooId} width={geo.width} height={geo.canvasHeight} blur={BLUR_REST} />\n          </defs>\n          <g filter={`url(#${gooId})`}>\n            <path\n              ref={panelBlobRef}\n              d={geo.blobPath}\n              className=\"fill-background [transform-box:fill-box]\"\n              style={{ transformOrigin: `${geo.originX}px ${geo.originY}px` }}\n            />\n            <circle\n              ref={triggerBlobRef}\n              cx={geo.cx}\n              cy={geo.cy}\n              r={HALF}\n              className=\"fill-background [transform-box:fill-box] [transform-origin:50%_50%]\"\n            />\n            {GRAB_CHAIN.map((link, i) => (\n              <circle\n                key={link.follow}\n                ref={(el) => {\n                  chainRefs.current[i] = el\n                }}\n                cx={geo.cx}\n                cy={geo.cy}\n                r={11}\n                className=\"fill-background [transform-box:fill-box] [transform-origin:50%_50%]\"\n              />\n            ))}\n          </g>\n        </svg>\n\n        {/* Layer 3 - rows and hit areas, riding the same tweens above the\n            liquid, so the text stays crisp and accessibility lives in real\n            DOM buttons. */}\n        <div\n          ref={panelRef}\n          id={menuId}\n          role=\"menu\"\n          aria-label={label}\n          inert={!isOpen}\n          className=\"absolute opacity-0\"\n          style={{ ...panelBox, padding: PANEL_PAD }}\n          onPointerDown={(event) => {\n            /* The finger comes out of the edge you actually pressed, not out of\n               the panel's middle - from the middle it would be drawn entirely\n               inside a body 168 units wide and never show at all. */\n            if (!isOpen) return\n            const rect = rootRef.current?.getBoundingClientRect()\n            if (!rect) return\n            stretch.beginGrab(0, event, {\n              x: (event.clientX - rect.left) / (size / BUTTON) - HALF,\n              y: (event.clientY - rect.top) / (size / BUTTON) - HALF,\n            })\n          }}\n          onPointerMove={(event) => stretch.pointerMove(event)}\n          onPointerUp={() => stretch.release()}\n          onPointerCancel={() => stretch.release()}\n        >\n          {items.map((item, i) => (\n            <button\n              key={item.id ?? item.label}\n              ref={(el) => {\n                rowRefs.current[i] = el\n              }}\n              type=\"button\"\n              role=\"menuitem\"\n              className=\"text-foreground/85 hover:text-foreground hover:bg-foreground/5 focus-visible:outline-foreground flex w-full items-center gap-2 rounded-[10px] px-2 text-left text-[13px] outline-none transition-colors focus-visible:outline-2 focus-visible:-outline-offset-2\"\n              style={{ height: ROW }}\n              onClick={() => {\n                if (stretch.consumeClick()) return\n                item.onSelect?.()\n                runClose(false)\n              }}\n            >\n              {item.icon ? <span className=\"flex shrink-0\">{item.icon}</span> : null}\n              <span className=\"truncate\">{item.label}</span>\n            </button>\n          ))}\n        </div>\n\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          className=\"text-foreground/80 hover:text-foreground hover:bg-foreground/5 [[data-liquid]_&]:bg-transparent focus-visible:outline-foreground absolute inset-0 grid place-items-center rounded-full outline-none transition-colors focus-visible:outline-2 focus-visible:outline-offset-1\"\n          aria-expanded={isOpen}\n          aria-haspopup=\"menu\"\n          aria-controls={menuId}\n          aria-label={label}\n          onClick={toggle}\n          onPointerDown={(event) => stretch.beginGrab(\"trigger\", event, { x: 0, y: 0 })}\n          onPointerMove={(event) => stretch.pointerMove(event)}\n          onPointerUp={() => stretch.release()}\n          onPointerCancel={() => stretch.release()}\n        >\n          <span ref={iconRef} className=\"flex\">\n            {icon ?? <PlusGlyph />}\n          </span>\n        </button>\n      </div>\n    </div>\n  )\n}\n\n/** The default trigger glyph. A plus, so the 135° throw lands it as a cross. */\nfunction PlusGlyph() {\n  return (\n    <svg viewBox=\"0 0 14 14\" className=\"size-3.5\" aria-hidden=\"true\">\n      <path\n        d=\"M7 1.5v11M1.5 7h11\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.6\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  )\n}\n\nconst isEl = <T,>(el: T | null): el is T => el !== null\n"
    }
  ]
}
