{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pong",
  "type": "registry:component",
  "title": "Pong",
  "description": "The original. Player vs CPU. First to 7. W/S or arrow keys.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/games/pong.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* -------------------------------------------------------------------------- */\n/*                              Shared primitives                              */\n/* -------------------------------------------------------------------------- */\n\nconst THEME_TOKENS = [\n  \"background\",\n  \"foreground\",\n  \"primary\",\n  \"primary-foreground\",\n  \"secondary\",\n  \"accent\",\n  \"muted\",\n  \"muted-foreground\",\n  \"border\",\n  \"ring\",\n  \"destructive\",\n] as const;\ntype ThemeTokens = Record<(typeof THEME_TOKENS)[number], string>;\n\nfunction readTheme(el: HTMLElement | null): ThemeTokens {\n  const target = el ?? (typeof document !== \"undefined\" ? document.documentElement : null);\n  const cs = target ? getComputedStyle(target) : null;\n  const out = {} as ThemeTokens;\n  for (const t of THEME_TOKENS) out[t] = cs ? cs.getPropertyValue(`--${t}`).trim() : \"\";\n  return out;\n}\n\nfunction useGameTheme(ref: React.RefObject<HTMLElement | null>): ThemeTokens {\n  const [theme, setTheme] = React.useState<ThemeTokens>(() => readTheme(null));\n  React.useEffect(() => {\n    const read = () => setTheme(readTheme(ref.current));\n    read();\n    const mo = new MutationObserver(read);\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    });\n    return () => mo.disconnect();\n  }, [ref]);\n  return theme;\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/*                              Game constants                                 */\n/* -------------------------------------------------------------------------- */\n\n// Logical drawing space — all physics/layout/drawing happen in this coordinate\n// system; the canvas is scaled to fit its container while preserving aspect.\nconst LOGW = 360;\nconst LOGH = 240;\n\nconst PADDLE_W = 8;\nconst PADDLE_H = 50;\nconst PADDLE_RADIUS = 4;\nconst BALL_SIZE = 10;\nconst BALL_RADIUS = 5;\nconst PADDLE_MARGIN = 14;\nconst MAX_SCORE = 7;\n\n// Ball speed in px/s at normal motion\nconst BALL_SPEED_NORMAL = 220;\nconst BALL_SPEED_REDUCED = 80;\nconst CPU_SPEED_NORMAL = 170; // px/s\nconst CPU_SPEED_REDUCED = 60;\n\n/* -------------------------------------------------------------------------- */\n/*                              Game state (refs)                              */\n/* -------------------------------------------------------------------------- */\n\ninterface BallState {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n}\n\ninterface GameState {\n  playerY: number;\n  cpuY: number;\n  ball: BallState;\n  playerScore: number;\n  cpuScore: number;\n  pointFlash: number; // timestamp of last point, for flash\n  pointWinner: \"player\" | \"cpu\" | null;\n}\n\nfunction initBall(towardPlayer: boolean, speed: number): BallState {\n  const angle = (Math.random() * 0.6 - 0.3); // -0.3 to 0.3 radians\n  const vx = towardPlayer ? -speed * Math.cos(angle) : speed * Math.cos(angle);\n  const vy = speed * Math.sin(angle);\n  return { x: LOGW / 2, y: LOGH / 2, vx, vy };\n}\n\nfunction initGameState(speed: number): GameState {\n  return {\n    playerY: LOGH / 2 - PADDLE_H / 2,\n    cpuY: LOGH / 2 - PADDLE_H / 2,\n    ball: initBall(Math.random() > 0.5, speed),\n    playerScore: 0,\n    cpuScore: 0,\n    pointFlash: 0,\n    pointWinner: null,\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Component                                  */\n/* -------------------------------------------------------------------------- */\n\nexport interface PongProps {\n  className?: string;\n  width?: number;\n  height?: number;\n  paused?: boolean;\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\nexport function Pong({\n  className,\n  width,\n  paused = false,\n  autoFocus = true,\n  captureGlobalKeys = true,\n  persistHighScore: _persistHighScore,\n  onScoreChange,\n  onGameOver,\n  onStart,\n}: PongProps) {\n  const wrapperRef = React.useRef<HTMLDivElement | null>(null);\n  const playfieldRef = React.useRef<HTMLDivElement | null>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n  const theme = useGameTheme(wrapperRef as React.RefObject<HTMLElement | null>);\n  const reduce = usePrefersReducedMotion();\n\n  const [status, setStatus] = React.useState<\"idle\" | \"playing\" | \"gameover\">(\"idle\");\n  const [playerScore, setPlayerScore] = React.useState(0);\n  const [cpuScore, setCpuScore] = React.useState(0);\n  const [winner, setWinner] = React.useState<\"player\" | \"cpu\" | null>(null);\n  const [offscreen, setOffscreen] = React.useState(false);\n\n  // Mutable refs the rAF loop reads without re-subscribing\n  const statusRef = React.useRef(status);\n  const pausedRef = React.useRef(paused);\n  const offscreenRef = React.useRef(offscreen);\n  const themeRef = React.useRef(theme);\n  const reduceRef = React.useRef(reduce);\n  const gameStateRef = React.useRef<GameState | null>(null);\n  // Track pointer drag y for player paddle\n  const pointerYRef = React.useRef<number | null>(null);\n  // Track pressed keys\n  const keysRef = React.useRef<Set<string>>(new Set());\n\n  statusRef.current = status;\n  pausedRef.current = paused;\n  offscreenRef.current = offscreen;\n  themeRef.current = theme;\n  reduceRef.current = reduce;\n\n  // Announce score changes\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  // Pause when scrolled offscreen or tab hidden\n  React.useEffect(() => {\n    const el = wrapperRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([e]) => {\n      if (e) setOffscreen(!e.isIntersecting);\n    });\n    io.observe(el);\n    const onVis = () => setOffscreen(document.hidden);\n    document.addEventListener(\"visibilitychange\", onVis);\n    return () => {\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, []);\n\n  const start = React.useCallback(() => {\n    const speed = reduceRef.current ? BALL_SPEED_REDUCED : BALL_SPEED_NORMAL;\n    gameStateRef.current = initGameState(speed);\n    setPlayerScore(0);\n    setCpuScore(0);\n    setWinner(null);\n    setStatus(\"playing\");\n    setAnnouncement(\"Game started\");\n    onStart?.();\n    onScoreChange?.(0);\n    // Focus the wrapper so keyboard controls work after the Start button unmounts.\n    wrapperRef.current?.focus();\n  }, [onScoreChange, onStart]);\n\n  // Responsive canvas sizing: match the playfield's rendered CSS size × DPR,\n  // then scale the transform so all drawing happens in LOGW×LOGH logical space.\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const field = playfieldRef.current;\n    if (!canvas || !field) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const resize = () => {\n      const rect = field.getBoundingClientRect();\n      const w = Math.max(1, Math.round(rect.width));\n      const h = Math.max(1, Math.round(rect.height));\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = w * dpr;\n      canvas.height = h * dpr;\n      const scale = w / LOGW;\n      ctx.setTransform(scale * dpr, 0, 0, scale * dpr, 0, 0);\n    };\n    resize();\n    const ro = new ResizeObserver(resize);\n    ro.observe(field);\n    return () => ro.disconnect();\n  }, []);\n\n  // Drawing + main loop\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const drawRoundRect = (\n      x: number,\n      y: number,\n      w: number,\n      h: number,\n      r: number,\n      fill: string,\n    ) => {\n      ctx.fillStyle = fill;\n      ctx.beginPath();\n      ctx.roundRect(x, y, w, h, r);\n      ctx.fill();\n    };\n\n    const draw = (gs: GameState | null) => {\n      const t = themeRef.current;\n\n      // Clear (transform already maps logical space onto the device canvas)\n      ctx.clearRect(0, 0, LOGW, LOGH);\n\n      // Background\n      ctx.fillStyle = t.background || \"#000\";\n      ctx.fillRect(0, 0, LOGW, LOGH);\n\n      // Border\n      ctx.strokeStyle = t.border || \"rgba(255,255,255,0.2)\";\n      ctx.lineWidth = 1;\n      ctx.strokeRect(0.5, 0.5, LOGW - 1, LOGH - 1);\n\n      // Dashed center line\n      const now = performance.now();\n      const flashActive = gs && (now - gs.pointFlash) < 400;\n      const midLineColor = flashActive\n        ? t.primary || \"#ff0\"\n        : t[\"muted-foreground\"] || \"rgba(255,255,255,0.15)\";\n      ctx.strokeStyle = midLineColor;\n      ctx.lineWidth = 1;\n      ctx.setLineDash([6, 6]);\n      ctx.beginPath();\n      ctx.moveTo(LOGW / 2, 4);\n      ctx.lineTo(LOGW / 2, LOGH - 4);\n      ctx.stroke();\n      ctx.setLineDash([]);\n\n      if (!gs) return;\n\n      // Player paddle (left) — primary\n      drawRoundRect(\n        PADDLE_MARGIN,\n        gs.playerY,\n        PADDLE_W,\n        PADDLE_H,\n        PADDLE_RADIUS,\n        t.primary || \"#fff\",\n      );\n\n      // CPU paddle (right) — muted-foreground (visible on both light/dark, distinct from the primary player)\n      drawRoundRect(\n        LOGW - PADDLE_MARGIN - PADDLE_W,\n        gs.cpuY,\n        PADDLE_W,\n        PADDLE_H,\n        PADDLE_RADIUS,\n        t[\"muted-foreground\"] || \"#888\",\n      );\n\n      // Ball — primary (rounded square)\n      drawRoundRect(\n        gs.ball.x - BALL_SIZE / 2,\n        gs.ball.y - BALL_SIZE / 2,\n        BALL_SIZE,\n        BALL_SIZE,\n        BALL_RADIUS,\n        t.primary || \"#fff\",\n      );\n    };\n\n    let raf = 0;\n    let last = 0;\n\n    const frame = (now: number) => {\n      raf = requestAnimationFrame(frame);\n      if (last === 0) last = now;\n      const dt = Math.min((now - last) / 1000, 0.05); // seconds, capped\n      last = now;\n\n      const active = statusRef.current === \"playing\" && !pausedRef.current && !offscreenRef.current;\n      const gs = gameStateRef.current;\n\n      if (active && gs) {\n        const speed = reduceRef.current ? BALL_SPEED_REDUCED : BALL_SPEED_NORMAL;\n        const cpuSpeed = reduceRef.current ? CPU_SPEED_REDUCED : CPU_SPEED_NORMAL;\n\n        // Player paddle movement via keys\n        const keys = keysRef.current;\n        const paddleSpeed = cpuSpeed * 1.3; // player paddle speed\n        if (keys.has(\"ArrowUp\") || keys.has(\"w\") || keys.has(\"W\")) {\n          gs.playerY = Math.max(0, gs.playerY - paddleSpeed * dt);\n        }\n        if (keys.has(\"ArrowDown\") || keys.has(\"s\") || keys.has(\"S\")) {\n          gs.playerY = Math.min(LOGH - PADDLE_H, gs.playerY + paddleSpeed * dt);\n        }\n\n        // Player paddle movement via pointer\n        const ptrY = pointerYRef.current;\n        if (ptrY !== null) {\n          const targetY = ptrY - PADDLE_H / 2;\n          const clamped = Math.max(0, Math.min(LOGH - PADDLE_H, targetY));\n          // Snap to pointer position (within capped speed)\n          const diff = clamped - gs.playerY;\n          const maxMove = paddleSpeed * dt;\n          if (Math.abs(diff) <= maxMove) {\n            gs.playerY = clamped;\n          } else {\n            gs.playerY += Math.sign(diff) * maxMove;\n          }\n        }\n\n        // CPU paddle AI — track ball with capped speed\n        const cpuCenter = gs.cpuY + PADDLE_H / 2;\n        const diff = gs.ball.y - cpuCenter;\n        const maxMove = cpuSpeed * dt;\n        if (Math.abs(diff) > 2) {\n          gs.cpuY += Math.sign(diff) * Math.min(Math.abs(diff), maxMove);\n          gs.cpuY = Math.max(0, Math.min(LOGH - PADDLE_H, gs.cpuY));\n        }\n\n        // Move ball\n        gs.ball.x += gs.ball.vx * dt;\n        gs.ball.y += gs.ball.vy * dt;\n\n        // Wall bounce (top/bottom)\n        if (gs.ball.y - BALL_SIZE / 2 < 0) {\n          gs.ball.y = BALL_SIZE / 2;\n          gs.ball.vy = Math.abs(gs.ball.vy);\n        }\n        if (gs.ball.y + BALL_SIZE / 2 > LOGH) {\n          gs.ball.y = LOGH - BALL_SIZE / 2;\n          gs.ball.vy = -Math.abs(gs.ball.vy);\n        }\n\n        // Player paddle collision (left)\n        const playerLeft = PADDLE_MARGIN;\n        const playerRight = PADDLE_MARGIN + PADDLE_W;\n        if (\n          gs.ball.vx < 0 &&\n          gs.ball.x - BALL_SIZE / 2 <= playerRight &&\n          gs.ball.x + BALL_SIZE / 2 >= playerLeft &&\n          gs.ball.y + BALL_SIZE / 2 >= gs.playerY &&\n          gs.ball.y - BALL_SIZE / 2 <= gs.playerY + PADDLE_H\n        ) {\n          gs.ball.x = playerRight + BALL_SIZE / 2;\n          // Angle based on hit position (-1 to 1 relative to paddle center)\n          const hitPos = (gs.ball.y - (gs.playerY + PADDLE_H / 2)) / (PADDLE_H / 2);\n          const angle = hitPos * (Math.PI / 4); // max 45 degrees\n          const currentSpeed = Math.hypot(gs.ball.vx, gs.ball.vy);\n          const newSpeed = Math.max(currentSpeed, speed);\n          gs.ball.vx = newSpeed * Math.cos(angle);\n          gs.ball.vy = newSpeed * Math.sin(angle);\n        }\n\n        // CPU paddle collision (right)\n        const cpuLeft = LOGW - PADDLE_MARGIN - PADDLE_W;\n        const cpuRight = LOGW - PADDLE_MARGIN;\n        if (\n          gs.ball.vx > 0 &&\n          gs.ball.x + BALL_SIZE / 2 >= cpuLeft &&\n          gs.ball.x - BALL_SIZE / 2 <= cpuRight &&\n          gs.ball.y + BALL_SIZE / 2 >= gs.cpuY &&\n          gs.ball.y - BALL_SIZE / 2 <= gs.cpuY + PADDLE_H\n        ) {\n          gs.ball.x = cpuLeft - BALL_SIZE / 2;\n          const hitPos = (gs.ball.y - (gs.cpuY + PADDLE_H / 2)) / (PADDLE_H / 2);\n          const angle = hitPos * (Math.PI / 4);\n          const currentSpeed = Math.hypot(gs.ball.vx, gs.ball.vy);\n          const newSpeed = Math.max(currentSpeed, speed);\n          gs.ball.vx = -(newSpeed * Math.cos(angle));\n          gs.ball.vy = newSpeed * Math.sin(angle);\n        }\n\n        // Score check\n        let scored = false;\n        if (gs.ball.x < 0) {\n          // CPU scores\n          gs.cpuScore += 1;\n          gs.pointFlash = performance.now();\n          gs.pointWinner = \"cpu\";\n          scored = true;\n          const newCpuScore = gs.cpuScore;\n          const newPlayerScore = gs.playerScore;\n          setCpuScore(newCpuScore);\n          onScoreChange?.(newPlayerScore);\n          setAnnouncement(`CPU scores. Player ${newPlayerScore}, CPU ${newCpuScore}`);\n\n          if (gs.cpuScore >= MAX_SCORE) {\n            setStatus(\"gameover\");\n            setWinner(\"cpu\");\n            setAnnouncement(\"Game over. CPU wins!\");\n            onGameOver?.({ score: gs.playerScore, won: false });\n            gameStateRef.current = gs;\n          } else {\n            gs.ball = initBall(true, speed); // serve toward player (they lost)\n          }\n        } else if (gs.ball.x > LOGW) {\n          // Player scores\n          gs.playerScore += 1;\n          gs.pointFlash = performance.now();\n          gs.pointWinner = \"player\";\n          scored = true;\n          const newPlayerScore = gs.playerScore;\n          const newCpuScore = gs.cpuScore;\n          setPlayerScore(newPlayerScore);\n          onScoreChange?.(newPlayerScore);\n          setAnnouncement(`Player scores. Player ${newPlayerScore}, CPU ${newCpuScore}`);\n\n          if (gs.playerScore >= MAX_SCORE) {\n            setStatus(\"gameover\");\n            setWinner(\"player\");\n            setAnnouncement(\"Game over. You win!\");\n            onGameOver?.({ score: gs.playerScore, won: true });\n            gameStateRef.current = gs;\n          } else {\n            gs.ball = initBall(false, speed); // serve toward CPU (they lost)\n          }\n        }\n\n        if (scored) {\n          // Reset paddle positions gently\n          gs.playerY = LOGH / 2 - PADDLE_H / 2;\n          gs.cpuY = LOGH / 2 - PADDLE_H / 2;\n        }\n      } else if (!active) {\n        // Nothing\n      }\n\n      draw(gameStateRef.current);\n    };\n\n    raf = requestAnimationFrame(frame);\n    return () => cancelAnimationFrame(raf);\n  }, [onGameOver, onScoreChange]);\n\n  // Keyboard\n  React.useEffect(() => {\n    const onKeyDown = (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      const gameKeys = new Set([\"ArrowUp\", \"ArrowDown\", \"w\", \"s\", \"W\", \"S\"]);\n      if (gameKeys.has(e.key)) {\n        e.preventDefault();\n        keysRef.current.add(e.key);\n        if (statusRef.current !== \"playing\") start();\n      } else if (e.key === \" \" || e.key === \"Enter\") {\n        e.preventDefault();\n        if (statusRef.current !== \"playing\") start();\n      }\n    };\n    const onKeyUp = (e: KeyboardEvent) => {\n      keysRef.current.delete(e.key);\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\", onKeyDown as EventListener);\n    target?.addEventListener(\"keyup\", onKeyUp as EventListener);\n    return () => {\n      target?.removeEventListener(\"keydown\", onKeyDown as EventListener);\n      target?.removeEventListener(\"keyup\", onKeyUp as EventListener);\n    };\n  }, [start, captureGlobalKeys]);\n\n  // Pointer / touch drag for player paddle\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const getRelativeY = (clientY: number) => {\n      const rect = canvas.getBoundingClientRect();\n      return ((clientY - rect.top) / rect.height) * LOGH;\n    };\n\n    const onPointerDown = (e: PointerEvent) => {\n      canvas.setPointerCapture(e.pointerId);\n      pointerYRef.current = getRelativeY(e.clientY);\n      if (statusRef.current !== \"playing\") start();\n    };\n    const onPointerMove = (e: PointerEvent) => {\n      if (e.buttons === 0) return;\n      pointerYRef.current = getRelativeY(e.clientY);\n    };\n    const onPointerUp = () => {\n      pointerYRef.current = null;\n    };\n\n    canvas.addEventListener(\"pointerdown\", onPointerDown);\n    canvas.addEventListener(\"pointermove\", onPointerMove);\n    canvas.addEventListener(\"pointerup\", onPointerUp);\n    canvas.addEventListener(\"pointercancel\", onPointerUp);\n    return () => {\n      canvas.removeEventListener(\"pointerdown\", onPointerDown);\n      canvas.removeEventListener(\"pointermove\", onPointerMove);\n      canvas.removeEventListener(\"pointerup\", onPointerUp);\n      canvas.removeEventListener(\"pointercancel\", onPointerUp);\n    };\n  }, [start]);\n\n  React.useEffect(() => {\n    if (autoFocus) wrapperRef.current?.focus();\n  }, [autoFocus]);\n\n  return (\n    <div\n      ref={wrapperRef}\n      tabIndex={0}\n      role=\"application\"\n      aria-label=\"Pong game\"\n      style={width ? { maxWidth: width } : undefined}\n      className={cn(\n        \"relative flex w-full 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    >\n      {/* Score bar */}\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        >\n          You {playerScore}\n        </span>\n        <span className=\"text-muted-foreground text-xs\">First to {MAX_SCORE}</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          CPU {cpuScore}\n        </span>\n      </div>\n\n      {/* aria-live region */}\n      <div aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </div>\n\n      <div\n        ref={playfieldRef}\n        className=\"relative aspect-[3/2] w-full overflow-hidden rounded-lg border bg-background\"\n      >\n        <canvas ref={canvasRef} className=\"absolute inset-0 block h-full w-full touch-none\" />\n\n        {status !== \"playing\" && (\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 === \"gameover\" ? (\n                <>\n                  <p className=\"font-semibold text-lg\">\n                    {winner === \"player\" ? \"You win!\" : \"CPU wins\"}\n                  </p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    {playerScore} – {cpuScore}\n                  </p>\n                </>\n              ) : (\n                <>\n                  <p className=\"font-semibold text-lg\">Pong</p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    W/S or Arrow keys to move. Drag on mobile.\n                  </p>\n                </>\n              )}\n              <button\n                type=\"button\"\n                onClick={start}\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                {status === \"gameover\" ? \"Play again\" : \"Start\"}\n              </button>\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default Pong;\n",
      "type": "registry:component",
      "target": "@/components/games/pong.tsx"
    }
  ],
  "categories": [
    "games"
  ],
  "meta": {
    "iframeHeight": "460px",
    "surface": "canvas"
  }
}
