diff --git a/src/App.tsx b/src/App.tsx
index fbf4355..8230013 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,20 @@
import { AppRoutes } from './app/AppRoutes'
+import { useKeyboardInput } from './input/useKeyboardInput'
+import { usePoseInput } from './input/usePoseInput'
+
+function InputControllers() {
+ useKeyboardInput()
+ usePoseInput()
+ return null
+}
function App() {
- return
+ return (
+ <>
+
+
+ >
+ )
}
export default App
diff --git a/src/features/song-select/SongSelectScreen.tsx b/src/features/song-select/SongSelectScreen.tsx
index 4d7cf26..7232ce3 100644
--- a/src/features/song-select/SongSelectScreen.tsx
+++ b/src/features/song-select/SongSelectScreen.tsx
@@ -1,5 +1,6 @@
-import { useEffect, useMemo, useState } from 'react'
+import { useMemo, useState } from 'react'
import type { CSSProperties } from 'react'
+import { useGameInput } from '../../input/useGameInput'
import { ARHud } from './ARHud'
import { SONGS } from './song-data'
import { SongCarousel } from './SongCarousel'
@@ -14,21 +15,21 @@ function wrapIndex(index: number, length: number): number {
export function SongSelectScreen() {
const [currentIndex, setCurrentIndex] = useState(0)
const activeSong = SONGS[currentIndex]
+ const handleConfirm = () => {}
- useEffect(() => {
- const onKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'ArrowLeft') {
+ useGameInput((action) => {
+ if (action === 'LEFT') {
setCurrentIndex((prev) => wrapIndex(prev - 1, SONGS.length))
- }
-
- if (event.key === 'ArrowRight') {
- setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
- }
}
- window.addEventListener('keydown', onKeyDown)
- return () => window.removeEventListener('keydown', onKeyDown)
- }, [])
+ if (action === 'RIGHT') {
+ setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
+ }
+
+ if (action === 'BUTTON_A') {
+ handleConfirm()
+ }
+ })
const cssVars = useMemo(
() =>
@@ -60,7 +61,7 @@ export function SongSelectScreen() {
{}}
+ onConfirm={handleConfirm}
/>
diff --git a/src/input/actions.ts b/src/input/actions.ts
new file mode 100644
index 0000000..7be4e53
--- /dev/null
+++ b/src/input/actions.ts
@@ -0,0 +1 @@
+export type GameAction = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' | 'BUTTON_A' | 'BUTTON_B'
diff --git a/src/input/inputBus.ts b/src/input/inputBus.ts
new file mode 100644
index 0000000..c0f374c
--- /dev/null
+++ b/src/input/inputBus.ts
@@ -0,0 +1,16 @@
+import type { GameAction } from './actions'
+
+type GameActionListener = (action: GameAction) => void
+
+const listeners = new Set()
+
+export function emit(action: GameAction): void {
+ listeners.forEach((listener) => listener(action))
+}
+
+export function subscribe(listener: GameActionListener): () => void {
+ listeners.add(listener)
+ return () => {
+ listeners.delete(listener)
+ }
+}
diff --git a/src/input/useGameInput.ts b/src/input/useGameInput.ts
new file mode 100644
index 0000000..93cd794
--- /dev/null
+++ b/src/input/useGameInput.ts
@@ -0,0 +1,7 @@
+import { useEffect } from 'react'
+import type { GameAction } from './actions'
+import { subscribe } from './inputBus'
+
+export function useGameInput(onAction: (action: GameAction) => void): void {
+ useEffect(() => subscribe(onAction), [onAction])
+}
diff --git a/src/input/useKeyboardInput.ts b/src/input/useKeyboardInput.ts
new file mode 100644
index 0000000..a892aaa
--- /dev/null
+++ b/src/input/useKeyboardInput.ts
@@ -0,0 +1,25 @@
+import { useEffect } from 'react'
+import type { GameAction } from './actions'
+import { emit } from './inputBus'
+
+const KEY_TO_ACTION: Partial> = {
+ ArrowLeft: 'LEFT',
+ ArrowRight: 'RIGHT',
+ ArrowUp: 'UP',
+ ArrowDown: 'DOWN',
+ Enter: 'BUTTON_A',
+ Escape: 'BUTTON_B',
+}
+
+export function useKeyboardInput(): void {
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ const action = KEY_TO_ACTION[event.key]
+ if (!action) return
+ emit(action)
+ }
+
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ }, [])
+}
diff --git a/src/input/usePoseInput.ts b/src/input/usePoseInput.ts
new file mode 100644
index 0000000..89c4e6f
--- /dev/null
+++ b/src/input/usePoseInput.ts
@@ -0,0 +1,21 @@
+import { useEffect } from 'react'
+import { emit } from './inputBus'
+
+export function usePoseInput(): void {
+ useEffect(() => {
+ const emitAction = (action: Parameters[0]) => {
+ emit(action)
+ }
+
+ // MediaPipe integration point:
+ // Convert detected pose/gesture events into GameAction values and call `emitAction(action)`.
+ // Example mapping idea: swipe-left -> 'LEFT', swipe-right -> 'RIGHT',
+ // hand-raise -> 'BUTTON_A', cancel gesture -> 'BUTTON_B'.
+ // Keep all MediaPipe-specific setup/teardown in this hook so UI components remain input-agnostic.
+ void emitAction
+
+ return () => {
+ // Cleanup MediaPipe resources/listeners here when integration is added.
+ }
+ }, [])
+}