create-airjam 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/index.js +11 -3
  2. package/package.json +6 -3
  3. package/templates/pong/.env.example +11 -0
  4. package/templates/pong/.env.local +10 -0
  5. package/templates/pong/AI_INSTRUCTIONS.md +44 -0
  6. package/templates/pong/README.md +111 -0
  7. package/templates/pong/airjam-docs/getting-started/architecture/page.md +165 -0
  8. package/templates/pong/airjam-docs/getting-started/game-ideas/page.md +114 -0
  9. package/templates/pong/airjam-docs/getting-started/introduction/page.md +122 -0
  10. package/templates/pong/airjam-docs/how-it-works/host-system/page.md +241 -0
  11. package/templates/pong/airjam-docs/sdk/hooks/page.md +403 -0
  12. package/templates/pong/airjam-docs/sdk/input-system/page.md +336 -0
  13. package/templates/pong/airjam-docs/sdk/networked-state/page.md +575 -0
  14. package/templates/pong/dist/assets/index-B9l0NKly.js +269 -0
  15. package/templates/pong/dist/assets/index-CHKqdIQG.css +1 -0
  16. package/templates/pong/dist/index.html +14 -0
  17. package/templates/pong/eslint.config.js +33 -0
  18. package/templates/pong/index.html +6 -1
  19. package/templates/pong/node_modules/.bin/air-jam-server +17 -0
  20. package/templates/pong/node_modules/.bin/eslint +17 -0
  21. package/templates/pong/node_modules/.bin/eslint-config-prettier +17 -0
  22. package/templates/pong/node_modules/.bin/jiti +17 -0
  23. package/templates/pong/node_modules/.bin/tsc +17 -0
  24. package/templates/pong/node_modules/.bin/tsserver +17 -0
  25. package/templates/pong/node_modules/.bin/tsx +17 -0
  26. package/templates/pong/node_modules/.bin/vite +17 -0
  27. package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js +66143 -0
  28. package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js.map +7 -0
  29. package/templates/pong/node_modules/.vite/deps/_metadata.json +73 -0
  30. package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js +292 -0
  31. package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js.map +7 -0
  32. package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js +38 -0
  33. package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  34. package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js +280 -0
  35. package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js.map +7 -0
  36. package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js +13810 -0
  37. package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js.map +7 -0
  38. package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js +1004 -0
  39. package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js.map +7 -0
  40. package/templates/pong/node_modules/.vite/deps/package.json +3 -0
  41. package/templates/pong/node_modules/.vite/deps/react-dom.js +6 -0
  42. package/templates/pong/node_modules/.vite/deps/react-dom.js.map +7 -0
  43. package/templates/pong/node_modules/.vite/deps/react-dom_client.js +20217 -0
  44. package/templates/pong/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  45. package/templates/pong/node_modules/.vite/deps/react-router-dom.js +13900 -0
  46. package/templates/pong/node_modules/.vite/deps/react-router-dom.js.map +7 -0
  47. package/templates/pong/node_modules/.vite/deps/react.js +5 -0
  48. package/templates/pong/node_modules/.vite/deps/react.js.map +7 -0
  49. package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  50. package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  51. package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  52. package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  53. package/templates/pong/node_modules/.vite/deps/zod.js +476 -0
  54. package/templates/pong/node_modules/.vite/deps/zod.js.map +7 -0
  55. package/templates/pong/package.json +12 -1
  56. package/templates/pong/src/App.tsx +2 -2
  57. package/templates/pong/src/controller-view.tsx +143 -0
  58. package/templates/pong/src/host-view.tsx +401 -0
  59. package/templates/pong/src/main.tsx +2 -1
  60. package/templates/pong/src/store.ts +80 -0
  61. package/templates/pong/tsconfig.json +3 -2
  62. package/templates/pong/vite.config.ts +3 -0
  63. package/templates/pong/src/ControllerView.tsx +0 -64
  64. package/templates/pong/src/HostView.tsx +0 -148
@@ -0,0 +1,476 @@
1
+ import {
2
+ $brand,
3
+ $input,
4
+ $output,
5
+ NEVER,
6
+ TimePrecision,
7
+ ZodAny,
8
+ ZodArray,
9
+ ZodBase64,
10
+ ZodBase64URL,
11
+ ZodBigInt,
12
+ ZodBigIntFormat,
13
+ ZodBoolean,
14
+ ZodCIDRv4,
15
+ ZodCIDRv6,
16
+ ZodCUID,
17
+ ZodCUID2,
18
+ ZodCatch,
19
+ ZodCodec,
20
+ ZodCustom,
21
+ ZodCustomStringFormat,
22
+ ZodDate,
23
+ ZodDefault,
24
+ ZodDiscriminatedUnion,
25
+ ZodE164,
26
+ ZodEmail,
27
+ ZodEmoji,
28
+ ZodEnum,
29
+ ZodError,
30
+ ZodFile,
31
+ ZodFirstPartyTypeKind,
32
+ ZodFunction,
33
+ ZodGUID,
34
+ ZodIPv4,
35
+ ZodIPv6,
36
+ ZodISODate,
37
+ ZodISODateTime,
38
+ ZodISODuration,
39
+ ZodISOTime,
40
+ ZodIntersection,
41
+ ZodIssueCode,
42
+ ZodJWT,
43
+ ZodKSUID,
44
+ ZodLazy,
45
+ ZodLiteral,
46
+ ZodMAC,
47
+ ZodMap,
48
+ ZodNaN,
49
+ ZodNanoID,
50
+ ZodNever,
51
+ ZodNonOptional,
52
+ ZodNull,
53
+ ZodNullable,
54
+ ZodNumber,
55
+ ZodNumberFormat,
56
+ ZodObject,
57
+ ZodOptional,
58
+ ZodPipe,
59
+ ZodPrefault,
60
+ ZodPromise,
61
+ ZodReadonly,
62
+ ZodRealError,
63
+ ZodRecord,
64
+ ZodSet,
65
+ ZodString,
66
+ ZodStringFormat,
67
+ ZodSuccess,
68
+ ZodSymbol,
69
+ ZodTemplateLiteral,
70
+ ZodTransform,
71
+ ZodTuple,
72
+ ZodType,
73
+ ZodULID,
74
+ ZodURL,
75
+ ZodUUID,
76
+ ZodUndefined,
77
+ ZodUnion,
78
+ ZodUnknown,
79
+ ZodVoid,
80
+ ZodXID,
81
+ ZodXor,
82
+ _ZodString,
83
+ _catch,
84
+ _default,
85
+ _endsWith,
86
+ _enum,
87
+ _function,
88
+ _gt,
89
+ _gte,
90
+ _includes,
91
+ _instanceof,
92
+ _length,
93
+ _lowercase,
94
+ _lt,
95
+ _lte,
96
+ _maxLength,
97
+ _maxSize,
98
+ _mime,
99
+ _minLength,
100
+ _minSize,
101
+ _multipleOf,
102
+ _negative,
103
+ _nonnegative,
104
+ _nonpositive,
105
+ _normalize,
106
+ _null,
107
+ _overwrite,
108
+ _positive,
109
+ _property,
110
+ _regex,
111
+ _size,
112
+ _slugify,
113
+ _startsWith,
114
+ _toLowerCase,
115
+ _toUpperCase,
116
+ _trim,
117
+ _undefined,
118
+ _uppercase,
119
+ _void,
120
+ any,
121
+ array,
122
+ base64,
123
+ base64url,
124
+ bigint,
125
+ boolean,
126
+ check,
127
+ cidrv4,
128
+ cidrv6,
129
+ clone,
130
+ codec,
131
+ coerce_exports,
132
+ config,
133
+ core_exports,
134
+ cuid,
135
+ cuid2,
136
+ custom,
137
+ date,
138
+ decode,
139
+ decodeAsync,
140
+ describe,
141
+ discriminatedUnion,
142
+ e164,
143
+ email,
144
+ emoji,
145
+ encode,
146
+ encodeAsync,
147
+ external_exports,
148
+ file,
149
+ flattenError,
150
+ float32,
151
+ float64,
152
+ formatError,
153
+ fromJSONSchema,
154
+ getErrorMap,
155
+ globalRegistry,
156
+ guid,
157
+ hash,
158
+ hex,
159
+ hostname,
160
+ httpUrl,
161
+ int,
162
+ int32,
163
+ int64,
164
+ intersection,
165
+ ipv4,
166
+ ipv6,
167
+ iso_exports,
168
+ json,
169
+ jwt,
170
+ keyof,
171
+ ksuid,
172
+ lazy,
173
+ literal,
174
+ locales_exports,
175
+ looseObject,
176
+ looseRecord,
177
+ mac,
178
+ map,
179
+ meta,
180
+ nan,
181
+ nanoid,
182
+ nativeEnum,
183
+ never,
184
+ nonoptional,
185
+ nullable,
186
+ nullish,
187
+ number,
188
+ object,
189
+ optional,
190
+ parse,
191
+ parseAsync,
192
+ partialRecord,
193
+ pipe,
194
+ prefault,
195
+ preprocess,
196
+ prettifyError,
197
+ promise,
198
+ readonly,
199
+ record,
200
+ refine,
201
+ regexes_exports,
202
+ registry,
203
+ safeDecode,
204
+ safeDecodeAsync,
205
+ safeEncode,
206
+ safeEncodeAsync,
207
+ safeParse,
208
+ safeParseAsync,
209
+ set,
210
+ setErrorMap,
211
+ strictObject,
212
+ string,
213
+ stringFormat,
214
+ stringbool,
215
+ success,
216
+ superRefine,
217
+ symbol,
218
+ templateLiteral,
219
+ toJSONSchema,
220
+ transform,
221
+ treeifyError,
222
+ tuple,
223
+ uint32,
224
+ uint64,
225
+ ulid,
226
+ union,
227
+ unknown,
228
+ url,
229
+ util_exports,
230
+ uuid,
231
+ uuidv4,
232
+ uuidv6,
233
+ uuidv7,
234
+ xid,
235
+ xor,
236
+ zod_default
237
+ } from "./chunk-TYOCAO5S.js";
238
+ import "./chunk-DC5AMYBS.js";
239
+ export {
240
+ $brand,
241
+ $input,
242
+ $output,
243
+ NEVER,
244
+ TimePrecision,
245
+ ZodAny,
246
+ ZodArray,
247
+ ZodBase64,
248
+ ZodBase64URL,
249
+ ZodBigInt,
250
+ ZodBigIntFormat,
251
+ ZodBoolean,
252
+ ZodCIDRv4,
253
+ ZodCIDRv6,
254
+ ZodCUID,
255
+ ZodCUID2,
256
+ ZodCatch,
257
+ ZodCodec,
258
+ ZodCustom,
259
+ ZodCustomStringFormat,
260
+ ZodDate,
261
+ ZodDefault,
262
+ ZodDiscriminatedUnion,
263
+ ZodE164,
264
+ ZodEmail,
265
+ ZodEmoji,
266
+ ZodEnum,
267
+ ZodError,
268
+ ZodFile,
269
+ ZodFirstPartyTypeKind,
270
+ ZodFunction,
271
+ ZodGUID,
272
+ ZodIPv4,
273
+ ZodIPv6,
274
+ ZodISODate,
275
+ ZodISODateTime,
276
+ ZodISODuration,
277
+ ZodISOTime,
278
+ ZodIntersection,
279
+ ZodIssueCode,
280
+ ZodJWT,
281
+ ZodKSUID,
282
+ ZodLazy,
283
+ ZodLiteral,
284
+ ZodMAC,
285
+ ZodMap,
286
+ ZodNaN,
287
+ ZodNanoID,
288
+ ZodNever,
289
+ ZodNonOptional,
290
+ ZodNull,
291
+ ZodNullable,
292
+ ZodNumber,
293
+ ZodNumberFormat,
294
+ ZodObject,
295
+ ZodOptional,
296
+ ZodPipe,
297
+ ZodPrefault,
298
+ ZodPromise,
299
+ ZodReadonly,
300
+ ZodRealError,
301
+ ZodRecord,
302
+ ZodSet,
303
+ ZodString,
304
+ ZodStringFormat,
305
+ ZodSuccess,
306
+ ZodSymbol,
307
+ ZodTemplateLiteral,
308
+ ZodTransform,
309
+ ZodTuple,
310
+ ZodType,
311
+ ZodULID,
312
+ ZodURL,
313
+ ZodUUID,
314
+ ZodUndefined,
315
+ ZodUnion,
316
+ ZodUnknown,
317
+ ZodVoid,
318
+ ZodXID,
319
+ ZodXor,
320
+ _ZodString,
321
+ _default,
322
+ _function,
323
+ any,
324
+ array,
325
+ base64,
326
+ base64url,
327
+ bigint,
328
+ boolean,
329
+ _catch as catch,
330
+ check,
331
+ cidrv4,
332
+ cidrv6,
333
+ clone,
334
+ codec,
335
+ coerce_exports as coerce,
336
+ config,
337
+ core_exports as core,
338
+ cuid,
339
+ cuid2,
340
+ custom,
341
+ date,
342
+ decode,
343
+ decodeAsync,
344
+ zod_default as default,
345
+ describe,
346
+ discriminatedUnion,
347
+ e164,
348
+ email,
349
+ emoji,
350
+ encode,
351
+ encodeAsync,
352
+ _endsWith as endsWith,
353
+ _enum as enum,
354
+ file,
355
+ flattenError,
356
+ float32,
357
+ float64,
358
+ formatError,
359
+ fromJSONSchema,
360
+ _function as function,
361
+ getErrorMap,
362
+ globalRegistry,
363
+ _gt as gt,
364
+ _gte as gte,
365
+ guid,
366
+ hash,
367
+ hex,
368
+ hostname,
369
+ httpUrl,
370
+ _includes as includes,
371
+ _instanceof as instanceof,
372
+ int,
373
+ int32,
374
+ int64,
375
+ intersection,
376
+ ipv4,
377
+ ipv6,
378
+ iso_exports as iso,
379
+ json,
380
+ jwt,
381
+ keyof,
382
+ ksuid,
383
+ lazy,
384
+ _length as length,
385
+ literal,
386
+ locales_exports as locales,
387
+ looseObject,
388
+ looseRecord,
389
+ _lowercase as lowercase,
390
+ _lt as lt,
391
+ _lte as lte,
392
+ mac,
393
+ map,
394
+ _maxLength as maxLength,
395
+ _maxSize as maxSize,
396
+ meta,
397
+ _mime as mime,
398
+ _minLength as minLength,
399
+ _minSize as minSize,
400
+ _multipleOf as multipleOf,
401
+ nan,
402
+ nanoid,
403
+ nativeEnum,
404
+ _negative as negative,
405
+ never,
406
+ _nonnegative as nonnegative,
407
+ nonoptional,
408
+ _nonpositive as nonpositive,
409
+ _normalize as normalize,
410
+ _null as null,
411
+ nullable,
412
+ nullish,
413
+ number,
414
+ object,
415
+ optional,
416
+ _overwrite as overwrite,
417
+ parse,
418
+ parseAsync,
419
+ partialRecord,
420
+ pipe,
421
+ _positive as positive,
422
+ prefault,
423
+ preprocess,
424
+ prettifyError,
425
+ promise,
426
+ _property as property,
427
+ readonly,
428
+ record,
429
+ refine,
430
+ _regex as regex,
431
+ regexes_exports as regexes,
432
+ registry,
433
+ safeDecode,
434
+ safeDecodeAsync,
435
+ safeEncode,
436
+ safeEncodeAsync,
437
+ safeParse,
438
+ safeParseAsync,
439
+ set,
440
+ setErrorMap,
441
+ _size as size,
442
+ _slugify as slugify,
443
+ _startsWith as startsWith,
444
+ strictObject,
445
+ string,
446
+ stringFormat,
447
+ stringbool,
448
+ success,
449
+ superRefine,
450
+ symbol,
451
+ templateLiteral,
452
+ toJSONSchema,
453
+ _toLowerCase as toLowerCase,
454
+ _toUpperCase as toUpperCase,
455
+ transform,
456
+ treeifyError,
457
+ _trim as trim,
458
+ tuple,
459
+ uint32,
460
+ uint64,
461
+ ulid,
462
+ _undefined as undefined,
463
+ union,
464
+ unknown,
465
+ _uppercase as uppercase,
466
+ url,
467
+ util_exports as util,
468
+ uuid,
469
+ uuidv4,
470
+ uuidv6,
471
+ uuidv7,
472
+ _void as void,
473
+ xid,
474
+ xor,
475
+ external_exports as z
476
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -5,8 +5,11 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
+ "dev:server": "air-jam-server",
8
9
  "build": "tsc && vite build",
9
- "preview": "vite preview"
10
+ "preview": "vite preview",
11
+ "typecheck": "tsc --noEmit",
12
+ "install:standalone": "pnpm install --ignore-workspace"
10
13
  },
11
14
  "dependencies": {
12
15
  "@air-jam/sdk": "^0.1.0",
@@ -16,12 +19,20 @@
16
19
  "zod": "^4.2.1"
17
20
  },
18
21
  "devDependencies": {
22
+ "@air-jam/server": "^0.1.0",
23
+ "@eslint/js": "^9.39.1",
19
24
  "@tailwindcss/vite": "^4.0.0",
20
25
  "@types/react": "^19.0.0",
21
26
  "@types/react-dom": "^19.0.0",
22
27
  "@vitejs/plugin-react": "^4.3.0",
28
+ "eslint": "^9.39.1",
29
+ "eslint-config-prettier": "^10.1.8",
30
+ "eslint-plugin-react-hooks": "^7.0.1",
31
+ "eslint-plugin-react-refresh": "^0.4.24",
32
+ "globals": "^16.5.0",
23
33
  "tailwindcss": "^4.0.0",
24
34
  "typescript": "~5.6.0",
35
+ "typescript-eslint": "^8.46.4",
25
36
  "vite": "^6.0.0"
26
37
  }
27
38
  }
@@ -1,7 +1,7 @@
1
1
  import { AirJamProvider } from "@air-jam/sdk";
2
2
  import { Route, Routes } from "react-router-dom";
3
- import { ControllerView } from "./ControllerView";
4
- import { HostView } from "./HostView";
3
+ import { ControllerView } from "./controller-view";
4
+ import { HostView } from "./host-view";
5
5
  import { gameInputSchema } from "./types";
6
6
 
7
7
  export function App() {
@@ -0,0 +1,143 @@
1
+ import {
2
+ AirJamDebug,
3
+ ControllerShell,
4
+ useAirJamController,
5
+ } from "@air-jam/sdk";
6
+ import { useEffect, useRef } from "react";
7
+ import { usePongStore, type PongState } from "./store";
8
+
9
+ const TEAM1_COLOR = "#f97316"; // Solaris (Orange)
10
+ const TEAM2_COLOR = "#38bdf8"; // Nebulon (Blue)
11
+
12
+ export function ControllerView() {
13
+ const controller = useAirJamController();
14
+ const directionRef = useRef(0);
15
+
16
+ // Use the networked store
17
+ const teamAssignments = usePongStore(
18
+ (state: PongState) => state.teamAssignments,
19
+ );
20
+ const actions = usePongStore((state: PongState) => state.actions);
21
+
22
+ const myAssignment = controller.controllerId
23
+ ? teamAssignments[controller.controllerId]
24
+ : null;
25
+ const myTeam = myAssignment?.team ?? null;
26
+
27
+ // Send input loop (only when playing)
28
+ useEffect(() => {
29
+ if (controller.connectionStatus !== "connected") return;
30
+ if (controller.gameState !== "playing") return;
31
+
32
+ let animationId: number;
33
+ const loop = () => {
34
+ controller.sendInput({
35
+ direction: directionRef.current,
36
+ action: false,
37
+ });
38
+ console.log("directionRef.current", directionRef.current);
39
+ animationId = requestAnimationFrame(loop);
40
+ };
41
+ loop();
42
+ return () => cancelAnimationFrame(animationId);
43
+ }, [controller.connectionStatus, controller.gameState, controller]);
44
+
45
+ return (
46
+ <div className="relative bg-black">
47
+ <ControllerShell
48
+ connectionStatus={controller.connectionStatus}
49
+ roomId={controller.roomId}
50
+ requiredOrientation="portrait"
51
+ gameState={controller.gameState}
52
+ onTogglePlayPause={() => controller.sendSystemCommand("toggle_pause")}
53
+ onReconnect={() => controller.reconnect()}
54
+ onRefresh={() => window.location.reload()}
55
+ >
56
+ {/* Debug State Component */}
57
+ <div className="absolute top-5 right-5 z-50">
58
+ <AirJamDebug
59
+ state={usePongStore((state: PongState) => state)}
60
+ title="Pong Game State"
61
+ />
62
+ </div>
63
+ {controller.gameState === "paused" ? (
64
+ // Team selection UI (shown when paused)
65
+ <div className="flex h-full w-full flex-col gap-2 p-2">
66
+ {/* Up button - Select Team 1 */}
67
+ <button
68
+ type="button"
69
+ className={`flex-1 touch-none rounded-xl text-4xl font-bold text-white shadow-lg select-none hover:opacity-90 active:scale-95 ${
70
+ myTeam === "team1"
71
+ ? "ring-4 ring-white ring-offset-2 ring-offset-zinc-900"
72
+ : "opacity-70"
73
+ }`}
74
+ style={{
75
+ backgroundColor: myTeam === "team1" ? TEAM1_COLOR : "#3f3f46",
76
+ willChange: "transform",
77
+ transition: "none",
78
+ }}
79
+ onTouchStart={() => actions.joinTeam("team1")}
80
+ onMouseDown={() => actions.joinTeam("team1")}
81
+ >
82
+ SOLARIS
83
+ </button>
84
+
85
+ {/* Down button - Select Team 2 */}
86
+ <button
87
+ type="button"
88
+ className={`flex-1 touch-none rounded-xl text-4xl font-bold text-white shadow-lg select-none hover:opacity-90 active:scale-95 ${
89
+ myTeam === "team2"
90
+ ? "ring-4 ring-white ring-offset-2 ring-offset-zinc-900"
91
+ : "opacity-70"
92
+ }`}
93
+ style={{
94
+ backgroundColor: myTeam === "team2" ? TEAM2_COLOR : "#3f3f46",
95
+ willChange: "transform",
96
+ transition: "none",
97
+ }}
98
+ onTouchStart={() => actions.joinTeam("team2")}
99
+ onMouseDown={() => actions.joinTeam("team2")}
100
+ >
101
+ NEBULON
102
+ </button>
103
+ </div>
104
+ ) : (
105
+ // Game control buttons
106
+ <div className="flex h-full w-full flex-col gap-2 p-2">
107
+ {/* Up button */}
108
+ <button
109
+ type="button"
110
+ className="flex-1 touch-none rounded-xl bg-zinc-800 text-4xl font-bold text-white shadow-lg select-none hover:bg-zinc-700 active:scale-95 active:bg-zinc-700"
111
+ style={{
112
+ willChange: "transform",
113
+ transition: "none",
114
+ }}
115
+ onTouchStart={() => (directionRef.current = -1)}
116
+ onTouchEnd={() => (directionRef.current = 0)}
117
+ onMouseDown={() => (directionRef.current = -1)}
118
+ onMouseUp={() => (directionRef.current = 0)}
119
+ >
120
+ ▲ UP
121
+ </button>
122
+
123
+ {/* Down button */}
124
+ <button
125
+ type="button"
126
+ className="flex-1 touch-none rounded-xl bg-zinc-800 text-4xl font-bold text-white shadow-lg select-none hover:bg-zinc-700 active:scale-95 active:bg-zinc-700"
127
+ style={{
128
+ willChange: "transform",
129
+ transition: "none",
130
+ }}
131
+ onTouchStart={() => (directionRef.current = 1)}
132
+ onTouchEnd={() => (directionRef.current = 0)}
133
+ onMouseDown={() => (directionRef.current = 1)}
134
+ onMouseUp={() => (directionRef.current = 0)}
135
+ >
136
+ ▼ DOWN
137
+ </button>
138
+ </div>
139
+ )}
140
+ </ControllerShell>
141
+ </div>
142
+ );
143
+ }