{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dino-runner",
  "type": "registry:component",
  "title": "Dino Runner",
  "description": "The offline-dino endless runner. Jump the cacti. Perfect for 404 pages.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/games/dino-runner.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 (design) coordinate space. All draw + physics happen in this space;\n// the canvas is scaled to fit the responsive playfield.\nconst LOGW = 480;\nconst LOGH = 180;\n\nconst GROUND_Y_RATIO = 0.72; // ground line as fraction of logical height\nconst GROUND_Y = Math.floor(LOGH * GROUND_Y_RATIO);\n\nconst DINO_W = 28;\nconst DINO_H = 32;\nconst DINO_X = 60; // fixed horizontal position\n\nconst GRAVITY = 1800; // px/s²\nconst JUMP_VY = -560; // px/s (negative = upward) — clears tallest cactus at apex\nconst DUCK_HEIGHT = 18; // reduced height when ducking\nconst INITIAL_SPEED = 220; // px/s\nconst SPEED_INCREMENT = 8; // px/s per 100 score points\nconst MAX_SPEED = 600;\n\nconst SCORE_RATE = 10; // score points per second\n\n// Collision forgiveness: inset both boxes so a near-miss isn't a hit.\nconst DINO_INSET_X = 5;\nconst DINO_INSET_Y = 5;\nconst CACTUS_INSET = 2;\n\n/* -------------------------------------------------------------------------- */\n/*                               Game types                                    */\n/* -------------------------------------------------------------------------- */\n\ninterface Obstacle {\n  id: number;\n  x: number;\n  w: number;\n  h: number;\n}\n\ninterface DinoState {\n  y: number; // height of dino bottom above ground (0 = on ground)\n  vy: number; // vertical velocity px/s (positive = downward)\n  ducking: boolean;\n  jumping: boolean;\n  step: number; // accumulated phase for running leg animation\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Props                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface DinoRunnerProps {\n  className?: string;\n  /** Optional max width (px) of the responsive game; defaults to fluid full-width. */\n  width?: 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\n/* -------------------------------------------------------------------------- */\n/*                               Component                                      */\n/* -------------------------------------------------------------------------- */\n\nexport function DinoRunner({\n  className,\n  width,\n  paused = false,\n  autoFocus = true,\n  captureGlobalKeys = true,\n  persistHighScore = true,\n  onScoreChange,\n  onGameOver,\n  onStart,\n}: DinoRunnerProps) {\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);\n  const reduce = usePrefersReducedMotion();\n\n  const [status, setStatus] = React.useState<\"idle\" | \"playing\" | \"gameover\">(\"idle\");\n  const [score, setScore] = React.useState(0);\n  const [high, setHigh] = React.useState(0);\n  const [offscreen, setOffscreen] = React.useState(false);\n\n  const storageKey =\n    typeof persistHighScore === \"string\"\n      ? persistHighScore\n      : persistHighScore === false\n        ? null\n        : \"gamekitui:dino-runner:hi\";\n\n  // ---- mutable live refs (read by rAF loop 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  statusRef.current = status;\n  pausedRef.current = paused;\n  offscreenRef.current = offscreen;\n  themeRef.current = theme;\n  reduceRef.current = reduce;\n\n  // ---- game state refs ----\n  const dinoRef = React.useRef<DinoState>({\n    y: 0,\n    vy: 0,\n    ducking: false,\n    jumping: false,\n    step: 0,\n  });\n  const obstaclesRef = React.useRef<Obstacle[]>([]);\n  const speedRef = React.useRef(INITIAL_SPEED);\n  const scoreRef = React.useRef(0);\n  const nextObstacleIdRef = React.useRef(0);\n  const nextObstacleXRef = React.useRef(LOGW + 80);\n  // accumulated score fraction (for smooth scoring)\n  const scoreAccRef = React.useRef(0);\n  // ground scroll for decorative ticks\n  const groundOffsetRef = React.useRef(0);\n\n  // ---- load high score ----\n  React.useEffect(() => {\n    if (!storageKey) return;\n    try {\n      const raw = window.localStorage.getItem(storageKey);\n      if (raw) setHigh(Number(raw) || 0);\n    } catch {\n      /* ignore */\n    }\n  }, [storageKey]);\n\n  const submitHigh = React.useCallback(\n    (s: number) => {\n      setHigh((prev) => {\n        if (s <= prev) return prev;\n        if (storageKey) {\n          try {\n            window.localStorage.setItem(storageKey, String(s));\n          } catch {\n            /* ignore */\n          }\n        }\n        return s;\n      });\n    },\n    [storageKey],\n  );\n\n  // ---- offscreen / visibility pause ----\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  // ---- start / reset ----\n  const start = React.useCallback(() => {\n    dinoRef.current = { y: 0, vy: 0, ducking: false, jumping: false, step: 0 };\n    obstaclesRef.current = [];\n    speedRef.current = INITIAL_SPEED;\n    scoreRef.current = 0;\n    scoreAccRef.current = 0;\n    groundOffsetRef.current = 0;\n    nextObstacleIdRef.current = 0;\n    nextObstacleXRef.current = LOGW + 80 + Math.random() * 200;\n    setScore(0);\n    setStatus(\"playing\");\n    onStart?.();\n    onScoreChange?.(0);\n    // Focus the wrapper so keyboard controls work after the Start button unmounts.\n    wrapperRef.current?.focus();\n  }, [onStart, onScoreChange]);\n\n  // ---- responsive canvas sizing: match playfield CSS size × DPR, scale to 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 = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      const scale = w / LOGW;\n      // Draw in logical space (LOGW × LOGH); transform maps it onto the device pixels.\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  // ---- main rAF loop + draw ----\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 groundY = GROUND_Y;\n\n    // ---- obstacle spawning helpers ----\n    const spawnGap = () => 280 + Math.random() * 220;\n\n    const spawnObstacle = (): Obstacle => {\n      const h = 22 + Math.random() * 22; // 22–44 px tall\n      const w = 12 + Math.random() * 10; // 12–22 px wide\n      return {\n        id: nextObstacleIdRef.current++,\n        x: nextObstacleXRef.current,\n        w,\n        h,\n      };\n    };\n\n    // ---- draw helpers ----\n    const roundRect = (\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 drawDino = (t: ThemeTokens, dino: DinoState) => {\n      const ducking = dino.ducking;\n      const dh = ducking ? DUCK_HEIGHT : DINO_H;\n      // Bottom of the body sits `dino.y` above the ground.\n      const dy = groundY - dh - dino.y; // top pixel of dino body\n\n      const bodyColor = t.primary || \"#000\";\n      const legColor = t.primary || \"#000\";\n\n      // body\n      roundRect(DINO_X, dy, DINO_W, dh, 5, bodyColor);\n\n      // eye (white)\n      const eyeR = 3;\n      const eyeX = DINO_X + DINO_W - eyeR - 4;\n      const eyeY = dy + eyeR + 4;\n      ctx.fillStyle = t[\"primary-foreground\"] || \"#fff\";\n      ctx.beginPath();\n      ctx.arc(eyeX, eyeY, eyeR, 0, Math.PI * 2);\n      ctx.fill();\n\n      // pupil\n      ctx.fillStyle = bodyColor;\n      ctx.beginPath();\n      ctx.arc(eyeX + 1, eyeY + 1, 1.5, 0, Math.PI * 2);\n      ctx.fill();\n\n      if (!ducking) {\n        const legW = 7;\n        const bodyBottom = dy + dh;\n        const airborne = dino.jumping || dino.y > 0.5;\n\n        if (airborne) {\n          // Tucked jump pose: both legs short and pulled up.\n          const legH = 6;\n          roundRect(DINO_X + 4, bodyBottom - 2, legW, legH, 2, legColor);\n          roundRect(DINO_X + 14, bodyBottom - 2, legW, legH, 2, legColor);\n        } else {\n          // Running: alternate two leg poses based on the step phase.\n          const phase = Math.floor(dino.step) % 2 === 0;\n          const frontH = phase ? 10 : 5;\n          const backH = phase ? 5 : 10;\n          roundRect(DINO_X + 4, bodyBottom, legW, backH, 2, legColor);\n          roundRect(DINO_X + 14, bodyBottom, legW, frontH, 2, legColor);\n        }\n      }\n    };\n\n    const drawObstacle = (t: ThemeTokens, obs: Obstacle) => {\n      const x = obs.x;\n      const y = groundY - obs.h;\n      const cactusColor = t[\"muted-foreground\"] || \"#555\";\n\n      // main stem\n      roundRect(x, y, obs.w, obs.h, 3, cactusColor);\n\n      // arm stubs for taller cacti\n      if (obs.h > 30) {\n        const armW = Math.floor(obs.w * 0.7);\n        const armH = 5;\n        const midY = y + obs.h * 0.35;\n        roundRect(x - armW + 2, midY, armW, armH, 2, cactusColor);\n        roundRect(x + obs.w - 2, midY + 4, armW, armH, 2, cactusColor);\n      }\n    };\n\n    const drawGround = (t: ThemeTokens) => {\n      ctx.strokeStyle = t[\"muted-foreground\"] || \"#888\";\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(0, groundY);\n      ctx.lineTo(LOGW, groundY);\n      ctx.stroke();\n\n      // scrolling tick marks\n      ctx.strokeStyle = t[\"muted-foreground\"] || \"#888\";\n      ctx.lineWidth = 1;\n      ctx.globalAlpha = 0.4;\n      const spacing = 40;\n      const offset = groundOffsetRef.current % spacing;\n      for (let x = -offset; x < LOGW; x += spacing) {\n        ctx.beginPath();\n        ctx.moveTo(x, groundY + 2);\n        ctx.lineTo(x + 8, groundY + 2);\n        ctx.stroke();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const draw = () => {\n      const t = themeRef.current;\n      ctx.clearRect(0, 0, LOGW, LOGH);\n      ctx.fillStyle = t.background || \"#fff\";\n      ctx.fillRect(0, 0, LOGW, LOGH);\n\n      drawGround(t);\n\n      for (const obs of obstaclesRef.current) {\n        drawObstacle(t, obs);\n      }\n\n      drawDino(t, dinoRef.current);\n    };\n\n    // ---- collision detection (tight AABB on both axes, with forgiveness inset) ----\n    const checkCollision = (dino: DinoState): boolean => {\n      const dh = dino.ducking ? DUCK_HEIGHT : DINO_H;\n      // Dino body box in logical space.\n      const dinoLeft = DINO_X + DINO_INSET_X;\n      const dinoRight = DINO_X + DINO_W - DINO_INSET_X;\n      const dinoBottom = groundY - dino.y; // rises as the dino jumps\n      const dinoTop = dinoBottom - dh;\n      // Apply vertical forgiveness.\n      const dTop = dinoTop + DINO_INSET_Y;\n      const dBottom = dinoBottom - DINO_INSET_Y;\n\n      for (const obs of obstaclesRef.current) {\n        const cLeft = obs.x + CACTUS_INSET;\n        const cRight = obs.x + obs.w - CACTUS_INSET;\n        const cTop = groundY - obs.h + CACTUS_INSET;\n        const cBottom = groundY - CACTUS_INSET;\n\n        const overlapX = dinoLeft < cRight && dinoRight > cLeft;\n        const overlapY = dTop < cBottom && dBottom > cTop;\n        if (overlapX && overlapY) return true;\n      }\n      return false;\n    };\n\n    // ---- game over callback holder ----\n    const handleGameOver = (finalScore: number) => {\n      setStatus(\"gameover\");\n      submitHigh(finalScore);\n      onGameOver?.({ score: finalScore, won: false });\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 dtMs = now - last;\n      last = now;\n\n      const active =\n        statusRef.current === \"playing\" &&\n        !pausedRef.current &&\n        !offscreenRef.current;\n\n      if (active) {\n        // cap dt to avoid tunneling on tab refocus; freeze under reduced motion\n        const dt = reduceRef.current ? 0 : Math.min(dtMs / 1000, 0.05);\n\n        const dino = dinoRef.current;\n        const speed = speedRef.current;\n\n        // ---- physics ----\n        if (dino.jumping || dino.y > 0) {\n          dino.vy += GRAVITY * dt;\n          dino.y -= dino.vy * dt;\n          if (dino.y <= 0) {\n            dino.y = 0;\n            dino.vy = 0;\n            dino.jumping = false;\n          }\n        } else {\n          // ---- running leg animation: advance step phase proportional to speed ----\n          dino.step += dt * (speed / 30);\n        }\n\n        // ---- scroll obstacles ----\n        groundOffsetRef.current += speed * dt;\n        const surviving: Obstacle[] = [];\n        for (const obs of obstaclesRef.current) {\n          obs.x -= speed * dt;\n          if (obs.x + obs.w > -10) surviving.push(obs);\n        }\n        obstaclesRef.current = surviving;\n\n        // ---- spawn new obstacle ----\n        nextObstacleXRef.current -= speed * dt;\n        if (nextObstacleXRef.current <= LOGW) {\n          obstaclesRef.current.push(spawnObstacle());\n          nextObstacleXRef.current = LOGW + spawnGap();\n        }\n\n        // ---- score ----\n        scoreAccRef.current += SCORE_RATE * dt;\n        const newScore = Math.floor(scoreAccRef.current);\n        if (newScore !== scoreRef.current) {\n          scoreRef.current = newScore;\n          setScore(newScore);\n          onScoreChange?.(newScore);\n          // ramp speed\n          const targetSpeed = Math.min(\n            INITIAL_SPEED + Math.floor(newScore / 100) * SPEED_INCREMENT,\n            MAX_SPEED,\n          );\n          speedRef.current = targetSpeed;\n        }\n\n        // ---- collision ----\n        if (checkCollision(dino)) {\n          handleGameOver(scoreRef.current);\n        }\n      }\n\n      draw();\n    };\n\n    raf = requestAnimationFrame(frame);\n    return () => cancelAnimationFrame(raf);\n  }, [onGameOver, onScoreChange, submitHigh]);\n\n  // ---- keyboard input ----\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\n      const key = e.key;\n      const isJump = key === \" \" || key === \"ArrowUp\" || key === \"w\" || key === \"W\";\n      const isDuck = key === \"ArrowDown\" || key === \"s\" || key === \"S\";\n\n      if (!isJump && !isDuck) return;\n      e.preventDefault();\n\n      if (statusRef.current === \"idle\") {\n        start();\n        return;\n      }\n      if (statusRef.current === \"gameover\") {\n        if (isJump) start();\n        return;\n      }\n\n      const dino = dinoRef.current;\n      if (isJump && !dino.jumping && dino.y === 0) {\n        dino.vy = JUMP_VY;\n        dino.jumping = true;\n        dino.ducking = false;\n      }\n      if (isDuck) {\n        dino.ducking = true;\n      }\n    };\n\n    const onKeyUp = (e: KeyboardEvent) => {\n      const isDuck = e.key === \"ArrowDown\" || e.key === \"s\" || e.key === \"S\";\n      if (isDuck) dinoRef.current.ducking = false;\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    target?.addEventListener(\"keyup\", onKeyUp as EventListener);\n    return () => {\n      target?.removeEventListener(\"keydown\", onKey as EventListener);\n      target?.removeEventListener(\"keyup\", onKeyUp as EventListener);\n    };\n  }, [start, captureGlobalKeys]);\n\n  // ---- pointer input (mouse + touch + pen): tap/click to start & jump ----\n  React.useEffect(() => {\n    const el = wrapperRef.current;\n    if (!el) return;\n    const onPointerDown = (e: PointerEvent) => {\n      // Let overlay buttons (Start / Play again) handle their own clicks.\n      if ((e.target as HTMLElement | null)?.closest(\"button\")) return;\n      e.preventDefault();\n      if (statusRef.current !== \"playing\") {\n        start();\n        return;\n      }\n      const dino = dinoRef.current;\n      if (!dino.jumping && dino.y === 0) {\n        dino.vy = JUMP_VY;\n        dino.jumping = true;\n        dino.ducking = false;\n      }\n    };\n    el.addEventListener(\"pointerdown\", onPointerDown);\n    return () => el.removeEventListener(\"pointerdown\", onPointerDown);\n  }, [start]);\n\n  // ---- autofocus ----\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=\"Dino Runner 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 className=\"inline-flex items-center rounded-md border bg-secondary px-2 py-0.5 font-medium text-secondary-foreground tabular-nums\">\n          Score {score}\n        </span>\n        <span className=\"text-muted-foreground tabular-nums\">Best {high}</span>\n      </div>\n\n      {/* Responsive playfield */}\n      <div\n        ref={playfieldRef}\n        className=\"relative aspect-[8/3] 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        {/* Overlay */}\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\">Game over</p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    You scored {score}\n                    {score >= high && score > 0 ? \" — new best!\" : \"\"}\n                  </p>\n                </>\n              ) : (\n                <>\n                  <p className=\"font-semibold text-lg\">Dino Runner</p>\n                  <p className=\"text-muted-foreground text-sm\">\n                    Space / Up / W to jump. Down / S to duck.\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\n      {/* Accessible live region */}\n      <div aria-live=\"polite\" className=\"sr-only\">\n        {status === \"playing\" ? `Score: ${score}` : null}\n        {status === \"gameover\" ? `Game over. Final score: ${score}. Best: ${high}.` : null}\n      </div>\n    </div>\n  );\n}\n\nexport default DinoRunner;\n",
      "type": "registry:component",
      "target": "@/components/games/dino-runner.tsx"
    }
  ],
  "categories": [
    "games"
  ],
  "meta": {
    "iframeHeight": "420px",
    "surface": "canvas"
  }
}
