{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "pong-01",
	"title": "Pong 01",
	"type": "registry:block",
	"description": "A retro Pong game rendered on the Matrix display: keyboard paddle control, AI opponent, Web Audio sound effects, and a local (localStorage) win counter. No backend.",
	"registryDependencies": [
		"button",
		"card",
		"https://sv11.ui.twango.dev/r/matrix.json"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\texport type PongGameProps = { class?: string };\n</script>\n\n<script lang=\"ts\">\n\timport { onMount, onDestroy } from \"svelte\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { Button } from \"$UI$/button/index.js\";\n\timport { Card, CardContent } from \"$UI$/card/index.js\";\n\timport { Matrix, digits, type Frame } from \"$UI$/matrix/index.js\";\n\timport { PongEngine, COLS, ROWS, PADDLE_HEIGHT, type PongState } from \"./game-engine.js\";\n\timport { renderWord } from \"./bitmaps.js\";\n\timport { PongSounds } from \"./sound.js\";\n\timport { getWins, recordWin } from \"./score-store.js\";\n\n\tlet { class: className }: PongGameProps = $props();\n\n\tconst sounds = new PongSounds();\n\tconst engine = new PongEngine((sound) => sounds.play(sound));\n\n\tlet gameState = $state<PongState>(\"title\");\n\tlet playerScore = $state(0);\n\tlet aiScore = $state(0);\n\tlet wins = $state(0);\n\tlet frame = $state<Frame>(renderWord(\"PONG\"));\n\n\tlet playerInput = 0;\n\tlet raf = 0;\n\tlet lastTime = 0;\n\tlet winRecorded = false;\n\tlet container: HTMLDivElement | null = null;\n\n\tconst hint = $derived(\n\t\t{\n\t\t\ttitle: \"Press Space or tap Start · ↑ ↓ to move\",\n\t\t\tcountdown: \"Get ready…\",\n\t\t\tplaying: \"↑ ↓ to move · P to pause\",\n\t\t\tpaused: \"Paused · P to resume\",\n\t\t\tgameOver:\n\t\t\t\tengine.winner === \"player\" ? \"You win! Space to play again\" : \"CPU wins · Space to retry\",\n\t\t}[gameState]\n\t);\n\n\tfunction buildFrame(): Frame {\n\t\tif (engine.state === \"title\") return renderWord(\"PONG\");\n\t\tif (engine.state === \"gameOver\") return renderWord(engine.winner === \"player\" ? \"WIN\" : \"LOSE\");\n\n\t\tconst f: Frame = Array.from({ length: ROWS }, () => Array(COLS).fill(0));\n\t\tconst set = (r: number, c: number, v: number) => {\n\t\t\tconst rr = Math.round(r);\n\t\t\tconst cc = Math.round(c);\n\t\t\tif (rr >= 0 && rr < ROWS && cc >= 0 && cc < COLS) f[rr][cc] = v;\n\t\t};\n\n\t\tconst midCol = Math.floor(COLS / 2);\n\t\tfor (let r = 0; r < ROWS; r += 2) f[r][midCol] = 0.2;\n\n\t\tconst py = Math.round(engine.player.y);\n\t\tconst ay = Math.round(engine.ai.y);\n\t\tfor (let i = 0; i < PADDLE_HEIGHT; i++) {\n\t\t\tset(py + i, 0, 1);\n\t\t\tset(ay + i, COLS - 1, 1);\n\t\t}\n\n\t\tengine.ball.trail.forEach((p, i) => set(p.y, p.x, Math.max(0.15, 0.5 - i * 0.12)));\n\t\tset(engine.ball.y, engine.ball.x, 1);\n\t\treturn f;\n\t}\n\n\tfunction sync() {\n\t\tgameState = engine.state;\n\t\tplayerScore = engine.playerScore;\n\t\taiScore = engine.aiScore;\n\t\tif (engine.state === \"gameOver\" && engine.winner === \"player\" && !winRecorded) {\n\t\t\twinRecorded = true;\n\t\t\twins = recordWin();\n\t\t}\n\t\tframe = buildFrame();\n\t}\n\n\tfunction loop(now: number) {\n\t\tconst dt = lastTime ? Math.min(0.1, (now - lastTime) / 1000) : 0;\n\t\tlastTime = now;\n\t\tif (engine.state === \"playing\") engine.update(dt, playerInput);\n\t\tsync();\n\t\tif (engine.state === \"playing\" || engine.state === \"paused\") {\n\t\t\traf = requestAnimationFrame(loop);\n\t\t} else {\n\t\t\traf = 0;\n\t\t}\n\t}\n\n\tfunction startGame() {\n\t\tsounds.resume();\n\t\t// Focus the board so the keyboard controls work straight away (handlers are\n\t\t// scoped to the container, not the window, to avoid hijacking page scroll).\n\t\tcontainer?.focus();\n\t\twinRecorded = false;\n\t\tlastTime = 0;\n\t\tengine.startGame();\n\t\tsync();\n\t\tif (!raf) raf = requestAnimationFrame(loop);\n\t}\n\n\tfunction onKeydown(event: KeyboardEvent) {\n\t\tswitch (event.key) {\n\t\t\tcase \"ArrowUp\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tplayerInput = -1;\n\t\t\t\tbreak;\n\t\t\tcase \"ArrowDown\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tplayerInput = 1;\n\t\t\t\tbreak;\n\t\t\tcase \" \":\n\t\t\t\tif (engine.state === \"title\" || engine.state === \"gameOver\") {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tstartGame();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"p\":\n\t\t\tcase \"P\":\n\t\t\t\tif (engine.state === \"playing\" || engine.state === \"paused\") {\n\t\t\t\t\tengine.togglePause();\n\t\t\t\t\tsync();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tfunction onKeyup(event: KeyboardEvent) {\n\t\tif (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") playerInput = 0;\n\t}\n\n\t// Attach key handling imperatively (rather than markup on:keydown) so the board\n\t// is a focus-scoped, keyboard-driven widget: it only consumes arrow/space keys\n\t// while focused, leaving page scrolling intact and avoiding window-wide key\n\t// trapping on docs pages.\n\tfunction focusableBoard(node: HTMLDivElement) {\n\t\tcontainer = node;\n\t\tnode.tabIndex = 0;\n\t\tnode.addEventListener(\"keydown\", onKeydown);\n\t\tnode.addEventListener(\"keyup\", onKeyup);\n\t\treturn {\n\t\t\tdestroy() {\n\t\t\t\tcontainer = null;\n\t\t\t\tnode.removeEventListener(\"keydown\", onKeydown);\n\t\t\t\tnode.removeEventListener(\"keyup\", onKeyup);\n\t\t\t},\n\t\t};\n\t}\n\n\tonMount(() => {\n\t\twins = getWins();\n\t});\n\n\tonDestroy(() => {\n\t\tif (raf) cancelAnimationFrame(raf);\n\t\tsounds.destroy();\n\t});\n\n\tconst canStart = $derived(gameState === \"title\" || gameState === \"gameOver\");\n</script>\n\n<div\n\tuse:focusableBoard\n\trole=\"group\"\n\taria-label=\"Pong game\"\n\tclass=\"focus-visible:ring-ring/50 rounded-xl outline-none focus-visible:ring-2\"\n>\n\t<Card class={cn(\"mx-auto w-full max-w-2xl\", className)}>\n\t\t<CardContent class=\"flex flex-col items-center gap-5 py-6\">\n\t\t\t<div class=\"flex w-full max-w-md items-center justify-between\">\n\t\t\t\t<div class=\"flex flex-col items-center gap-1\">\n\t\t\t\t\t<Matrix rows={7} cols={5} pattern={digits[playerScore]} size={9} gap={2} />\n\t\t\t\t\t<span class=\"text-muted-foreground text-[10px] tracking-widest\">YOU</span>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"text-muted-foreground font-mono text-xs tracking-widest\">\n\t\t\t\t\tWINS {wins.toString().padStart(3, \"0\")}\n\t\t\t\t</span>\n\t\t\t\t<div class=\"flex flex-col items-center gap-1\">\n\t\t\t\t\t<Matrix rows={7} cols={5} pattern={digits[aiScore]} size={9} gap={2} />\n\t\t\t\t\t<span class=\"text-muted-foreground text-[10px] tracking-widest\">CPU</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<Matrix\n\t\t\t\trows={ROWS}\n\t\t\t\tcols={COLS}\n\t\t\t\tpattern={frame}\n\t\t\t\tsize={16}\n\t\t\t\tgap={3}\n\t\t\t\tclass=\"text-foreground\"\n\t\t\t\taria-label=\"Pong game board\"\n\t\t\t/>\n\n\t\t\t<div class=\"flex flex-col items-center gap-3\">\n\t\t\t\t<p class=\"text-muted-foreground h-4 text-center text-xs\">{hint}</p>\n\t\t\t\t{#if canStart}\n\t\t\t\t\t<Button size=\"sm\" onclick={startGame}>\n\t\t\t\t\t\t{gameState === \"title\" ? \"Start\" : \"Play again\"}\n\t\t\t\t\t</Button>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</CardContent>\n\t</Card>\n</div>\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/pong-game.svelte"
		},
		{
			"content": "// Pure Pong physics — no framework, no DOM. The component drives it with a\n// requestAnimationFrame loop and renders the state onto a Matrix display.\n\nexport const COLS = 21;\nexport const ROWS = 7;\nexport const PADDLE_HEIGHT = 3;\nexport const WIN_SCORE = 3;\n\nconst INITIAL_SPEED = 0.15;\nconst MAX_SPEED = 0.4;\nconst SPEED_INCREASE = 1.08;\nconst PLAYER_SPEED = 0.32;\nconst AI_REACTION_SPEED = 0.14;\nconst AI_PREDICTION_ERROR = 1.2;\nconst TRAIL_LENGTH = 4;\n\nexport type PongState = \"title\" | \"countdown\" | \"playing\" | \"paused\" | \"gameOver\";\nexport type PongSound = \"paddleHit\" | \"wallHit\" | \"score\" | \"gameStart\" | \"win\";\n\ntype Point = { x: number; y: number };\n\nexport class PongEngine {\n\tstate: PongState = \"title\";\n\tball = { x: 10, y: 3, velX: INITIAL_SPEED, velY: 0.05, trail: [] as Point[] };\n\tplayer = { y: 2, targetY: 2 };\n\tai = { y: 2, targetY: 2 };\n\tplayerScore = 0;\n\taiScore = 0;\n\twinner: \"player\" | \"ai\" | null = null;\n\n\t#onSound?: (sound: PongSound) => void;\n\n\tconstructor(onSound?: (sound: PongSound) => void) {\n\t\tthis.#onSound = onSound;\n\t\tthis.#centerPaddles();\n\t}\n\n\t#centerPaddles() {\n\t\tconst mid = (ROWS - PADDLE_HEIGHT) / 2;\n\t\tthis.player.y = mid;\n\t\tthis.player.targetY = mid;\n\t\tthis.ai.y = mid;\n\t\tthis.ai.targetY = mid;\n\t}\n\n\tstartGame() {\n\t\tthis.playerScore = 0;\n\t\tthis.aiScore = 0;\n\t\tthis.winner = null;\n\t\tthis.#centerPaddles();\n\t\tthis.resetBall(Math.random() < 0.5);\n\t\tthis.state = \"playing\";\n\t\tthis.#onSound?.(\"gameStart\");\n\t}\n\n\ttogglePause() {\n\t\tif (this.state === \"playing\") this.state = \"paused\";\n\t\telse if (this.state === \"paused\") this.state = \"playing\";\n\t}\n\n\tresetBall(towardPlayer: boolean) {\n\t\tthis.ball.x = (COLS - 1) / 2;\n\t\tthis.ball.y = Math.random() * (ROWS - 2) + 1;\n\t\tthis.ball.velX = (towardPlayer ? -1 : 1) * INITIAL_SPEED;\n\t\tthis.ball.velY = (Math.random() - 0.5) * 0.12;\n\t\tthis.ball.trail = [];\n\t}\n\n\t/** Advance the simulation. `dt` is seconds since the last frame. */\n\tupdate(dt: number, playerInput: number) {\n\t\tif (this.state !== \"playing\") return;\n\t\tconst step = dt * 60; // velocities are tuned per 60fps frame\n\n\t\t// Player paddle follows input.\n\t\tthis.player.y = clamp(\n\t\t\tthis.player.y + playerInput * PLAYER_SPEED * step,\n\t\t\t0,\n\t\t\tROWS - PADDLE_HEIGHT\n\t\t);\n\n\t\t// AI predicts where the ball will cross its column and eases toward it.\n\t\tif (this.ball.velX > 0) {\n\t\t\tconst framesToReach = (COLS - 1 - this.ball.x) / Math.max(this.ball.velX, 0.001);\n\t\t\tconst predicted = this.ball.y + this.ball.velY * framesToReach;\n\t\t\tconst bounced = reflect(predicted, 0, ROWS - 1);\n\t\t\tthis.ai.targetY = bounced - PADDLE_HEIGHT / 2 + (Math.random() - 0.5) * AI_PREDICTION_ERROR;\n\t\t}\n\t\tthis.ai.targetY = clamp(this.ai.targetY, 0, ROWS - PADDLE_HEIGHT);\n\t\tthis.ai.y += (this.ai.targetY - this.ai.y) * AI_REACTION_SPEED * step;\n\t\tthis.ai.y = clamp(this.ai.y, 0, ROWS - PADDLE_HEIGHT);\n\n\t\t// Ball trail.\n\t\tthis.ball.trail.unshift({ x: this.ball.x, y: this.ball.y });\n\t\tif (this.ball.trail.length > TRAIL_LENGTH) this.ball.trail.pop();\n\n\t\t// Ball motion.\n\t\tthis.ball.x += this.ball.velX * step;\n\t\tthis.ball.y += this.ball.velY * step;\n\n\t\t// Top / bottom walls.\n\t\tif (this.ball.y <= 0) {\n\t\t\tthis.ball.y = 0;\n\t\t\tthis.ball.velY = Math.abs(this.ball.velY);\n\t\t\tthis.#onSound?.(\"wallHit\");\n\t\t} else if (this.ball.y >= ROWS - 1) {\n\t\t\tthis.ball.y = ROWS - 1;\n\t\t\tthis.ball.velY = -Math.abs(this.ball.velY);\n\t\t\tthis.#onSound?.(\"wallHit\");\n\t\t}\n\n\t\t// Player paddle (column 0).\n\t\tif (this.ball.velX < 0 && this.ball.x <= 1) {\n\t\t\tif (this.#hits(this.player.y)) {\n\t\t\t\tthis.ball.x = 1;\n\t\t\t\tthis.#bounce(this.player.y, 1);\n\t\t\t}\n\t\t}\n\t\t// AI paddle (column COLS-1).\n\t\tif (this.ball.velX > 0 && this.ball.x >= COLS - 2) {\n\t\t\tif (this.#hits(this.ai.y)) {\n\t\t\t\tthis.ball.x = COLS - 2;\n\t\t\t\tthis.#bounce(this.ai.y, -1);\n\t\t\t}\n\t\t}\n\n\t\t// Scoring.\n\t\tif (this.ball.x < 0) this.#score(\"ai\");\n\t\telse if (this.ball.x > COLS - 1) this.#score(\"player\");\n\t}\n\n\t#hits(paddleY: number) {\n\t\treturn this.ball.y >= paddleY - 0.5 && this.ball.y <= paddleY + PADDLE_HEIGHT - 0.5;\n\t}\n\n\t#bounce(paddleY: number, direction: 1 | -1) {\n\t\tconst center = paddleY + PADDLE_HEIGHT / 2 - 0.5;\n\t\tconst offset = (this.ball.y - center) / (PADDLE_HEIGHT / 2);\n\t\tconst speed = Math.min(Math.abs(this.ball.velX) * SPEED_INCREASE, MAX_SPEED);\n\t\tthis.ball.velX = direction * speed;\n\t\tthis.ball.velY = clamp(this.ball.velY + offset * 0.12, -MAX_SPEED, MAX_SPEED);\n\t\tthis.#onSound?.(\"paddleHit\");\n\t}\n\n\t#score(scorer: \"player\" | \"ai\") {\n\t\tif (scorer === \"player\") this.playerScore++;\n\t\telse this.aiScore++;\n\t\tthis.#onSound?.(\"score\");\n\n\t\tif (this.playerScore >= WIN_SCORE || this.aiScore >= WIN_SCORE) {\n\t\t\tthis.winner = this.playerScore > this.aiScore ? \"player\" : \"ai\";\n\t\t\tthis.state = \"gameOver\";\n\t\t\tthis.#onSound?.(\"win\");\n\t\t\treturn;\n\t\t}\n\t\tthis.resetBall(scorer === \"ai\");\n\t}\n}\n\nfunction clamp(value: number, min: number, max: number) {\n\treturn Math.max(min, Math.min(max, value));\n}\n\n/** Reflect a value into the [min, max] range as if bouncing off the edges. */\nfunction reflect(value: number, min: number, max: number) {\n\tconst span = max - min;\n\tif (span <= 0) return min;\n\tconst range = span * 2;\n\tlet t = (value - min) % range;\n\tif (t < 0) t += range;\n\treturn min + (t <= span ? t : range - t);\n}\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/game-engine.ts"
		},
		{
			"content": "import type { Frame } from \"$UI$/matrix/index.js\";\nimport { COLS, ROWS } from \"./game-engine.js\";\n\n// 3×5 bitmap font, just the glyphs needed for PONG / WIN / LOSE.\nconst GLYPHS: Record<string, number[][]> = {\n\tP: [\n\t\t[1, 1, 1],\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t\t[1, 0, 0],\n\t\t[1, 0, 0],\n\t],\n\tO: [\n\t\t[1, 1, 1],\n\t\t[1, 0, 1],\n\t\t[1, 0, 1],\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t],\n\tN: [\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t\t[1, 1, 1],\n\t\t[1, 1, 1],\n\t\t[1, 0, 1],\n\t],\n\tG: [\n\t\t[1, 1, 1],\n\t\t[1, 0, 0],\n\t\t[1, 0, 1],\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t],\n\tW: [\n\t\t[1, 0, 1],\n\t\t[1, 0, 1],\n\t\t[1, 1, 1],\n\t\t[1, 1, 1],\n\t\t[1, 0, 1],\n\t],\n\tI: [\n\t\t[1, 1, 1],\n\t\t[0, 1, 0],\n\t\t[0, 1, 0],\n\t\t[0, 1, 0],\n\t\t[1, 1, 1],\n\t],\n\tL: [\n\t\t[1, 0, 0],\n\t\t[1, 0, 0],\n\t\t[1, 0, 0],\n\t\t[1, 0, 0],\n\t\t[1, 1, 1],\n\t],\n\tS: [\n\t\t[1, 1, 1],\n\t\t[1, 0, 0],\n\t\t[1, 1, 1],\n\t\t[0, 0, 1],\n\t\t[1, 1, 1],\n\t],\n\tE: [\n\t\t[1, 1, 1],\n\t\t[1, 0, 0],\n\t\t[1, 1, 1],\n\t\t[1, 0, 0],\n\t\t[1, 1, 1],\n\t],\n};\n\nconst GLYPH_W = 3;\nconst GLYPH_H = 5;\n\n/** Render a short word centered in a ROWS×COLS frame using the bitmap font. */\nexport function renderWord(word: string, cols = COLS, rows = ROWS): Frame {\n\tconst letters = word.toUpperCase().split(\"\");\n\tconst width = letters.length * GLYPH_W + (letters.length - 1);\n\tconst startCol = Math.floor((cols - width) / 2);\n\tconst startRow = Math.floor((rows - GLYPH_H) / 2);\n\n\tconst frame: Frame = Array.from({ length: rows }, () => Array(cols).fill(0));\n\tlet col = startCol;\n\tfor (const letter of letters) {\n\t\tconst glyph = GLYPHS[letter];\n\t\tif (glyph) {\n\t\t\tfor (let r = 0; r < GLYPH_H; r++) {\n\t\t\t\tfor (let c = 0; c < GLYPH_W; c++) {\n\t\t\t\t\tconst row = startRow + r;\n\t\t\t\t\tconst cc = col + c;\n\t\t\t\t\tif (row >= 0 && row < rows && cc >= 0 && cc < cols) frame[row][cc] = glyph[r][c];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcol += GLYPH_W + 1;\n\t}\n\treturn frame;\n}\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/bitmaps.ts"
		},
		{
			"content": "import type { PongSound } from \"./game-engine.js\";\n\ntype Tone = { freq: number; type: OscillatorType; duration: number };\n\nconst TONES: Record<PongSound, Tone> = {\n\tpaddleHit: { freq: 440, type: \"square\", duration: 0.05 },\n\twallHit: { freq: 220, type: \"square\", duration: 0.04 },\n\tscore: { freq: 660, type: \"sine\", duration: 0.14 },\n\tgameStart: { freq: 523, type: \"triangle\", duration: 0.12 },\n\twin: { freq: 784, type: \"sine\", duration: 0.22 },\n};\n\n/**\n * Self-contained sound effects synthesized with the Web Audio API — no audio\n * files, no network. Construct lazily and call `resume()` from a user gesture\n * to satisfy autoplay policies.\n */\nexport class PongSounds {\n\t#ctx: AudioContext | null = null;\n\tenabled = true;\n\n\t#context(): AudioContext | null {\n\t\tif (typeof window === \"undefined\") return null;\n\t\tconst Ctor =\n\t\t\twindow.AudioContext ??\n\t\t\t(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\t\tif (!Ctor) return null;\n\t\tthis.#ctx ??= new Ctor();\n\t\treturn this.#ctx;\n\t}\n\n\tresume() {\n\t\tvoid this.#context()?.resume();\n\t}\n\n\tplay(sound: PongSound) {\n\t\tif (!this.enabled) return;\n\t\tconst ctx = this.#context();\n\t\tif (!ctx) return;\n\t\tconst tone = TONES[sound];\n\t\tconst now = ctx.currentTime;\n\n\t\tconst osc = ctx.createOscillator();\n\t\tconst gain = ctx.createGain();\n\t\tosc.type = tone.type;\n\t\tosc.frequency.setValueAtTime(tone.freq, now);\n\t\tif (sound === \"score\" || sound === \"win\") {\n\t\t\tosc.frequency.exponentialRampToValueAtTime(tone.freq * 1.5, now + tone.duration);\n\t\t}\n\t\tgain.gain.setValueAtTime(0.0001, now);\n\t\tgain.gain.exponentialRampToValueAtTime(0.18, now + 0.005);\n\t\tgain.gain.exponentialRampToValueAtTime(0.0001, now + tone.duration);\n\n\t\tosc.connect(gain).connect(ctx.destination);\n\t\tosc.start(now);\n\t\tosc.stop(now + tone.duration + 0.02);\n\t}\n\n\t/** Release the AudioContext. Call from the host's teardown so repeated\n\t * mount/unmount cycles don't accumulate live contexts. */\n\tdestroy() {\n\t\tconst ctx = this.#ctx;\n\t\tthis.#ctx = null;\n\t\tif (ctx && ctx.state !== \"closed\") void ctx.close().catch(() => {});\n\t}\n}\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/sound.ts"
		},
		{
			"content": "// Local-only win counter (no backend). The upstream block tracked a live player\n// count via Redis; here we persist the player's wins in localStorage instead, so\n// the block stays self-contained and provider-agnostic.\n\nconst KEY = \"sv11-pong-01-wins\";\n\nexport function getWins(): number {\n\tif (typeof localStorage === \"undefined\") return 0;\n\ttry {\n\t\tconst value = Number(localStorage.getItem(KEY));\n\t\treturn Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n\t} catch {\n\t\treturn 0; // storage unavailable (private mode, blocked) — treat as no wins\n\t}\n}\n\nexport function recordWin(): number {\n\tconst next = getWins() + 1;\n\ttry {\n\t\tlocalStorage.setItem(KEY, String(next));\n\t} catch {\n\t\t/* storage unavailable (private mode, etc.) — ignore */\n\t}\n\treturn next;\n}\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/score-store.ts"
		},
		{
			"content": "import Pong01 from \"./pong-game.svelte\";\n\nexport { Pong01, Pong01 as default };\nexport type { PongGameProps } from \"./pong-game.svelte\";\nexport { PongEngine, COLS, ROWS, PADDLE_HEIGHT, WIN_SCORE } from \"./game-engine.js\";\nexport type { PongState, PongSound } from \"./game-engine.js\";\nexport { PongSounds } from \"./sound.js\";\nexport { getWins, recordWin } from \"./score-store.js\";\n",
			"type": "registry:block",
			"target": "components/blocks/pong-01/index.ts"
		}
	]
}