{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "minesweeper",
  "type": "registry:component",
  "title": "Minesweeper",
  "description": "Deterministic 9×9 Minesweeper. Left-click reveals, right-click / long-press flags.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/games/minesweeper.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                                   Engine                                    */\n/* -------------------------------------------------------------------------- */\n\nconst COLS = 9;\nconst ROWS = 9;\nconst MINE_COUNT = 10;\n\ntype CellState = {\n  mine: boolean;\n  revealed: boolean;\n  flagged: boolean;\n  adjacentMines: number;\n};\n\ntype Grid = CellState[][];\n\nfunction createEmptyGrid(): Grid {\n  const grid: Grid = [];\n  for (let r = 0; r < ROWS; r++) {\n    const row: CellState[] = [];\n    for (let c = 0; c < COLS; c++) {\n      row.push({ mine: false, revealed: false, flagged: false, adjacentMines: 0 });\n    }\n    grid.push(row);\n  }\n  return grid;\n}\n\nfunction getNeighbors(r: number, c: number): [number, number][] {\n  const neighbors: [number, number][] = [];\n  for (let dr = -1; dr <= 1; dr++) {\n    for (let dc = -1; dc <= 1; dc++) {\n      if (dr === 0 && dc === 0) continue;\n      const nr = r + dr;\n      const nc = c + dc;\n      if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS) {\n        neighbors.push([nr, nc]);\n      }\n    }\n  }\n  return neighbors;\n}\n\nfunction placeMines(grid: Grid, safeR: number, safeC: number): Grid {\n  // Safe zone: clicked cell + its neighbors\n  const safeSet = new Set<string>();\n  safeSet.add(`${safeR},${safeC}`);\n  for (const [nr, nc] of getNeighbors(safeR, safeC)) {\n    safeSet.add(`${nr},${nc}`);\n  }\n\n  const candidates: [number, number][] = [];\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      if (!safeSet.has(`${r},${c}`)) {\n        candidates.push([r, c]);\n      }\n    }\n  }\n\n  // Fisher-Yates shuffle, take first MINE_COUNT\n  for (let i = candidates.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    const tmp = candidates[i];\n    const swap = candidates[j];\n    if (tmp !== undefined && swap !== undefined) {\n      candidates[i] = swap;\n      candidates[j] = tmp;\n    }\n  }\n\n  const newGrid: Grid = grid.map((row) => row.map((cell) => ({ ...cell })));\n\n  for (let i = 0; i < MINE_COUNT && i < candidates.length; i++) {\n    const coord = candidates[i];\n    if (coord === undefined) continue;\n    const [mr, mc] = coord;\n    const mineRow = newGrid[mr];\n    if (mineRow === undefined) continue;\n    const mineCell = mineRow[mc];\n    if (mineCell === undefined) continue;\n    mineCell.mine = true;\n  }\n\n  // Calculate adjacency\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      const row = newGrid[r];\n      if (row === undefined) continue;\n      const cell = row[c];\n      if (cell === undefined) continue;\n      if (cell.mine) continue;\n      let count = 0;\n      for (const [nr, nc] of getNeighbors(r, c)) {\n        const nrow = newGrid[nr];\n        if (nrow === undefined) continue;\n        const ncell = nrow[nc];\n        if (ncell === undefined) continue;\n        if (ncell.mine) count++;\n      }\n      cell.adjacentMines = count;\n    }\n  }\n\n  return newGrid;\n}\n\nfunction floodReveal(grid: Grid, startR: number, startC: number): Grid {\n  const newGrid: Grid = grid.map((row) => row.map((cell) => ({ ...cell })));\n  const queue: [number, number][] = [[startR, startC]];\n  const visited = new Set<string>();\n\n  while (queue.length > 0) {\n    const item = queue.shift();\n    if (item === undefined) break;\n    const [r, c] = item;\n    const key = `${r},${c}`;\n    if (visited.has(key)) continue;\n    visited.add(key);\n\n    const row = newGrid[r];\n    if (row === undefined) continue;\n    const cell = row[c];\n    if (cell === undefined) continue;\n    if (cell.flagged || cell.mine) continue;\n\n    cell.revealed = true;\n\n    if (cell.adjacentMines === 0) {\n      for (const [nr, nc] of getNeighbors(r, c)) {\n        const nrow = newGrid[nr];\n        if (nrow === undefined) continue;\n        const ncell = nrow[nc];\n        if (ncell === undefined) continue;\n        if (!ncell.revealed && !ncell.flagged) {\n          queue.push([nr, nc]);\n        }\n      }\n    }\n  }\n\n  return newGrid;\n}\n\n/**\n * Chord (a.k.a. \"double-click\") reveal: activating an already-revealed numbered\n * cell whose flagged-neighbor count equals its number reveals all of its\n * non-flagged neighbors. Reuses floodReveal so empty cells cascade as usual and\n * mines stay flagged. Returns the new grid plus whether a mine got revealed\n * (the player flagged wrong → loss, handled by the caller exactly like a normal\n * mine reveal). When the chord conditions aren't met, the grid is returned\n * unchanged.\n */\nfunction chordReveal(grid: Grid, r: number, c: number): { grid: Grid; hitMine: boolean } {\n  const row = grid[r];\n  if (row === undefined) return { grid, hitMine: false };\n  const cell = row[c];\n  if (cell === undefined || !cell.revealed || cell.mine || cell.adjacentMines === 0) {\n    return { grid, hitMine: false };\n  }\n\n  const neighbors = getNeighbors(r, c);\n  let flaggedCount = 0;\n  for (const [nr, nc] of neighbors) {\n    const nrow = grid[nr];\n    if (nrow === undefined) continue;\n    const ncell = nrow[nc];\n    if (ncell === undefined) continue;\n    if (ncell.flagged) flaggedCount++;\n  }\n\n  // Only chord when the flag count matches the number (standard behavior).\n  if (flaggedCount !== cell.adjacentMines) return { grid, hitMine: false };\n\n  let workGrid = grid;\n  let hitMine = false;\n  for (const [nr, nc] of neighbors) {\n    const nrow = workGrid[nr];\n    if (nrow === undefined) continue;\n    const ncell = nrow[nc];\n    if (ncell === undefined) continue;\n    if (ncell.flagged || ncell.revealed) continue;\n    if (ncell.mine) hitMine = true;\n    // Reuse the normal flood logic so \"0\" cells cascade as usual.\n    workGrid = floodReveal(workGrid, nr, nc);\n  }\n\n  return { grid: workGrid, hitMine };\n}\n\nfunction revealAllMines(grid: Grid): Grid {\n  return grid.map((row) =>\n    row.map((cell) => (cell.mine ? { ...cell, revealed: true } : { ...cell })),\n  );\n}\n\nfunction checkWin(grid: Grid): boolean {\n  for (const row of grid) {\n    for (const cell of row) {\n      if (!cell.mine && !cell.revealed) return false;\n    }\n  }\n  return true;\n}\n\nfunction countFlags(grid: Grid): number {\n  let count = 0;\n  for (const row of grid) {\n    for (const cell of row) {\n      if (cell.flagged) count++;\n    }\n  }\n  return count;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Shared primitives                              */\n/* -------------------------------------------------------------------------- */\n\nfunction usePrefersReducedMotion(): boolean {\n  const [reduce, setReduce] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduce(mq.matches);\n    const onChange = (e: MediaQueryListEvent) => setReduce(e.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduce;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Component                                  */\n/* -------------------------------------------------------------------------- */\n\nexport interface MinesweeperProps {\n  className?: string;\n  width?: number;\n  autoFocus?: boolean;\n  /**\n   * When true (the default), the game listens for keys on `window` so it works\n   * without being focused first — ideal for single-game pages (404s, loading /\n   * empty states). Set false when multiple games share a page, or the game sits\n   * in scrollable content, so it only responds while focused. Ignores keys aimed\n   * at form fields.\n   */\n  captureGlobalKeys?: boolean;\n  persistHighScore?: boolean | string;\n  onScoreChange?: (score: number) => void;\n  onGameOver?: (r: { score: number; won: boolean }) => void;\n  onStart?: () => void;\n}\n\nconst NUMBER_COLORS: Record<number, string> = {\n  1: \"text-blue-600 dark:text-blue-400\",\n  2: \"text-green-600 dark:text-green-500\",\n  3: \"text-red-600 dark:text-red-400\",\n  4: \"text-purple-800 dark:text-purple-400\",\n  5: \"text-red-900 dark:text-red-300\",\n  6: \"text-teal-600 dark:text-teal-400\",\n  7: \"text-black dark:text-white\",\n  8: \"text-muted-foreground\",\n};\n\nfunction formatTime(seconds: number): string {\n  const m = Math.floor(seconds / 60);\n  const s = seconds % 60;\n  return m > 0 ? `${m}:${s.toString().padStart(2, \"0\")}` : `${s}s`;\n}\n\nexport function Minesweeper({\n  className,\n  width,\n  autoFocus = true,\n  captureGlobalKeys = true,\n  persistHighScore = true,\n  onScoreChange,\n  onGameOver,\n  onStart,\n}: MinesweeperProps) {\n  const wrapperRef = React.useRef<HTMLDivElement | null>(null);\n  const reduce = usePrefersReducedMotion();\n\n  const storageKey =\n    typeof persistHighScore === \"string\"\n      ? persistHighScore\n      : persistHighScore === false\n        ? null\n        : \"gamekitui:minesweeper:hi\";\n\n  type GameStatus = \"idle\" | \"playing\" | \"won\" | \"lost\";\n\n  const [status, setStatus] = React.useState<GameStatus>(\"idle\");\n  const [grid, setGrid] = React.useState<Grid>(createEmptyGrid);\n  const [elapsed, setElapsed] = React.useState(0);\n  const [bestTime, setBestTime] = React.useState<number | null>(null);\n\n  // Refs for timer\n  const startTimeRef = React.useRef<number | null>(null);\n  const timerIdRef = React.useRef<ReturnType<typeof setInterval> | null>(null);\n  const statusRef = React.useRef<GameStatus>(\"idle\");\n  statusRef.current = status;\n\n  // Long-press refs for touch flag toggling\n  const longPressTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const longPressTriggeredRef = React.useRef(false);\n\n  // Load best time from localStorage\n  React.useEffect(() => {\n    if (!storageKey) return;\n    try {\n      const raw = window.localStorage.getItem(storageKey);\n      if (raw) {\n        const parsed = Number(raw);\n        if (!isNaN(parsed) && parsed > 0) setBestTime(parsed);\n      }\n    } catch {\n      /* ignore */\n    }\n  }, [storageKey]);\n\n  const saveBestTime = React.useCallback(\n    (seconds: number) => {\n      setBestTime((prev) => {\n        const isNew = prev === null || seconds < prev;\n        if (!isNew) return prev;\n        if (storageKey) {\n          try {\n            window.localStorage.setItem(storageKey, String(seconds));\n          } catch {\n            /* ignore */\n          }\n        }\n        return seconds;\n      });\n    },\n    [storageKey],\n  );\n\n  // Timer management\n  const stopTimer = React.useCallback(() => {\n    if (timerIdRef.current !== null) {\n      clearInterval(timerIdRef.current);\n      timerIdRef.current = null;\n    }\n  }, []);\n\n  const startTimer = React.useCallback(() => {\n    stopTimer();\n    startTimeRef.current = Date.now();\n    setElapsed(0);\n    timerIdRef.current = setInterval(() => {\n      if (startTimeRef.current !== null) {\n        setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));\n      }\n    }, 500);\n  }, [stopTimer]);\n\n  // Pause timer on visibility change\n  React.useEffect(() => {\n    const onVis = () => {\n      if (document.hidden) {\n        stopTimer();\n      } else if (statusRef.current === \"playing\" && startTimeRef.current !== null) {\n        // Restart timer keeping elapsed time\n        const alreadyElapsed = elapsed;\n        timerIdRef.current = setInterval(() => {\n          setElapsed(alreadyElapsed + Math.floor((Date.now() - (startTimeRef.current ?? Date.now())) / 1000));\n        }, 500);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n    return () => document.removeEventListener(\"visibilitychange\", onVis);\n  }, [stopTimer, elapsed]);\n\n  // Cleanup timer on unmount\n  React.useEffect(() => {\n    return () => stopTimer();\n  }, [stopTimer]);\n\n  const startGame = React.useCallback(() => {\n    stopTimer();\n    setGrid(createEmptyGrid());\n    setElapsed(0);\n    startTimeRef.current = null;\n    setStatus(\"idle\");\n    wrapperRef.current?.focus();\n  }, [stopTimer]);\n\n  const handleCellReveal = React.useCallback(\n    (r: number, c: number) => {\n      if (statusRef.current === \"won\" || statusRef.current === \"lost\") return;\n\n      setGrid((prev) => {\n        const row = prev[r];\n        if (row === undefined) return prev;\n        const cell = row[c];\n        if (cell === undefined) return prev;\n\n        // Chord: activating an already-revealed number reveals its non-flagged\n        // neighbors when the flag count matches. Only meaningful while playing.\n        if (cell.revealed) {\n          if (cell.mine || cell.adjacentMines === 0) return prev;\n          if (statusRef.current !== \"playing\") return prev;\n\n          const { grid: chorded, hitMine } = chordReveal(prev, r, c);\n          if (chorded === prev) return prev; // conditions not met → no-op\n\n          if (hitMine) {\n            const revealed = revealAllMines(chorded);\n            stopTimer();\n            setStatus(\"lost\");\n            onGameOver?.({ score: elapsed, won: false });\n            onScoreChange?.(elapsed);\n            return revealed;\n          }\n\n          if (checkWin(chorded)) {\n            stopTimer();\n            setStatus(\"won\");\n            saveBestTime(elapsed);\n            onGameOver?.({ score: elapsed, won: true });\n            onScoreChange?.(elapsed);\n            return chorded;\n          }\n\n          return chorded;\n        }\n\n        if (cell.flagged) return prev;\n\n        let workGrid = prev;\n\n        // First reveal: place mines, start timer\n        if (statusRef.current === \"idle\") {\n          workGrid = placeMines(prev, r, c);\n          setStatus(\"playing\");\n          startTimer();\n          onStart?.();\n        }\n\n        if (statusRef.current === \"idle\" || statusRef.current === \"playing\") {\n          const workRow = workGrid[r];\n          if (workRow === undefined) return prev;\n          const workCell = workRow[c];\n          if (workCell === undefined) return prev;\n\n          if (workCell.mine) {\n            // Hit a mine\n            const revealed = revealAllMines(workGrid);\n            stopTimer();\n            setStatus(\"lost\");\n            onGameOver?.({ score: elapsed, won: false });\n            onScoreChange?.(elapsed);\n            return revealed;\n          }\n\n          const flooded = floodReveal(workGrid, r, c);\n          if (checkWin(flooded)) {\n            stopTimer();\n            setStatus(\"won\");\n            saveBestTime(elapsed);\n            onGameOver?.({ score: elapsed, won: true });\n            onScoreChange?.(elapsed);\n            return flooded;\n          }\n\n          return flooded;\n        }\n\n        return prev;\n      });\n    },\n    [elapsed, onGameOver, onScoreChange, onStart, saveBestTime, startTimer, stopTimer],\n  );\n\n  const handleCellFlag = React.useCallback((r: number, c: number) => {\n    if (statusRef.current !== \"playing\" && statusRef.current !== \"idle\") return;\n    setGrid((prev) => {\n      const row = prev[r];\n      if (row === undefined) return prev;\n      const cell = row[c];\n      if (cell === undefined) return prev;\n      if (cell.revealed) return prev;\n\n      return prev.map((rowArr, ri) =>\n        ri === r\n          ? rowArr.map((cellItem, ci) =>\n              ci === c ? { ...cellItem, flagged: !cellItem.flagged } : cellItem,\n            )\n          : rowArr,\n      );\n    });\n  }, []);\n\n  // Context menu prevention on the wrapper\n  const onContextMenu = React.useCallback((e: React.MouseEvent) => {\n    e.preventDefault();\n  }, []);\n\n  // Touch long-press handlers\n  const onTouchStart = React.useCallback(\n    (r: number, c: number) => (e: React.TouchEvent) => {\n      e.stopPropagation();\n      longPressTriggeredRef.current = false;\n      longPressTimerRef.current = setTimeout(() => {\n        longPressTriggeredRef.current = true;\n        handleCellFlag(r, c);\n      }, 450);\n    },\n    [handleCellFlag],\n  );\n\n  const onTouchEnd = React.useCallback(\n    (r: number, c: number) => (e: React.TouchEvent) => {\n      e.stopPropagation();\n      if (longPressTimerRef.current !== null) {\n        clearTimeout(longPressTimerRef.current);\n        longPressTimerRef.current = null;\n      }\n      if (!longPressTriggeredRef.current) {\n        handleCellReveal(r, c);\n      }\n    },\n    [handleCellReveal],\n  );\n\n  const onTouchCancel = React.useCallback(() => {\n    if (longPressTimerRef.current !== null) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n  }, []);\n\n  React.useEffect(() => {\n    if (autoFocus) wrapperRef.current?.focus();\n  }, [autoFocus]);\n\n  // Keyboard navigation\n  const [cursor, setCursor] = React.useState<[number, number]>([0, 0]);\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      // In global mode, ignore keys aimed at form fields / editable content.\n      const t = e.target as HTMLElement | null;\n      if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;\n      if (status === \"won\" || status === \"lost\") {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          startGame();\n        }\n        return;\n      }\n      switch (e.key) {\n        case \"ArrowUp\":\n          e.preventDefault();\n          setCursor(([r, c]) => [Math.max(0, r - 1), c]);\n          break;\n        case \"ArrowDown\":\n          e.preventDefault();\n          setCursor(([r, c]) => [Math.min(ROWS - 1, r + 1), c]);\n          break;\n        case \"ArrowLeft\":\n          e.preventDefault();\n          setCursor(([r, c]) => [r, Math.max(0, c - 1)]);\n          break;\n        case \"ArrowRight\":\n          e.preventDefault();\n          setCursor(([r, c]) => [r, Math.min(COLS - 1, c + 1)]);\n          break;\n        case \"Enter\":\n        case \" \":\n          e.preventDefault();\n          setCursor(([r, c]) => {\n            handleCellReveal(r, c);\n            return [r, c];\n          });\n          break;\n        case \"f\":\n        case \"F\":\n          e.preventDefault();\n          setCursor(([r, c]) => {\n            handleCellFlag(r, c);\n            return [r, c];\n          });\n          break;\n        default:\n          break;\n      }\n    };\n    // Single-game pages can opt into window-level capture so keys work without\n    // focusing the game; otherwise listen on the wrapper (focus-scoped).\n    const target: Window | HTMLElement | null = captureGlobalKeys\n      ? window\n      : wrapperRef.current;\n    target?.addEventListener(\"keydown\", onKey as EventListener);\n    return () => {\n      target?.removeEventListener(\"keydown\", onKey as EventListener);\n    };\n  }, [status, handleCellReveal, handleCellFlag, startGame, captureGlobalKeys]);\n\n  const flags = countFlags(grid);\n  const minesRemaining = MINE_COUNT - flags;\n\n  const statusText = React.useMemo(() => {\n    if (status === \"idle\") return \"Click a cell to start. Right-click or long-press to flag.\";\n    if (status === \"won\") return `You win! Time: ${formatTime(elapsed)}${bestTime !== null && elapsed <= bestTime ? \" — new best!\" : \"\"}`;\n    if (status === \"lost\") return \"Game over — mine detonated!\";\n    return `${minesRemaining >= 0 ? minesRemaining : 0} mines remaining`;\n  }, [status, elapsed, bestTime, minesRemaining]);\n\n  const isGameOver = status === \"won\" || status === \"lost\";\n\n  return (\n    <div\n      ref={wrapperRef}\n      tabIndex={0}\n      role=\"application\"\n      aria-label=\"Minesweeper game\"\n      style={width !== undefined ? { maxWidth: width } : undefined}\n      className={cn(\n        \"relative flex w-full max-w-md select-none flex-col gap-2 outline-none\",\n        // Focus ring only matters when keys are focus-scoped; global capture hides it.\n        !captureGlobalKeys &&\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n      onContextMenu={onContextMenu}\n    >\n      {/* Score badges */}\n      <div className=\"flex items-center justify-between gap-3 text-sm\">\n        <span\n          className=\"inline-flex items-center rounded-md border bg-secondary px-2 py-0.5 font-medium text-secondary-foreground tabular-nums\"\n          title=\"Mines remaining\"\n        >\n          {/* Mine marker: a small diamond shape via CSS */}\n          <span className=\"mr-1 inline-block h-2.5 w-2.5 rounded-sm bg-foreground opacity-70\" aria-hidden=\"true\" />\n          {Math.max(0, minesRemaining)}\n        </span>\n        <span\n          className=\"inline-flex items-center rounded-md border bg-secondary px-2 py-0.5 font-medium text-secondary-foreground tabular-nums\"\n          title=\"Elapsed time\"\n        >\n          {formatTime(elapsed)}\n        </span>\n        {bestTime !== null && (\n          <span className=\"text-muted-foreground tabular-nums\" title=\"Best time\">\n            Best {formatTime(bestTime)}\n          </span>\n        )}\n      </div>\n\n      {/* Grid */}\n      <div className=\"relative rounded-lg border bg-background overflow-hidden\">\n        <div\n          role=\"grid\"\n          aria-label=\"Minesweeper grid\"\n          className=\"@container grid grid-cols-9 grid-rows-9 w-full aspect-square gap-[1.5%]\"\n        >\n          {grid.map((row, r) =>\n            row.map((cell, c) => {\n              const isCursor = cursor[0] === r && cursor[1] === c;\n              const numColor = cell.adjacentMines > 0 ? (NUMBER_COLORS[cell.adjacentMines] ?? \"text-foreground\") : \"\";\n\n              return (\n                <button\n                  key={`${r}-${c}`}\n                  type=\"button\"\n                  role=\"gridcell\"\n                  aria-label={\n                    cell.revealed\n                      ? cell.mine\n                        ? \"Mine\"\n                        : cell.adjacentMines > 0\n                          ? `${cell.adjacentMines} adjacent mines`\n                          : \"Empty cell\"\n                      : cell.flagged\n                        ? \"Flagged cell\"\n                        : \"Hidden cell\"\n                  }\n                  aria-pressed={cell.flagged}\n                  // Revealed numbered cells stay enabled so they can be\n                  // chorded; revealed empties/mines and game-over disable.\n                  disabled={\n                    isGameOver ||\n                    (cell.revealed && (cell.mine || cell.adjacentMines === 0))\n                  }\n                  onClick={() => {\n                    if (cell.revealed) {\n                      // Chord on a revealed number.\n                      if (!cell.mine && cell.adjacentMines > 0) {\n                        handleCellReveal(r, c);\n                      }\n                    } else if (!cell.flagged) {\n                      handleCellReveal(r, c);\n                    }\n                  }}\n                  onContextMenu={(e) => {\n                    e.preventDefault();\n                    handleCellFlag(r, c);\n                  }}\n                  onTouchStart={onTouchStart(r, c)}\n                  onTouchEnd={onTouchEnd(r, c)}\n                  onTouchCancel={onTouchCancel}\n                  className={cn(\n                    \"relative flex size-full items-center justify-center border-r border-b border-border\",\n                    \"text-[clamp(0.5rem,6cqi,1rem)] font-bold\",\n                    \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring\",\n                    // Revealed mine (loss) → destructive\n                    cell.revealed && cell.mine && \"bg-destructive text-destructive-foreground\",\n                    // Revealed safe cell → background. Numbered cells stay\n                    // clickable (chord), empties are inert.\n                    cell.revealed && !cell.mine && \"bg-background\",\n                    cell.revealed && !cell.mine && cell.adjacentMines === 0 && \"cursor-default\",\n                    cell.revealed && !cell.mine && cell.adjacentMines > 0 && !isGameOver && \"cursor-pointer\",\n                    // Covered, flagged\n                    !cell.revealed && cell.flagged && \"bg-secondary text-secondary-foreground\",\n                    // Covered, not flagged, hoverable\n                    !cell.revealed && !cell.flagged && !isGameOver && \"bg-muted hover:bg-accent cursor-pointer\",\n                    // Covered, game over (lost), no flag → show as muted\n                    !cell.revealed && !cell.flagged && isGameOver && \"bg-muted cursor-default\",\n                    // Keyboard cursor highlight\n                    isCursor && !cell.revealed && \"ring-1 ring-inset ring-ring\",\n                    // Reveal animation — only when not reduced motion\n                    !reduce && cell.revealed && !cell.mine && \"transition-colors duration-100\",\n                  )}\n                >\n                  {cell.revealed && !cell.mine && cell.adjacentMines > 0 && (\n                    <span className={numColor} aria-hidden=\"true\">\n                      {cell.adjacentMines}\n                    </span>\n                  )}\n                  {cell.revealed && cell.mine && (\n                    // Mine: small filled circle styled with destructive contrast, scales with cell\n                    <span\n                      className=\"inline-block rounded-full bg-destructive-foreground\"\n                      style={{ width: \"40%\", height: \"40%\" }}\n                      aria-hidden=\"true\"\n                    />\n                  )}\n                  {!cell.revealed && cell.flagged && (\n                    // Flag: a vertical bar (pole) + triangle top via CSS borders, scales with cell\n                    <span\n                      className=\"inline-flex items-center justify-center\"\n                      aria-hidden=\"true\"\n                    >\n                      <span\n                        className=\"inline-block rounded-sm bg-primary\"\n                        style={{ width: \"30%\", height: \"50%\" }}\n                      />\n                    </span>\n                  )}\n                </button>\n              );\n            }),\n          )}\n        </div>\n\n        {/* Game-over overlay (won/lost). At idle the board is shown and the first\n            click starts the game — no blocking overlay, like classic minesweeper. */}\n        {isGameOver && (\n          <div className=\"absolute inset-0 grid place-items-center bg-background/80 backdrop-blur-[1px]\">\n            <div className=\"flex flex-col items-center gap-3 px-6 text-center\">\n              {status === \"won\" ? (\n                <>\n                  <p className=\"font-semibold text-lg\">You win!</p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    Cleared in {formatTime(elapsed)}\n                    {bestTime !== null && elapsed <= bestTime ? \" — new best!\" : \"\"}\n                  </p>\n                </>\n              ) : (\n                <>\n                  <p className=\"font-semibold text-lg\">Game over</p>\n                  <p className=\"text-muted-foreground text-sm\">A mine was detonated.</p>\n                </>\n              )}\n              <button\n                type=\"button\"\n                onClick={startGame}\n                className={cn(\n                  \"inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 font-medium text-primary-foreground text-sm\",\n                  \"transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                )}\n              >\n                Play again\n              </button>\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* aria-live region */}\n      <p\n        className=\"text-center text-sm text-muted-foreground min-h-[1.25rem]\"\n        aria-live=\"polite\"\n        aria-atomic=\"true\"\n      >\n        {statusText}\n      </p>\n    </div>\n  );\n}\n\nexport default Minesweeper;\n",
      "type": "registry:component",
      "target": "@/components/games/minesweeper.tsx"
    }
  ],
  "categories": [
    "games"
  ],
  "meta": {
    "iframeHeight": "540px",
    "surface": "dom"
  }
}
