{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tic-tac-toe",
  "type": "registry:component",
  "title": "Tic-Tac-Toe",
  "description": "Classic 3×3 Tic-Tac-Toe with an unbeatable CPU. Pure DOM, fully themeable.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/games/tic-tac-toe.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                                   Engine                                    */\n/* -------------------------------------------------------------------------- */\n\ntype Player = \"X\" | \"O\";\ntype Cell = Player | null;\ntype Board = [Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell, Cell];\n\nconst WIN_LINES: [number, number, number][] = [\n  [0, 1, 2],\n  [3, 4, 5],\n  [6, 7, 8],\n  [0, 3, 6],\n  [1, 4, 7],\n  [2, 5, 8],\n  [0, 4, 8],\n  [2, 4, 6],\n];\n\nfunction checkWinner(board: Board): { winner: Player; line: [number, number, number] } | null {\n  for (const line of WIN_LINES) {\n    const [a, b, c] = line;\n    const va = board[a!];\n    const vb = board[b!];\n    const vc = board[c!];\n    if (va && va === vb && va === vc) {\n      return { winner: va, line };\n    }\n  }\n  return null;\n}\n\nfunction isDraw(board: Board): boolean {\n  return board.every((c) => c !== null) && checkWinner(board) === null;\n}\n\n/** Minimax — returns best score from the perspective of `player`. */\nfunction minimax(board: Board, player: Player, depth: number): number {\n  const result = checkWinner(board);\n  if (result) return result.winner === \"O\" ? 10 - depth : depth - 10;\n  if (isDraw(board)) return 0;\n\n  const next: Player = player === \"X\" ? \"O\" : \"X\";\n  let best = player === \"O\" ? -Infinity : Infinity;\n\n  for (let i = 0; i < 9; i++) {\n    if (board[i] !== null) continue;\n    const copy = board.slice() as Board;\n    copy[i] = player;\n    const score = minimax(copy, next, depth + 1);\n    if (player === \"O\") {\n      if (score > best) best = score;\n    } else {\n      if (score < best) best = score;\n    }\n  }\n  return best;\n}\n\n/**\n * Chance the CPU plays a random legal move instead of the optimal one.\n * Keeps the game beatable for a human (\"medium\" difficulty) while still\n * usually blocking and taking wins.\n */\nconst CPU_RANDOM_MOVE_CHANCE = 0.35;\n\nfunction legalMoves(board: Board): number[] {\n  const moves: number[] = [];\n  for (let i = 0; i < 9; i++) {\n    if (board[i] === null) moves.push(i);\n  }\n  return moves;\n}\n\nfunction bestMove(board: Board): number {\n  const legal = legalMoves(board);\n  if (legal.length === 0) return -1;\n\n  // Imperfection: sometimes the CPU just plays a random legal move so a human\n  // can win. Uses Math.random — the AI does not need to be reproducible.\n  if (Math.random() < CPU_RANDOM_MOVE_CHANCE) {\n    return legal[Math.floor(Math.random() * legal.length)]!;\n  }\n\n  // Otherwise play optimally, but pick RANDOMLY among all equally-best moves so\n  // the opening (and any tie) varies instead of always taking the first cell.\n  let best = -Infinity;\n  let bestMoves: number[] = [];\n  for (const i of legal) {\n    const copy = board.slice() as Board;\n    copy[i] = \"O\";\n    const score = minimax(copy, \"X\", 0);\n    if (score > best) {\n      best = score;\n      bestMoves = [i];\n    } else if (score === best) {\n      bestMoves.push(i);\n    }\n  }\n  return bestMoves[Math.floor(Math.random() * bestMoves.length)]!;\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 TicTacToeProps {\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\nfunction emptyBoard(): Board {\n  return [null, null, null, null, null, null, null, null, null];\n}\n\nexport function TicTacToe({\n  className,\n  width,\n  autoFocus = true,\n  captureGlobalKeys = true,\n  onGameOver,\n  onStart,\n}: TicTacToeProps) {\n  const wrapperRef = React.useRef<HTMLDivElement | null>(null);\n  const reduce = usePrefersReducedMotion();\n\n  const [board, setBoard] = React.useState<Board>(emptyBoard);\n  const [started, setStarted] = React.useState(false);\n  const [turn, setTurn] = React.useState<Player>(\"X\");\n  const [winResult, setWinResult] = React.useState<{\n    winner: Player;\n    line: [number, number, number];\n  } | null>(null);\n  const [draw, setDraw] = React.useState(false);\n  const [cursor, setCursor] = React.useState(0);\n\n  // Session tally\n  const [wins, setWins] = React.useState(0);\n  const [losses, setLosses] = React.useState(0);\n  const [draws, setDraws] = React.useState(0);\n\n  const gameOver = winResult !== null || draw;\n\n  const resetBoard = React.useCallback(() => {\n    setBoard(emptyBoard());\n    setTurn(\"X\");\n    setWinResult(null);\n    setDraw(false);\n    setCursor(0);\n    setStarted(true);\n  }, []);\n\n  const handleStart = React.useCallback(() => {\n    resetBoard();\n    wrapperRef.current?.focus();\n    onStart?.();\n  }, [resetBoard, onStart]);\n\n  // After human (X) places, run CPU (O).\n  const cpuMoveRef = React.useRef<Board | null>(null);\n\n  const placeAt = React.useCallback(\n    (index: number, currentBoard: Board, currentTurn: Player): void => {\n      if (currentBoard[index] !== null) return;\n      if (gameOver) return;\n\n      const next = currentBoard.slice() as Board;\n      next[index] = currentTurn;\n\n      const result = checkWinner(next);\n      if (result) {\n        setBoard(next);\n        setWinResult(result);\n        if (result.winner === \"X\") {\n          setWins((w) => w + 1);\n          onGameOver?.({ score: 0, won: true });\n        } else {\n          setLosses((l) => l + 1);\n          onGameOver?.({ score: 0, won: false });\n        }\n        return;\n      }\n      if (isDraw(next)) {\n        setBoard(next);\n        setDraw(true);\n        setDraws((d) => d + 1);\n        onGameOver?.({ score: 0, won: false });\n        return;\n      }\n\n      setBoard(next);\n      setTurn(currentTurn === \"X\" ? \"O\" : \"X\");\n      if (currentTurn === \"X\") {\n        cpuMoveRef.current = next;\n      }\n    },\n    [gameOver, onGameOver],\n  );\n\n  // CPU plays O — triggered when turn flips to O.\n  React.useEffect(() => {\n    if (turn !== \"O\" || gameOver || !started) return;\n    const pending = cpuMoveRef.current;\n    if (!pending) return;\n    cpuMoveRef.current = null;\n\n    const delay = reduce ? 0 : 300;\n    const id = setTimeout(() => {\n      const move = bestMove(pending);\n      if (move === -1) return;\n      placeAt(move, pending, \"O\");\n    }, delay);\n    return () => clearTimeout(id);\n  }, [turn, gameOver, started, reduce, placeAt]);\n\n  // Keyboard navigation.\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 (!started || gameOver) {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          handleStart();\n        }\n        return;\n      }\n      if (turn !== \"X\") return; // block input during CPU turn\n\n      switch (e.key) {\n        case \"ArrowLeft\":\n          e.preventDefault();\n          setCursor((c) => (c % 3 === 0 ? c + 2 : c - 1));\n          break;\n        case \"ArrowRight\":\n          e.preventDefault();\n          setCursor((c) => (c % 3 === 2 ? c - 2 : c + 1));\n          break;\n        case \"ArrowUp\":\n          e.preventDefault();\n          setCursor((c) => (c < 3 ? c + 6 : c - 3));\n          break;\n        case \"ArrowDown\":\n          e.preventDefault();\n          setCursor((c) => (c >= 6 ? c - 6 : c + 3));\n          break;\n        case \"Enter\":\n        case \" \":\n          e.preventDefault();\n          placeAt(cursor, board, turn);\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  }, [started, gameOver, turn, cursor, board, placeAt, handleStart, captureGlobalKeys]);\n\n  React.useEffect(() => {\n    if (autoFocus) wrapperRef.current?.focus();\n  }, [autoFocus]);\n\n  const winLine = winResult?.line ?? null;\n\n  const statusText = React.useMemo(() => {\n    if (!started) return \"Press Start to play\";\n    if (winResult) return winResult.winner === \"X\" ? \"You win!\" : \"CPU wins!\";\n    if (draw) return \"It's a draw!\";\n    if (turn === \"X\") return \"Your turn (X)\";\n    return \"CPU is thinking…\";\n  }, [started, winResult, draw, turn]);\n\n  return (\n    <div\n      ref={wrapperRef}\n      tabIndex={0}\n      role=\"application\"\n      aria-label=\"Tic-Tac-Toe game\"\n      style={width ? { maxWidth: width } : undefined}\n      className={cn(\n        \"relative flex w-full max-w-sm select-none flex-col gap-3 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    >\n      {/* Session tally */}\n      <div className=\"flex items-center justify-between gap-2 text-sm\">\n        <span className=\"inline-flex items-center rounded-md border bg-primary px-2 py-0.5 font-medium text-primary-foreground tabular-nums\">\n          W {wins}\n        </span>\n        <span className=\"inline-flex items-center rounded-md border bg-secondary px-2 py-0.5 font-medium text-secondary-foreground tabular-nums\">\n          L {losses}\n        </span>\n        <span className=\"inline-flex items-center rounded-md border bg-muted px-2 py-0.5 font-medium text-muted-foreground tabular-nums\">\n          D {draws}\n        </span>\n      </div>\n\n      {/* Board */}\n      <div className=\"relative rounded-lg border bg-muted overflow-hidden\">\n        <div\n          className=\"@container grid grid-cols-3 grid-rows-3 w-full aspect-square gap-[2%]\"\n          role=\"grid\"\n          aria-label=\"Tic-Tac-Toe board\"\n        >\n          {board.map((cell, i) => {\n            const isWinCell = winLine !== null && winLine.includes(i);\n            const isCursor = cursor === i && started && !gameOver && turn === \"X\";\n            const isLastRow = i >= 6;\n            const isLastCol = i % 3 === 2;\n\n            return (\n              <button\n                key={i}\n                type=\"button\"\n                role=\"gridcell\"\n                aria-label={`Cell ${i + 1}${cell ? `, ${cell}` : \", empty\"}`}\n                aria-pressed={cell !== null}\n                disabled={cell !== null || gameOver || turn !== \"X\" || !started}\n                onClick={() => {\n                  if (!started) {\n                    handleStart();\n                    return;\n                  }\n                  setCursor(i);\n                  placeAt(i, board, turn);\n                }}\n                className={cn(\n                  \"relative flex size-full items-center justify-center\",\n                  \"text-[clamp(1.5rem,22cqi,4rem)] font-bold leading-none\",\n                  \"border-border bg-background\",\n                  !isLastRow && \"border-b\",\n                  !isLastCol && \"border-r\",\n                  // hover accent when empty and playable\n                  cell === null && !gameOver && turn === \"X\" && started && \"hover:bg-accent cursor-pointer\",\n                  // cursor highlight\n                  isCursor && \"bg-accent\",\n                  // win cell highlight\n                  isWinCell && \"bg-primary/10\",\n                  // transition unless reduced motion\n                  !reduce && \"transition-colors duration-150\",\n                  // disabled cursor\n                  (cell !== null || gameOver || turn !== \"X\" || !started) && \"cursor-default\",\n                )}\n              >\n                {cell === \"X\" && (\n                  <span\n                    className={cn(\n                      \"leading-none\",\n                      isWinCell ? \"text-primary\" : \"text-primary\",\n                    )}\n                    aria-hidden=\"true\"\n                  >\n                    X\n                  </span>\n                )}\n                {cell === \"O\" && (\n                  <span\n                    className={cn(\n                      \"leading-none\",\n                      isWinCell ? \"text-secondary-foreground\" : \"text-secondary-foreground\",\n                    )}\n                    aria-hidden=\"true\"\n                  >\n                    O\n                  </span>\n                )}\n              </button>\n            );\n          })}\n        </div>\n\n        {/* Game-over / idle overlay */}\n        {(!started || gameOver) && (\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              {!started ? (\n                <>\n                  <p className=\"font-semibold text-lg\">Tic-Tac-Toe</p>\n                  <p className=\"text-muted-foreground text-sm\">You play X. Beat the CPU.</p>\n                </>\n              ) : winResult ? (\n                <>\n                  <p className=\"font-semibold text-lg\">\n                    {winResult.winner === \"X\" ? \"You win!\" : \"CPU wins!\"}\n                  </p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    {winResult.winner === \"X\" ? \"Impressive!\" : \"Better luck next time.\"}\n                  </p>\n                </>\n              ) : (\n                <>\n                  <p className=\"font-semibold text-lg\">Draw!</p>\n                  <p className=\"text-muted-foreground text-sm\">Neither side won.</p>\n                </>\n              )}\n              <button\n                type=\"button\"\n                onClick={handleStart}\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                {!started ? \"Start\" : \"Play again\"}\n              </button>\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Status / aria-live region */}\n      <p\n        className=\"text-center text-sm text-muted-foreground\"\n        aria-live=\"polite\"\n        aria-atomic=\"true\"\n      >\n        {statusText}\n      </p>\n    </div>\n  );\n}\n\nexport default TicTacToe;\n",
      "type": "registry:component",
      "target": "@/components/games/tic-tac-toe.tsx"
    }
  ],
  "categories": [
    "games"
  ],
  "meta": {
    "iframeHeight": "460px",
    "surface": "dom"
  }
}
