{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "minecraft-button",
  "type": "registry:ui",
  "title": "MinecraftButton",
  "description": "A pixel stone menu button that cracks every few taps, shatters into chips, and drops itself as a bobbing item you pick up to respawn it.",
  "files": [
    {
      "path": "components/evil-buttons/minecraft-button.tsx",
      "type": "registry:ui",
      "target": "components/evil-buttons/minecraft-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\n\n/** One texture pixel, in CSS pixels. Everything snaps to this grid. */\nconst PX = 3;\n/** Stone tile size in texels (Minecraft block textures are 16×16). */\nconst TILE = 16;\n\nconst STONE_BASE = \"#7d7d7d\";\nconst STONE_SHADES = [\"#747474\", \"#6a6a6a\", \"#868686\", \"#8f8f8f\", \"#5f5f5f\"];\n\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** A seeded 16×16 stone texture, so server and client render the same tile. */\nconst STONE_TEXTURE = (() => {\n  const rand = mulberry32(1337);\n  let rects = \"\";\n  for (let y = 0; y < TILE; y++) {\n    for (let x = 0; x < TILE; x++) {\n      if (rand() < 0.45) continue;\n      const shade = STONE_SHADES[Math.floor(rand() * STONE_SHADES.length)]!;\n      rects += `<rect x=\"${x}\" y=\"${y}\" width=\"1\" height=\"1\" fill=\"${shade}\"/>`;\n    }\n  }\n  const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${TILE}\" height=\"${TILE}\" shape-rendering=\"crispEdges\"><rect width=\"${TILE}\" height=\"${TILE}\" fill=\"${STONE_BASE}\"/>${rects}</svg>`;\n  return `url(\"data:image/svg+xml,${encodeURIComponent(svg)}\")`;\n})();\n\nconst pixelFont: React.CSSProperties = {\n  // Bring your own pixel font through --font-pixel (the docs use Pixelify Sans).\n  fontFamily: \"var(--font-pixel, ui-monospace), monospace\",\n};\n\nconst stoneStyle: React.CSSProperties = {\n  backgroundColor: STONE_BASE,\n  backgroundImage: STONE_TEXTURE,\n  backgroundSize: `${TILE * PX}px ${TILE * PX}px`,\n  imageRendering: \"pixelated\",\n};\n\n/**\n * Pixels of a crack network across a cols×rows grid, ordered so that any\n * prefix is a plausible \"earlier\" stage: every branch grows one step per round.\n */\nfunction crackOrder(cols: number, rows: number, seed: number) {\n  if (cols < 2 || rows < 2) return [] as [number, number][];\n  const rand = mulberry32(seed);\n  type Walker = { x: number; y: number; a: number; left: number };\n  const walkers: Walker[] = [];\n  const origins = Math.max(1, Math.round(cols / 24));\n\n  for (let i = 0; i < origins; i++) {\n    const ox = ((i + 0.5) / origins) * cols + (rand() - 0.5) * 4;\n    const oy = rows / 2 + (rand() - 0.5) * 3;\n    const branches = 4 + Math.floor(rand() * 2);\n    for (let b = 0; b < branches; b++) {\n      walkers.push({\n        x: ox,\n        y: oy,\n        a: (b / branches) * Math.PI * 2 + rand() * 0.8,\n        left: 7 + Math.floor(rand() * 12),\n      });\n    }\n  }\n\n  const seen = new Set<number>();\n  const order: [number, number][] = [];\n  while (walkers.some((w) => w.left > 0)) {\n    for (const w of [...walkers]) {\n      if (w.left <= 0) continue;\n      w.left--;\n      w.a += (rand() - 0.5) * 1.1;\n      w.x += Math.cos(w.a);\n      w.y += Math.sin(w.a) * 0.75;\n      const x = Math.round(w.x);\n      const y = Math.round(w.y);\n      if (x < 0 || y < 0 || x >= cols || y >= rows) {\n        w.left = 0;\n        continue;\n      }\n      const key = y * cols + x;\n      if (!seen.has(key)) {\n        seen.add(key);\n        order.push([x, y]);\n      }\n      if (rand() < 0.12 && w.left > 3) {\n        walkers.push({\n          x: w.x,\n          y: w.y,\n          a: w.a + (rand() < 0.5 ? -0.9 : 0.9),\n          left: Math.floor(w.left / 2),\n        });\n      }\n    }\n  }\n  return order;\n}\n\n/* ---------- Sound: synthesized, no assets ---------- */\n\nlet audioCtx: AudioContext | null = null;\n\nfunction getAudio() {\n  if (typeof window === \"undefined\") return null;\n  if (!audioCtx) {\n    const Ctor =\n      window.AudioContext ??\n      (window as unknown as { webkitAudioContext?: typeof AudioContext })\n        .webkitAudioContext;\n    if (!Ctor) return null;\n    audioCtx = new Ctor();\n  }\n  if (audioCtx.state === \"suspended\") void audioCtx.resume();\n  return audioCtx;\n}\n\nfunction playNoise(freq: number, duration: number, volume: number, q = 1) {\n  const ctx = getAudio();\n  if (!ctx) return;\n  const length = Math.floor(ctx.sampleRate * duration);\n  const buffer = ctx.createBuffer(1, length, ctx.sampleRate);\n  const data = buffer.getChannelData(0);\n  for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;\n\n  const source = ctx.createBufferSource();\n  source.buffer = buffer;\n  const filter = ctx.createBiquadFilter();\n  filter.type = \"bandpass\";\n  filter.frequency.value = freq;\n  filter.Q.value = q;\n  const gain = ctx.createGain();\n  const t = ctx.currentTime;\n  gain.gain.setValueAtTime(volume, t);\n  gain.gain.exponentialRampToValueAtTime(0.001, t + duration);\n\n  source.connect(filter).connect(gain).connect(ctx.destination);\n  source.start(t);\n}\n\nfunction playPop() {\n  const ctx = getAudio();\n  if (!ctx) return;\n  const osc = ctx.createOscillator();\n  const gain = ctx.createGain();\n  const t = ctx.currentTime;\n  const base = 480 + Math.random() * 160;\n  osc.type = \"triangle\";\n  osc.frequency.setValueAtTime(base, t);\n  osc.frequency.exponentialRampToValueAtTime(base * 2, t + 0.07);\n  gain.gain.setValueAtTime(0.16, t);\n  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);\n  osc.connect(gain).connect(ctx.destination);\n  osc.start(t);\n  osc.stop(t + 0.13);\n}\n\nconst sounds = {\n  dig: () => playNoise(900 + Math.random() * 400, 0.08, 0.35, 1.4),\n  crack: () => playNoise(520, 0.16, 0.5, 0.9),\n  break: () => {\n    playNoise(320, 0.38, 0.7, 0.7);\n    playNoise(1300, 0.22, 0.3, 1.2);\n  },\n  pop: playPop,\n  place: () => playNoise(240, 0.12, 0.55, 0.8),\n};\n\n/* ---------- Particles: pixel chips flung with gravity ---------- */\n\nfunction spawnChips(\n  layer: HTMLElement,\n  origin: { x: number; y: number },\n  area: { w: number; h: number },\n  count: number,\n  force: number,\n) {\n  for (let i = 0; i < count; i++) {\n    const chip = document.createElement(\"span\");\n    const size = PX * (1 + Math.floor(Math.random() * 2));\n    const color =\n      Math.random() < 0.3\n        ? STONE_BASE\n        : STONE_SHADES[Math.floor(Math.random() * STONE_SHADES.length)]!;\n    const x0 = origin.x + (Math.random() - 0.5) * area.w;\n    const y0 = origin.y + (Math.random() - 0.5) * area.h;\n    Object.assign(chip.style, {\n      position: \"absolute\",\n      left: `${x0}px`,\n      top: `${y0}px`,\n      width: `${size}px`,\n      height: `${size}px`,\n      background: color,\n      boxShadow: `inset -1px -1px 0 rgb(0 0 0 / 0.25)`,\n    } satisfies Partial<CSSStyleDeclaration>);\n    layer.appendChild(chip);\n\n    const vx = (Math.random() - 0.5) * 2 * force;\n    const vy = -(0.4 + Math.random()) * force;\n    const gravity = 900;\n    const duration = 450 + Math.random() * 450;\n    const frames: Keyframe[] = [];\n    for (let s = 0; s <= 10; s++) {\n      const t = (s / 10) * (duration / 1000);\n      const dx = Math.round((vx * t) / PX) * PX;\n      const dy = Math.round((vy * t + 0.5 * gravity * t * t) / PX) * PX;\n      frames.push({\n        transform: `translate(${dx}px, ${dy}px)`,\n        opacity: s < 7 ? 1 : 1 - (s - 7) / 3,\n      });\n    }\n    chip\n      .animate(frames, { duration, easing: \"linear\", fill: \"forwards\" })\n      .finished.then(\n        () => chip.remove(),\n        () => chip.remove(),\n      );\n  }\n}\n\n/* ---------- Pickaxe cursor ---------- */\n\n/** 16×16 diamond pickaxe sprite (drawn at 4×, head facing left), inlined so the item stays one file. */\nconst PICKAXE_SPRITE = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACmUlEQVR42u1bsUorQRQ9ioUiWCh2GlexeGCTKpWgrQhiwB8QXqEoiM37Bhv9AtFP0M5WQZs0pnwPIaxB3hPCswgEGwubRPaOmtmbuTOzm8zpbnZ2ZvZkztk7d3eBgICBxpDl/iMARQALQv3VAFQBxFITHLFMQHFipXQh2WHzplKWJGDYMgFS/7y1Pm0TkHlwJRCBp+myhTkn+zT2BK4Jbu5sFIimH59fSYO7sXkSzx79IvFb46XrACPTkyQeXZwj8cPWHonbnnDZKwFcCdjQtCmM5jTwHtAPBNRMTmbnAdfV/yTeXpuhs1n/SWJV8/+OT42utnlTuQXQ8aGOCbojIAO4AHAi1Vk/SGDgCDDSvAq2BP7OL5H4TNG8eh8vHOyS+E+lkdRwLxdf9UqAAEQ1bIo8SkAUuhUQgeb+Erm9aC5vCt1eYFPdz+ty+6H9QxKvFqdIrO4drioNo1zeFDoJuMj9ve4vggf4ngA8ewKbAF1u36y3kvf5MoDl5HF179A+/tGm7QnZJSAF1Pv8MvN8p54QPMBy/73k7U49wTYB1d/1VhmJROr86olIQs0T1krTTj3BNgHxF5PPlCcED3A8XuY8gU2Aut9n4pMnwHOe4HoFxMiYJwQP8Dy+d09gE6A+qzOE9zzB9wqI4dkTggdojveiUZO6vXNP0NUEIyhF0YmVElmi3zyrSz0B3Xg/CuNd84Tre1qf4NYYdSsghl6jknX+NOPpwPKE4AHM9rWUv0nBuidw3xGKQDVqu2ChjvfJE0yfO3BXQGzxYtOOJ+oJwQN8T4AJcU/IGwHi9YS8ERBD2BMG3gPyToBxDpI3Cahg1xP6jYAYhp6QdwkYo98ISOMJpI3tj6ZcI0L3Dzq8v5QVEBAQEBCQIbwDjb7c841t6aMAAAAASUVORK5CYII=\";\n\n/** The pointer sits on the head's top-left corner (texel 3,2); swings pivot on the grip (texel 14,13). */\nconst PICKAXE_HOTSPOT = { x: 3.5 * PX, y: 2.5 * PX };\nconst PICKAXE_GRIP = { x: 14.5 * PX, y: 13.5 * PX };\n/** Head lifts back clockwise, then strikes down counter-clockwise. */\nconst WIND_UP = \"rotate(24deg)\";\nconst STRIKE = \"rotate(-28deg)\";\n\nconst pickaxeStyle: React.CSSProperties = {\n  width: 16 * PX,\n  height: 16 * PX,\n  backgroundImage: `url(\"${PICKAXE_SPRITE}\")`,\n  backgroundSize: \"100% 100%\",\n  imageRendering: \"pixelated\",\n  transformOrigin: `${PICKAXE_GRIP.x}px ${PICKAXE_GRIP.y}px`,\n};\n\n/* ---------- Component ---------- */\n\nexport interface MinecraftButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Label used when no children are provided. */\n  label?: React.ReactNode;\n  /** Taps needed to advance one crack stage. */\n  tapsPerStage?: number;\n  /** Stages until the button breaks. Breaks after `tapsPerStage * stages` taps. */\n  stages?: number;\n  /** Play synthesized dig, crack, break, and pickup sounds. */\n  sound?: boolean;\n  /** Swap the mouse cursor for a pickaxe that swings on each tap. */\n  pickaxe?: boolean;\n  /** Auto-respawn this many ms after breaking. Omit to wait for the drop to be picked up. */\n  respawnAfter?: number;\n  /** Fired when the crack deepens to a new stage (1-based). */\n  onCrack?: (stage: number) => void;\n  /** Fired when the button finally breaks. */\n  onBreak?: () => void;\n  /** Fired when the button comes back. */\n  onRespawn?: () => void;\n}\n\nexport const MinecraftButton = React.forwardRef<\n  HTMLButtonElement,\n  MinecraftButtonProps\n>(\n  (\n    {\n      children,\n      label = \"Singleplayer\",\n      tapsPerStage = 5,\n      stages = 4,\n      sound = true,\n      pickaxe = true,\n      respawnAfter,\n      onCrack,\n      onBreak,\n      onRespawn,\n      onClick,\n      className,\n      style,\n      disabled,\n      type = \"button\",\n      ...props\n    },\n    ref,\n  ) => {\n    const buttonRef = React.useRef<HTMLButtonElement | null>(null);\n    const layerRef = React.useRef<HTMLSpanElement | null>(null);\n    const itemRef = React.useRef<HTMLButtonElement | null>(null);\n    const itemBodyRef = React.useRef<HTMLSpanElement | null>(null);\n    const [taps, setTaps] = React.useState(0);\n    const [broken, setBroken] = React.useState(false);\n    const [grid, setGrid] = React.useState({ cols: 0, rows: 0 });\n    const [seed, setSeed] = React.useState(7);\n    const [aiming, setAiming] = React.useState(false);\n    const pointerRef = React.useRef({ x: 0, y: 0 });\n    const cursorRef = React.useRef<HTMLDivElement | null>(null);\n    const swingRef = React.useRef<HTMLDivElement | null>(null);\n    const swingAnim = React.useRef<Animation | undefined>(undefined);\n\n    const placeCursor = React.useCallback((node: HTMLDivElement | null) => {\n      cursorRef.current = node;\n      if (!node) return;\n      const { x, y } = pointerRef.current;\n      node.style.transform = `translate(${x - PICKAXE_HOTSPOT.x}px, ${y - PICKAXE_HOTSPOT.y}px)`;\n    }, []);\n\n    const swing = (keyframes: Keyframe[], duration: number) => {\n      const node = swingRef.current;\n      if (!node || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n      swingAnim.current?.cancel();\n      swingAnim.current = node.animate(keyframes, {\n        duration,\n        easing: \"cubic-bezier(0.3, 0, 0.2, 1)\",\n        fill: \"forwards\",\n      });\n    };\n\n    const trackPointer = (event: React.PointerEvent) => {\n      const overButton =\n        pickaxe &&\n        !broken &&\n        !disabled &&\n        event.pointerType !== \"touch\" &&\n        !!buttonRef.current?.contains(event.target as Node);\n      pointerRef.current = { x: event.clientX, y: event.clientY };\n      if (cursorRef.current) placeCursor(cursorRef.current);\n      if (overButton !== aiming) setAiming(overButton);\n    };\n\n    const perStage = Math.max(1, Math.floor(tapsPerStage));\n    const stageCount = Math.max(1, Math.floor(stages));\n    const breakAt = perStage * stageCount;\n    // The last stage is the break itself, so the visible crack levels stop one short.\n    const crackLevels = Math.max(1, stageCount - 1);\n    const level = Math.min(Math.floor(taps / perStage), crackLevels);\n\n    const setButtonRef = (node: HTMLButtonElement | null) => {\n      buttonRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    };\n\n    React.useEffect(() => {\n      const button = buttonRef.current;\n      if (!button) return;\n      const observer = new ResizeObserver(() => {\n        setGrid({\n          cols: Math.ceil(button.offsetWidth / PX),\n          rows: Math.ceil(button.offsetHeight / PX),\n        });\n      });\n      observer.observe(button);\n      return () => observer.disconnect();\n    }, []);\n\n    const order = React.useMemo(\n      () => crackOrder(grid.cols, grid.rows, seed),\n      [grid.cols, grid.rows, seed],\n    );\n    const visible = order.slice(\n      0,\n      Math.round((order.length * level) / crackLevels),\n    );\n\n    const reduceMotion = () =>\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    const respawn = React.useCallback(() => {\n      setBroken(false);\n      setTaps(0);\n      setSeed((s) => s + 1);\n      onRespawn?.();\n      requestAnimationFrame(() => {\n        const button = buttonRef.current;\n        if (!button) return;\n        button.focus({ preventScroll: true });\n        if (!window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n          button.animate(\n            [{ transform: \"scale(0.9)\" }, { transform: \"scale(1)\" }],\n            { duration: 180, easing: \"steps(3, end)\" },\n          );\n        }\n      });\n    }, [onRespawn]);\n\n    const pickUp = () => {\n      if (sound) sounds.pop();\n      const item = itemRef.current;\n      if (item && !reduceMotion()) {\n        item\n          .animate(\n            [\n              { transform: \"translate(-50%, -50%) scale(1)\", opacity: 1 },\n              { transform: \"translate(-50%, -110%) scale(0.4)\", opacity: 0 },\n            ],\n            { duration: 140, easing: \"ease-in\", fill: \"forwards\" },\n          )\n          .finished.then(respawn, respawn);\n      } else {\n        respawn();\n      }\n    };\n\n    // Dropped item: pops out, then bobs and spins like an item entity.\n    React.useEffect(() => {\n      if (!broken) return;\n      itemRef.current?.focus({ preventScroll: true });\n      const body = itemBodyRef.current;\n      let loop: Animation | undefined;\n      let drop: Animation | undefined;\n      if (body && !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n        drop = body.animate(\n          [\n            { transform: \"translateY(0) scale(0.5)\" },\n            { transform: \"translateY(-18px) scale(0.9)\", offset: 0.4 },\n            { transform: \"translateY(0) scale(1)\" },\n          ],\n          { duration: 360, easing: \"ease-out\" },\n        );\n        drop.finished.then(\n          () => {\n            loop = body.animate(\n              [\n                { transform: \"translateY(0) rotateY(0deg)\" },\n                { transform: \"translateY(-5px) rotateY(180deg)\" },\n                { transform: \"translateY(0) rotateY(360deg)\" },\n              ],\n              { duration: 2600, iterations: Infinity, easing: \"linear\" },\n            );\n          },\n          () => {},\n        );\n      }\n      const timer =\n        respawnAfter !== undefined\n          ? window.setTimeout(respawn, Math.max(0, respawnAfter))\n          : undefined;\n      return () => {\n        drop?.cancel();\n        loop?.cancel();\n        window.clearTimeout(timer);\n      };\n    }, [broken, respawnAfter, respawn]);\n\n    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n      onClick?.(event);\n      if (disabled || broken || event.defaultPrevented) return;\n\n      // Strike from wherever the pointer-down wind-up left the pickaxe.\n      if (aiming) {\n        swing(\n          [\n            { transform: WIND_UP },\n            { transform: STRIKE, offset: 0.35 },\n            { transform: \"rotate(0deg)\" },\n          ],\n          240,\n        );\n      }\n\n      const button = buttonRef.current;\n      const layer = layerRef.current;\n      const next = taps + 1;\n      const nextLevel = Math.min(Math.floor(next / perStage), crackLevels);\n      const breaking = next >= breakAt;\n      const deepened = !breaking && nextLevel > level;\n      const motion = !reduceMotion();\n\n      if (button && layer && motion) {\n        const rect = button.getBoundingClientRect();\n        // Keyboard clicks report 0,0; chip from the center instead.\n        const fromPointer = event.detail > 0;\n        const origin = {\n          x: fromPointer ? event.clientX - rect.left : rect.width / 2,\n          y: fromPointer ? event.clientY - rect.top : rect.height / 2,\n        };\n        if (breaking) {\n          spawnChips(\n            layer,\n            { x: rect.width / 2, y: rect.height / 2 },\n            { w: rect.width, h: rect.height },\n            Math.min(90, Math.round(rect.width / 3)),\n            260,\n          );\n        } else {\n          spawnChips(\n            layer,\n            origin,\n            { w: 6, h: 6 },\n            deepened ? 10 : 4,\n            deepened ? 200 : 140,\n          );\n          const kick = deepened ? 3 : 1.5;\n          button.animate(\n            [\n              { transform: \"translate(0, 0)\" },\n              { transform: `translate(${-kick}px, ${kick / 2}px)` },\n              { transform: `translate(${kick}px, 0)` },\n              { transform: \"translate(0, 0)\" },\n            ],\n            { duration: deepened ? 180 : 110, easing: \"steps(4, end)\" },\n          );\n        }\n      }\n\n      if (sound) {\n        if (breaking) sounds.break();\n        else if (deepened) sounds.crack();\n        else sounds.dig();\n      }\n\n      setTaps(next);\n      if (deepened) onCrack?.(nextLevel);\n      if (breaking) {\n        setBroken(true);\n        onBreak?.();\n      }\n    };\n\n    const content = children ?? label;\n\n    return (\n      <span\n        className=\"relative inline-flex align-middle\"\n        onPointerEnter={trackPointer}\n        onPointerMove={trackPointer}\n        onPointerLeave={() => setAiming(false)}\n        onPointerDown={(event) => {\n          trackPointer(event);\n          if (aiming) swing([{ transform: WIND_UP }], 90);\n        }}\n      >\n        <button\n          ref={setButtonRef}\n          type={type}\n          disabled={disabled || broken}\n          aria-hidden={broken || undefined}\n          tabIndex={broken ? -1 : undefined}\n          data-stage={level || undefined}\n          onClick={handleClick}\n          style={{\n            ...stoneStyle,\n            ...pixelFont,\n            ...(aiming ? { cursor: \"none\" } : null),\n            ...style,\n          }}\n          className={cn(\n            \"group/mc relative inline-flex h-12 min-w-56 cursor-pointer items-center justify-center rounded-none px-6 outline-none select-none\",\n            \"text-lg leading-none tracking-wide text-[#e0e0e0] [text-shadow:3px_3px_0_#3f3f3f]\",\n            // Black texel outline with a light top-left bevel and dark bottom-right bevel.\n            \"shadow-[0_0_0_3px_#000,inset_3px_3px_0_rgb(255_255_255/0.32),inset_-3px_-3px_0_rgb(0_0_0/0.38)]\",\n            // Hover lights the outline white and turns the label pale yellow, like the Java menus.\n            \"hover:text-[#ffffa0] hover:shadow-[0_0_0_3px_#fff,inset_3px_3px_0_rgb(255_255_255/0.32),inset_-3px_-3px_0_rgb(0_0_0/0.38)]\",\n            \"focus-visible:text-[#ffffa0] focus-visible:shadow-[0_0_0_3px_#fff,inset_3px_3px_0_rgb(255_255_255/0.32),inset_-3px_-3px_0_rgb(0_0_0/0.38)]\",\n            // Pressing flips the bevel and sinks the face one texel.\n            \"active:translate-y-[3px] active:shadow-[0_0_0_3px_#fff,inset_3px_3px_0_rgb(0_0_0/0.38),inset_-3px_-3px_0_rgb(255_255_255/0.2)] motion-reduce:active:translate-y-0\",\n            \"disabled:cursor-not-allowed disabled:text-[#a0a0a0] disabled:brightness-75\",\n            broken && \"invisible\",\n            className,\n          )}\n          {...props}\n        >\n          {visible.length > 0 ? (\n            <svg\n              aria-hidden\n              className=\"pointer-events-none absolute inset-0 size-full\"\n              viewBox={`0 0 ${grid.cols} ${grid.rows}`}\n              preserveAspectRatio=\"none\"\n              shapeRendering=\"crispEdges\"\n            >\n              {visible.map(([x, y]) => (\n                <React.Fragment key={`${x}-${y}`}>\n                  <rect x={x} y={y + 1} width={1} height={1} fill=\"rgb(255 255 255 / 0.14)\" />\n                  <rect x={x} y={y} width={1} height={1} fill=\"rgb(16 16 16 / 0.82)\" />\n                </React.Fragment>\n              ))}\n            </svg>\n          ) : null}\n          <span className=\"relative\">{content}</span>\n        </button>\n\n        <span\n          ref={layerRef}\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 z-10 overflow-visible\"\n        />\n\n        {/* aiming only turns on from a pointer event, so this never renders on the server. */}\n        {aiming\n          ? createPortal(\n              <div\n                ref={placeCursor}\n                aria-hidden\n                className=\"pointer-events-none fixed top-0 left-0 z-[90]\"\n              >\n                <div\n                  ref={swingRef}\n                  className=\"drop-shadow-[2px_2px_0_rgb(0_0_0/0.25)]\"\n                  style={pickaxeStyle}\n                />\n              </div>,\n              document.body,\n            )\n          : null}\n\n        {broken ? (\n          <button\n            ref={itemRef}\n            type=\"button\"\n            onClick={pickUp}\n            aria-label={typeof content === \"string\" ? `Pick up ${content}` : \"Pick up\"}\n            className=\"group/item absolute top-1/2 left-1/2 z-20 grid size-12 -translate-x-1/2 -translate-y-1/2 cursor-pointer place-items-center outline-none [perspective:240px]\"\n          >\n            <span\n              ref={itemBodyRef}\n              style={stoneStyle}\n              className=\"block size-6 shadow-[0_0_0_2px_#000,inset_2px_2px_0_rgb(255_255_255/0.32),inset_-2px_-2px_0_rgb(0_0_0/0.38)] [transform-style:preserve-3d] group-focus-visible/item:shadow-[0_0_0_2px_#fff,inset_2px_2px_0_rgb(255_255_255/0.32),inset_-2px_-2px_0_rgb(0_0_0/0.38)]\"\n            />\n            <span className=\"absolute bottom-1 h-1 w-5 bg-black/30\" />\n            {/* Item tooltip: near-black purple panel with the violet gradient frame. */}\n            <span\n              style={pixelFont}\n              className=\"pointer-events-none absolute bottom-full left-1/2 mb-1 -translate-x-1/2 border-2 border-[#100010]/95 bg-[#100010]/95 px-1.5 py-1 text-sm leading-none whitespace-nowrap text-white opacity-0 shadow-[inset_0_0_0_1px_#5000ff80] [text-shadow:2px_2px_0_#3f3f3f] group-hover/item:opacity-100 group-focus-visible/item:opacity-100\">\n              {content}\n            </span>\n          </button>\n        ) : null}\n      </span>\n    );\n  },\n);\n\nMinecraftButton.displayName = \"MinecraftButton\";\n\nexport default MinecraftButton;\n"
    }
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ]
}
