koin.js 1.0.8 → 1.0.10

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.
package/dist/index.js CHANGED
@@ -2084,92 +2084,175 @@ function useMobile() {
2084
2084
 
2085
2085
  // src/components/VirtualController/layouts.ts
2086
2086
  var BUTTON_SMALL = 44;
2087
- var BUTTON_MEDIUM = 52;
2088
- var BUTTON_LARGE = 60;
2089
- var NES_LAYOUT = {
2090
- console: "NES",
2087
+ var BUTTON_MEDIUM = 56;
2088
+ var BUTTON_LARGE = 68;
2089
+ var START_SELECT_Y = 8;
2090
+ var SELECT_X = 38;
2091
+ var START_X = 62;
2092
+ var DPAD_BUTTONS = [
2093
+ { type: "up", label: "\u2191", x: 14, y: 55, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2094
+ { type: "down", label: "\u2193", x: 14, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2095
+ { type: "left", label: "\u2190", x: 6, y: 62.5, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2096
+ { type: "right", label: "\u2192", x: 22, y: 62.5, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true }
2097
+ ];
2098
+ var TWO_BUTTON_LAYOUT = {
2099
+ console: "2BUTTON",
2091
2100
  buttons: [
2092
- // D-pad - y:55-70 to stay above control bar
2093
- { type: "up", label: "\u2191", x: 12, y: 55, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2094
- { type: "down", label: "\u2193", x: 12, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2095
- { type: "left", label: "\u2190", x: 5, y: 62, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2096
- { type: "right", label: "\u2192", x: 19, y: 62, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2097
- // Action buttons
2098
- { type: "b", label: "B", x: 80, y: 64, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2099
- { type: "a", label: "A", x: 92, y: 56, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2100
- // System buttons - top center
2101
- { type: "select", label: "SEL", x: 38, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2102
- { type: "start", label: "START", x: 62, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true }
2101
+ ...DPAD_BUTTONS,
2102
+ { type: "b", label: "B", x: 78, y: 66, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2103
+ { type: "a", label: "A", x: 90, y: 56, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2104
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2105
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2103
2106
  ]
2104
2107
  };
2105
2108
  var SNES_LAYOUT = {
2106
2109
  console: "SNES",
2107
2110
  buttons: [
2108
- { type: "up", label: "\u2191", x: 12, y: 45, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2109
- { type: "down", label: "\u2193", x: 12, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2110
- { type: "left", label: "\u2190", x: 5, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2111
- { type: "right", label: "\u2192", x: 19, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2112
- { type: "y", label: "Y", x: 76, y: 45, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2113
- { type: "x", label: "X", x: 88, y: 37, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2114
- { type: "b", label: "B", x: 88, y: 53, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2115
- { type: "a", label: "A", x: 96, y: 45, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2116
- { type: "l", label: "L", x: 8, y: 18, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2117
- { type: "r", label: "R", x: 92, y: 18, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2118
- { type: "select", label: "SEL", x: 38, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2119
- { type: "start", label: "START", x: 62, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true }
2111
+ ...DPAD_BUTTONS,
2112
+ // Diamond: Y-X-B-A (left-top-bottom-right)
2113
+ { type: "y", label: "Y", x: 74, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2114
+ { type: "x", label: "X", x: 84, y: 46, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2115
+ { type: "b", label: "B", x: 84, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2116
+ { type: "a", label: "A", x: 94, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2117
+ // Shoulders
2118
+ { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2119
+ { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2120
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2121
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2120
2122
  ]
2121
2123
  };
2122
- var GB_LAYOUT = {
2123
- console: "GB",
2124
+ var GBA_LAYOUT = {
2125
+ console: "GBA",
2124
2126
  buttons: [
2125
- { type: "up", label: "\u2191", x: 12, y: 55, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2126
- { type: "down", label: "\u2193", x: 12, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2127
- { type: "left", label: "\u2190", x: 5, y: 62, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2128
- { type: "right", label: "\u2192", x: 19, y: 62, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2129
- { type: "b", label: "B", x: 80, y: 64, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2130
- { type: "a", label: "A", x: 92, y: 56, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2131
- { type: "select", label: "SEL", x: 38, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2132
- { type: "start", label: "START", x: 62, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true }
2127
+ ...DPAD_BUTTONS,
2128
+ { type: "b", label: "B", x: 80, y: 62, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2129
+ { type: "a", label: "A", x: 92, y: 52, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2130
+ { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2131
+ { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2132
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2133
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2133
2134
  ]
2134
2135
  };
2135
- var GBA_LAYOUT = {
2136
- console: "GBA",
2136
+ var SIX_BUTTON_LAYOUT = {
2137
+ console: "6BUTTON",
2137
2138
  buttons: [
2138
- { type: "up", label: "\u2191", x: 12, y: 45, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2139
- { type: "down", label: "\u2193", x: 12, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2140
- { type: "left", label: "\u2190", x: 5, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2141
- { type: "right", label: "\u2192", x: 19, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2142
- { type: "b", label: "B", x: 82, y: 55, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2143
- { type: "a", label: "A", x: 94, y: 45, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2144
- { type: "l", label: "L", x: 8, y: 18, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2145
- { type: "r", label: "R", x: 92, y: 18, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2146
- { type: "select", label: "SEL", x: 38, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true },
2147
- { type: "start", label: "START", x: 62, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true }
2139
+ ...DPAD_BUTTONS,
2140
+ // Top row (X, Y, Z) mapped to L, X, R
2141
+ { type: "l", label: "X", x: 74, y: 48, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2142
+ { type: "x", label: "Y", x: 84, y: 44, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2143
+ { type: "r", label: "Z", x: 94, y: 40, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2144
+ // Bottom row (A, B, C) mapped to Y, B, A
2145
+ { type: "y", label: "A", x: 72, y: 64, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2146
+ { type: "b", label: "B", x: 82, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2147
+ { type: "a", label: "C", x: 92, y: 56, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2148
+ { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2148
2149
  ]
2149
2150
  };
2150
- var GENESIS_LAYOUT = {
2151
- console: "GENESIS",
2151
+ var SATURN_LAYOUT = {
2152
+ console: "SATURN",
2152
2153
  buttons: [
2153
- { type: "up", label: "\u2191", x: 12, y: 45, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2154
- { type: "down", label: "\u2193", x: 12, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2155
- { type: "left", label: "\u2190", x: 5, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2156
- { type: "right", label: "\u2192", x: 19, y: 52, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2157
- { type: "a", label: "A", x: 74, y: 56, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2158
- { type: "b", label: "B", x: 85, y: 48, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2159
- { type: "c", label: "C", x: 96, y: 40, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2160
- { type: "start", label: "START", x: 50, y: 8, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true }
2154
+ ...DPAD_BUTTONS,
2155
+ // Face buttons (same as 6-button but uses standard buttons)
2156
+ // X/Y/Z on top
2157
+ { type: "x", label: "X", x: 74, y: 48, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2158
+ { type: "y", label: "Y", x: 84, y: 44, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2159
+ { type: "l", label: "Z", x: 94, y: 40, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2160
+ // A/B/C on bottom
2161
+ { type: "b", label: "A", x: 72, y: 64, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2162
+ { type: "a", label: "B", x: 82, y: 60, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2163
+ { type: "r", label: "C", x: 92, y: 56, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2164
+ // Triggers (L2/R2 for Saturn L/R)
2165
+ { type: "l2", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2166
+ { type: "r2", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2167
+ { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2168
+ ]
2169
+ };
2170
+ var NEOGEO_LAYOUT = {
2171
+ console: "NEOGEO",
2172
+ buttons: [
2173
+ ...DPAD_BUTTONS,
2174
+ // Curved A-B-C-D layout (maps to b-a-y-x for RetroPad)
2175
+ { type: "b", label: "A", x: 68, y: 68, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2176
+ { type: "a", label: "B", x: 78, y: 62, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2177
+ { type: "y", label: "C", x: 88, y: 56, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2178
+ { type: "x", label: "D", x: 96, y: 48, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2179
+ { type: "select", label: "COIN", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2180
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2181
+ ]
2182
+ };
2183
+ var PSX_LAYOUT = {
2184
+ console: "PSX",
2185
+ buttons: [
2186
+ ...DPAD_BUTTONS,
2187
+ // PS symbols: △◯✕□ → y, b, a, x (triangle-circle-cross-square)
2188
+ { type: "y", label: "\u25B3", x: 84, y: 46, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2189
+ { type: "b", label: "\u25CB", x: 94, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2190
+ { type: "a", label: "\u2715", x: 84, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2191
+ { type: "x", label: "\u25A1", x: 74, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2192
+ // Shoulders
2193
+ { type: "l", label: "L1", x: 8, y: 25, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2194
+ { type: "r", label: "R1", x: 92, y: 25, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2195
+ { type: "l2", label: "L2", x: 8, y: 15, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "rect" },
2196
+ { type: "r2", label: "R2", x: 92, y: 15, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "rect" },
2197
+ { type: "select", label: "SELECT", x: SELECT_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" },
2198
+ { type: "start", label: "START", x: START_X, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2199
+ ]
2200
+ };
2201
+ var N64_LAYOUT = {
2202
+ console: "N64",
2203
+ buttons: [
2204
+ ...DPAD_BUTTONS,
2205
+ // A/B
2206
+ { type: "a", label: "A", x: 80, y: 68, size: BUTTON_LARGE, showInPortrait: true, showInLandscape: true },
2207
+ { type: "b", label: "B", x: 72, y: 76, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2208
+ // C-buttons (use x/y as C-left/C-up, l2/r2 as C-down/C-right)
2209
+ { type: "y", label: "C\u2191", x: 92, y: 50, size: 36, showInPortrait: true, showInLandscape: true },
2210
+ { type: "x", label: "C\u2190", x: 86, y: 56, size: 36, showInPortrait: true, showInLandscape: true },
2211
+ { type: "l2", label: "C\u2193", x: 92, y: 62, size: 36, showInPortrait: true, showInLandscape: true },
2212
+ { type: "r2", label: "C\u2192", x: 98, y: 56, size: 36, showInPortrait: true, showInLandscape: true },
2213
+ // Shoulders
2214
+ { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2215
+ { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2216
+ // Z trigger (use select as workaround since it's a unique button)
2217
+ { type: "select", label: "Z", x: 8, y: 35, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2218
+ { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2219
+ ]
2220
+ };
2221
+ var DREAMCAST_LAYOUT = {
2222
+ console: "DREAMCAST",
2223
+ buttons: [
2224
+ ...DPAD_BUTTONS,
2225
+ // Standard diamond
2226
+ { type: "y", label: "Y", x: 74, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2227
+ { type: "x", label: "X", x: 84, y: 46, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2228
+ { type: "b", label: "B", x: 84, y: 70, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2229
+ { type: "a", label: "A", x: 94, y: 58, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true },
2230
+ // Triggers
2231
+ { type: "l", label: "L", x: 8, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2232
+ { type: "r", label: "R", x: 92, y: 20, size: BUTTON_MEDIUM, showInPortrait: true, showInLandscape: true, shape: "rect" },
2233
+ { type: "start", label: "START", x: 50, y: START_SELECT_Y, size: BUTTON_SMALL, showInPortrait: true, showInLandscape: true, shape: "pill" }
2161
2234
  ]
2162
2235
  };
2163
2236
  function getLayoutForSystem(system) {
2164
- const n = system.toUpperCase();
2165
- if (n.includes("NES") || n.includes("FAMICOM")) return NES_LAYOUT;
2166
- if (n.includes("SNES") || n.includes("SUPER")) return SNES_LAYOUT;
2167
- if (n.includes("GBA") || n.includes("ADVANCE")) return GBA_LAYOUT;
2168
- if (n.includes("GB") || n.includes("GAME BOY")) return GB_LAYOUT;
2169
- if (n.includes("GENESIS") || n.includes("MEGA")) return GENESIS_LAYOUT;
2170
- return NES_LAYOUT;
2237
+ const s = system.toUpperCase();
2238
+ if (s === "PS1" || s === "PSX" || s === "PSP") return PSX_LAYOUT;
2239
+ if (s === "N64") return N64_LAYOUT;
2240
+ if (s === "SNES" || s === "NDS") return SNES_LAYOUT;
2241
+ if (s === "GBA") return GBA_LAYOUT;
2242
+ if (s === "NES" || s === "GB" || s === "GBC" || s === "VIRTUAL_BOY") return TWO_BUTTON_LAYOUT;
2243
+ if (s === "DREAMCAST") return DREAMCAST_LAYOUT;
2244
+ if (s === "SATURN") return SATURN_LAYOUT;
2245
+ if (s === "GENESIS") return SIX_BUTTON_LAYOUT;
2246
+ if (s === "MASTER_SYSTEM" || s === "GAME_GEAR") return TWO_BUTTON_LAYOUT;
2247
+ if (s === "NEOGEO") return NEOGEO_LAYOUT;
2248
+ if (s.includes("NEOGEO_POCKET")) return TWO_BUTTON_LAYOUT;
2249
+ if (s === "ARCADE") return SIX_BUTTON_LAYOUT;
2250
+ if (s === "PC_ENGINE" || s === "TURBOGRAFX") return TWO_BUTTON_LAYOUT;
2251
+ if (s.includes("WONDERSWAN")) return TWO_BUTTON_LAYOUT;
2252
+ if (s.includes("ATARI")) return TWO_BUTTON_LAYOUT;
2253
+ return TWO_BUTTON_LAYOUT;
2171
2254
  }
2172
- var DRAG_HOLD_DELAY = 300;
2255
+ var DRAG_HOLD_DELAY = 350;
2173
2256
  var DRAG_MOVE_THRESHOLD = 10;
2174
2257
  var DRAG_CENTER_THRESHOLD = 0.4;
2175
2258
  function useTouchHandlers({
@@ -2202,6 +2285,9 @@ function useTouchHandlers({
2202
2285
  x: touchX - displayX / 100 * containerWidth,
2203
2286
  y: touchY - displayY / 100 * containerHeight
2204
2287
  };
2288
+ if (navigator.vibrate) {
2289
+ navigator.vibrate([10, 30, 10]);
2290
+ }
2205
2291
  if (!isSystemButton) {
2206
2292
  onRelease(buttonType);
2207
2293
  }
@@ -2317,55 +2403,119 @@ function useTouchHandlers({
2317
2403
  }
2318
2404
 
2319
2405
  // src/components/VirtualController/utils/buttonStyles.ts
2320
- function getButtonStyles(buttonType, isPressed) {
2406
+ var DEFAULT_FACE = {
2407
+ bg: "bg-white/10 backdrop-blur-md",
2408
+ text: "text-white",
2409
+ border: "border-white/30",
2410
+ shadow: "shadow-lg",
2411
+ transform: ""
2412
+ };
2413
+ var DEFAULT_SHOULDER = {
2414
+ bg: "bg-white/20 backdrop-blur-md",
2415
+ text: "text-white",
2416
+ border: "border-white/30",
2417
+ shadow: "shadow-sm",
2418
+ transform: ""
2419
+ };
2420
+ var DEFAULT_SYSTEM = {
2421
+ bg: "bg-black/60 backdrop-blur-md",
2422
+ text: "text-white/80",
2423
+ border: "border-white/20",
2424
+ shadow: "shadow-sm",
2425
+ transform: ""
2426
+ };
2427
+ var PRESSED_STYLE = {
2428
+ bg: "bg-white/90",
2429
+ text: "text-black",
2430
+ border: "border-white",
2431
+ shadow: "shadow-none",
2432
+ transform: "scale(0.95)"
2433
+ };
2434
+ function coloredButton(bgColor, textColor = "text-white") {
2435
+ return {
2436
+ bg: bgColor,
2437
+ text: textColor,
2438
+ border: "border-white/20",
2439
+ shadow: "shadow-lg",
2440
+ transform: ""
2441
+ };
2442
+ }
2443
+ function psButton(symbolColor) {
2444
+ return {
2445
+ bg: "bg-black/80 backdrop-blur-md",
2446
+ text: symbolColor,
2447
+ border: "border-white/30",
2448
+ shadow: "shadow-lg",
2449
+ transform: ""
2450
+ };
2451
+ }
2452
+ function getButtonStyles(buttonType, isPressed, consoleName = "") {
2321
2453
  if (isPressed) {
2322
- return {
2323
- bg: "bg-retro-primary",
2324
- text: "text-black",
2325
- border: "border-white",
2326
- shadow: "shadow-none",
2327
- transform: "translate-x-[3px] translate-y-[3px]"
2328
- };
2454
+ return PRESSED_STYLE;
2455
+ }
2456
+ const c = consoleName.toUpperCase();
2457
+ if (c === "NEOGEO") {
2458
+ switch (buttonType) {
2459
+ case "b":
2460
+ return coloredButton("bg-[#E60012]");
2461
+ // A = Red
2462
+ case "a":
2463
+ return coloredButton("bg-[#FFD600]", "text-black");
2464
+ // B = Yellow
2465
+ case "y":
2466
+ return coloredButton("bg-[#009944]");
2467
+ // C = Green
2468
+ case "x":
2469
+ return coloredButton("bg-[#0068B7]");
2470
+ }
2471
+ }
2472
+ if (c === "PSX" || c === "PS1" || c === "PSP") {
2473
+ switch (buttonType) {
2474
+ case "y":
2475
+ return psButton("text-[#25D998]");
2476
+ // △ = Green
2477
+ case "b":
2478
+ return psButton("text-[#FF5555]");
2479
+ // ○ = Red
2480
+ case "a":
2481
+ return psButton("text-[#5599FF]");
2482
+ // ✕ = Blue
2483
+ case "x":
2484
+ return psButton("text-[#E889DD]");
2485
+ }
2486
+ }
2487
+ if (c === "SNES" || c === "NDS") {
2488
+ switch (buttonType) {
2489
+ case "a":
2490
+ return coloredButton("bg-[#CC0000]");
2491
+ // A = Red
2492
+ case "b":
2493
+ return coloredButton("bg-[#CCCC00]", "text-black");
2494
+ // B = Yellow
2495
+ case "x":
2496
+ return coloredButton("bg-[#0033CC]");
2497
+ // X = Blue
2498
+ case "y":
2499
+ return coloredButton("bg-[#006600]");
2500
+ }
2329
2501
  }
2330
2502
  switch (buttonType) {
2331
2503
  case "a":
2332
2504
  case "b":
2333
- case "c":
2334
- return {
2335
- bg: "bg-red-600",
2336
- text: "text-white",
2337
- border: "border-white",
2338
- shadow: "shadow-hard",
2339
- transform: ""
2340
- };
2341
2505
  case "x":
2342
2506
  case "y":
2343
- return {
2344
- bg: "bg-blue-600",
2345
- text: "text-white",
2346
- border: "border-white",
2347
- shadow: "shadow-hard",
2348
- transform: ""
2349
- };
2507
+ return DEFAULT_FACE;
2350
2508
  case "l":
2351
2509
  case "r":
2510
+ case "l2":
2511
+ case "r2":
2512
+ return DEFAULT_SHOULDER;
2352
2513
  case "start":
2353
2514
  case "select":
2354
- return {
2355
- bg: "bg-retro-surface",
2356
- text: "text-white",
2357
- border: "border-white",
2358
- shadow: "shadow-hard",
2359
- transform: ""
2360
- };
2515
+ case "menu":
2516
+ return DEFAULT_SYSTEM;
2361
2517
  default:
2362
- return {
2363
- bg: "bg-black",
2364
- text: "text-white",
2365
- border: "border-white",
2366
- shadow: "shadow-hard",
2367
- transform: ""
2368
- };
2518
+ return DEFAULT_FACE;
2369
2519
  }
2370
2520
  }
2371
2521
  var VirtualButton = React2__default.default.memo(function VirtualButton2({
@@ -2379,12 +2529,11 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
2379
2529
  customPosition,
2380
2530
  onPositionChange,
2381
2531
  isLandscape = false,
2382
- systemColor = "#00FF41"
2383
- // Default retro green
2532
+ console: console2 = ""
2384
2533
  }) {
2385
2534
  const t = useKoinTranslation();
2386
2535
  const buttonRef = React2.useRef(null);
2387
- const isSystemButton = config.type === "start" || config.type === "select";
2536
+ const isSystemButton = config.type === "start" || config.type === "select" || config.type === "menu";
2388
2537
  const displayX = customPosition ? customPosition.x : config.x;
2389
2538
  const displayY = customPosition ? customPosition.y : config.y;
2390
2539
  let label = config.label;
@@ -2427,63 +2576,78 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
2427
2576
  const leftPercent = displayX / 100 * containerWidth - config.size / 2;
2428
2577
  const topPercent = displayY / 100 * containerHeight - config.size / 2;
2429
2578
  const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
2430
- const styles = getButtonStyles(config.type, isPressed);
2431
- const isActionButton = config.type === "a" || config.type === "b";
2432
- const borderRadius = isActionButton ? "50%" : "0";
2433
- const pressedStyle = isPressed ? {
2434
- backgroundColor: systemColor,
2435
- color: "#000000",
2436
- borderColor: "#FFFFFF"
2437
- } : {};
2579
+ const styles = getButtonStyles(config.type, isPressed, console2);
2580
+ let borderRadius = "50%";
2581
+ let width = `${config.size}px`;
2582
+ if (config.shape === "pill") {
2583
+ borderRadius = "20px";
2584
+ width = `${config.size * 1.8}px`;
2585
+ } else if (config.shape === "rect") {
2586
+ borderRadius = "8px";
2587
+ width = `${config.size * 2.5}px`;
2588
+ } else if (config.shape === "square") {
2589
+ borderRadius = "12px";
2590
+ } else {
2591
+ if (["l", "r", "l2", "r2"].includes(config.type)) {
2592
+ borderRadius = "10px";
2593
+ width = `${config.size * 2}px`;
2594
+ }
2595
+ }
2438
2596
  return /* @__PURE__ */ jsxRuntime.jsx(
2439
2597
  "button",
2440
2598
  {
2441
2599
  ref: buttonRef,
2442
2600
  className: `
2443
- absolute border-4 font-heading font-bold uppercase tracking-wider
2444
- transition-all duration-100 select-none
2601
+ absolute flex items-center justify-center
2602
+ font-heading font-bold uppercase tracking-wider
2603
+ transition-all duration-75 select-none
2445
2604
  pointer-events-auto touch-manipulation
2446
- ${isPressed ? "" : `${styles.bg} ${styles.text} ${styles.border} ${styles.shadow}`} ${styles.transform}
2447
- active:translate-x-[3px] active:translate-y-[3px] active:shadow-none
2605
+ backdrop-blur-sm
2606
+ ${isPressed ? "" : `${styles.bg} ${styles.border} ${styles.shadow}`}
2607
+ ${styles.text}
2608
+ active:shadow-none
2448
2609
  `,
2449
2610
  style: {
2450
- // Remove left/top and use transform instead for high performance (compositor only)
2451
2611
  top: 0,
2452
2612
  left: 0,
2453
- transform,
2613
+ transform: transform + (isPressed ? " scale(0.95)" : ""),
2454
2614
  willChange: "transform",
2455
- width: `${config.size}px`,
2615
+ width,
2456
2616
  height: `${config.size}px`,
2617
+ // Height stays consistent
2457
2618
  minWidth: `${config.size}px`,
2458
- minHeight: `${config.size}px`,
2459
- fontSize: config.size < 50 ? "10px" : "12px",
2619
+ fontSize: config.size < 50 ? "11px" : "16px",
2460
2620
  borderRadius,
2621
+ borderWidth: isPressed ? "0px" : "1.5px",
2622
+ // slightly thicker border
2461
2623
  lineHeight: "1",
2462
2624
  // Semi-transparent in landscape mode
2463
2625
  opacity: isLandscape ? 0.85 : 1,
2464
- // Prevent context menu on long-press
2465
2626
  WebkitTouchCallout: "none",
2466
2627
  WebkitUserSelect: "none",
2467
2628
  userSelect: "none",
2468
- ...pressedStyle
2629
+ // Direct style overrides if needed from getButtonStyles (for exact hex colors)
2630
+ backgroundColor: isPressed ? styles.bg.startsWith("bg-") ? void 0 : styles.bg : styles.bg.startsWith("bg-") ? void 0 : styles.bg,
2631
+ borderColor: isPressed ? void 0 : styles.border.startsWith("border-") ? void 0 : styles.border,
2632
+ color: styles.text.startsWith("text-") ? void 0 : styles.text
2469
2633
  },
2470
2634
  "aria-label": label,
2471
2635
  onContextMenu: (e) => e.preventDefault(),
2472
- children: label
2636
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "drop-shadow-md", children: label })
2473
2637
  }
2474
2638
  );
2475
2639
  });
2476
2640
  var VirtualButton_default = VirtualButton;
2477
2641
 
2478
2642
  // src/lib/controls/types.ts
2479
- var DPAD_BUTTONS = ["up", "down", "left", "right"];
2643
+ var DPAD_BUTTONS2 = ["up", "down", "left", "right"];
2480
2644
  var FACE_BUTTONS = ["a", "b", "x", "y"];
2481
2645
  var SHOULDER_BUTTONS = ["l", "r"];
2482
2646
  var TRIGGER_BUTTONS = ["l2", "r2"];
2483
2647
  var STICK_BUTTONS = ["l3", "r3"];
2484
2648
  var SYSTEM_BUTTONS = ["start", "select"];
2485
2649
  var ALL_BUTTONS = [
2486
- ...DPAD_BUTTONS,
2650
+ ...DPAD_BUTTONS2,
2487
2651
  ...FACE_BUTTONS,
2488
2652
  ...SHOULDER_BUTTONS,
2489
2653
  ...TRIGGER_BUTTONS,
@@ -2641,91 +2805,91 @@ var CONSOLE_CAPABILITIES = {
2641
2805
  // ============ Nintendo ============
2642
2806
  NES: {
2643
2807
  console: "NES",
2644
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2808
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2645
2809
  },
2646
2810
  SNES: {
2647
2811
  console: "SNES",
2648
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "start", "select"]
2812
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "start", "select"]
2649
2813
  },
2650
2814
  N64: {
2651
2815
  console: "N64",
2652
- buttons: [...DPAD_BUTTONS, "a", "b", "l", "r", "start"]
2816
+ buttons: [...DPAD_BUTTONS2, "a", "b", "l", "r", "start"]
2653
2817
  // No select, has Z trigger (mapped to l2)
2654
2818
  },
2655
2819
  GB: {
2656
2820
  console: "GB",
2657
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2821
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2658
2822
  },
2659
2823
  GBC: {
2660
2824
  console: "GBC",
2661
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2825
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2662
2826
  },
2663
2827
  GBA: {
2664
2828
  console: "GBA",
2665
- buttons: [...DPAD_BUTTONS, "a", "b", "l", "r", "start", "select"]
2829
+ buttons: [...DPAD_BUTTONS2, "a", "b", "l", "r", "start", "select"]
2666
2830
  },
2667
2831
  // ============ Sega ============
2668
2832
  SMS: {
2669
2833
  console: "SMS",
2670
- buttons: [...DPAD_BUTTONS, "a", "b", "start"]
2834
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start"]
2671
2835
  // 1, 2, Pause
2672
2836
  },
2673
2837
  GENESIS: {
2674
2838
  console: "GENESIS",
2675
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "start"]
2839
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "start"]
2676
2840
  // 6-button: A/B/C + X/Y/Z
2677
2841
  },
2678
2842
  GG: {
2679
2843
  console: "GG",
2680
- buttons: [...DPAD_BUTTONS, "a", "b", "start"]
2844
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start"]
2681
2845
  },
2682
2846
  SATURN: {
2683
2847
  console: "SATURN",
2684
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "l2", "r2", "start"]
2848
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "l2", "r2", "start"]
2685
2849
  // Full 8-button
2686
2850
  },
2687
2851
  // ============ Sony ============
2688
2852
  PS1: {
2689
2853
  console: "PS1",
2690
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "l2", "r2", "l3", "r3", "start", "select"]
2854
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "l2", "r2", "l3", "r3", "start", "select"]
2691
2855
  },
2692
2856
  // ============ NEC ============
2693
2857
  PCE: {
2694
2858
  console: "PCE",
2695
2859
  // TurboGrafx-16
2696
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2860
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2697
2861
  // I, II, Run, Select
2698
2862
  },
2699
2863
  // ============ Atari ============
2700
2864
  ATARI2600: {
2701
2865
  console: "ATARI2600",
2702
- buttons: [...DPAD_BUTTONS, "a", "select"]
2866
+ buttons: [...DPAD_BUTTONS2, "a", "select"]
2703
2867
  // Fire, Select/Reset
2704
2868
  },
2705
2869
  ATARI7800: {
2706
2870
  console: "ATARI7800",
2707
- buttons: [...DPAD_BUTTONS, "a", "b", "select"]
2871
+ buttons: [...DPAD_BUTTONS2, "a", "b", "select"]
2708
2872
  },
2709
2873
  LYNX: {
2710
2874
  console: "LYNX",
2711
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2875
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2712
2876
  // A, B, Option 1, Option 2
2713
2877
  },
2714
2878
  // ============ SNK ============
2715
2879
  NGPC: {
2716
2880
  console: "NGPC",
2717
2881
  // Neo Geo Pocket
2718
- buttons: [...DPAD_BUTTONS, "a", "b", "start", "select"]
2882
+ buttons: [...DPAD_BUTTONS2, "a", "b", "start", "select"]
2719
2883
  },
2720
2884
  // ============ Other ============
2721
2885
  WSC: {
2722
2886
  console: "WSC",
2723
2887
  // WonderSwan
2724
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "start"]
2888
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "start"]
2725
2889
  },
2726
2890
  ARCADE: {
2727
2891
  console: "ARCADE",
2728
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "start", "select"]
2892
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "start", "select"]
2729
2893
  // Generic 6-button
2730
2894
  }
2731
2895
  };
@@ -2766,7 +2930,7 @@ function getConsoleCapabilities(system) {
2766
2930
  const normalized = system.toUpperCase();
2767
2931
  return CONSOLE_CAPABILITIES[normalized] ?? {
2768
2932
  console: normalized,
2769
- buttons: [...DPAD_BUTTONS, "a", "b", "x", "y", "l", "r", "start", "select"]
2933
+ buttons: [...DPAD_BUTTONS2, "a", "b", "x", "y", "l", "r", "start", "select"]
2770
2934
  };
2771
2935
  }
2772
2936
  function getConsoleButtons(system) {
@@ -3054,13 +3218,11 @@ var DEFAULT_CONTROLS = DEFAULT_KEYBOARD;
3054
3218
 
3055
3219
  // src/components/VirtualController/utils/keyboardEvents.ts
3056
3220
  function getKeyboardCode(buttonType, controls) {
3057
- const controlMapping = controls || DEFAULT_CONTROLS;
3058
- const mappingKey = buttonType === "c" ? "x" : buttonType;
3059
- const key = mappingKey;
3060
- if (key in controlMapping) {
3061
- return controlMapping[key] ?? null;
3221
+ const key = buttonType;
3222
+ if (controls && key in controls && controls[key]) {
3223
+ return controls[key];
3062
3224
  }
3063
- return null;
3225
+ return DEFAULT_CONTROLS[key] ?? null;
3064
3226
  }
3065
3227
  function getKeyName(code) {
3066
3228
  if (code.startsWith("Key")) return code.slice(3).toLowerCase();
@@ -3086,23 +3248,34 @@ function dispatchKeyboardEvent(type, code) {
3086
3248
  canvas.dispatchEvent(event);
3087
3249
  return true;
3088
3250
  }
3251
+ var DRAG_HOLD_DELAY2 = 350;
3252
+ var CENTER_TOUCH_RADIUS = 0.25;
3089
3253
  var Dpad = React2__default.default.memo(function Dpad2({
3090
- size,
3254
+ size = 180,
3091
3255
  x,
3092
3256
  y,
3093
3257
  containerWidth,
3094
3258
  containerHeight,
3095
3259
  controls,
3096
3260
  systemColor = "#00FF41",
3097
- isLandscape = false
3261
+ isLandscape = false,
3262
+ customPosition,
3263
+ onPositionChange
3098
3264
  }) {
3099
3265
  const dpadRef = React2.useRef(null);
3100
3266
  const activeTouchRef = React2.useRef(null);
3101
3267
  const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
3102
- const upRef = React2.useRef(null);
3103
- const downRef = React2.useRef(null);
3104
- const leftRef = React2.useRef(null);
3105
- const rightRef = React2.useRef(null);
3268
+ const [isDragging, setIsDragging] = React2.useState(false);
3269
+ const dragTimerRef = React2.useRef(null);
3270
+ const dragStartRef = React2.useRef({ x: 0, y: 0, touchX: 0, touchY: 0 });
3271
+ const touchStartPosRef = React2.useRef({ x: 0, y: 0, time: 0 });
3272
+ const upPathRef = React2.useRef(null);
3273
+ const downPathRef = React2.useRef(null);
3274
+ const leftPathRef = React2.useRef(null);
3275
+ const rightPathRef = React2.useRef(null);
3276
+ const centerCircleRef = React2.useRef(null);
3277
+ const displayX = customPosition ? customPosition.x : x;
3278
+ const displayY = customPosition ? customPosition.y : y;
3106
3279
  const getKeyCode = React2.useCallback((direction) => {
3107
3280
  if (!controls) {
3108
3281
  const defaults = {
@@ -3120,24 +3293,41 @@ var Dpad = React2__default.default.memo(function Dpad2({
3120
3293
  const centerY = rect.top + rect.height / 2;
3121
3294
  const dx = touchX - centerX;
3122
3295
  const dy = touchY - centerY;
3123
- const deadZone = rect.width * 0.12;
3124
3296
  const distance = Math.sqrt(dx * dx + dy * dy);
3297
+ const deadZone = rect.width / 2 * 0.15;
3125
3298
  if (distance < deadZone) return /* @__PURE__ */ new Set();
3126
3299
  const directions = /* @__PURE__ */ new Set();
3127
3300
  const angle = Math.atan2(dy, dx) * (180 / Math.PI);
3128
- if (angle >= -157.5 && angle <= -22.5) directions.add("up");
3129
- if (angle >= 22.5 && angle <= 157.5) directions.add("down");
3130
- if (angle >= 112.5 || angle <= -112.5) directions.add("left");
3131
- if (angle >= -67.5 && angle <= 67.5) directions.add("right");
3301
+ if (angle >= -150 && angle <= -30) directions.add("up");
3302
+ if (angle >= 30 && angle <= 150) directions.add("down");
3303
+ if (angle >= 120 || angle <= -120) directions.add("left");
3304
+ if (angle >= -60 && angle <= 60) directions.add("right");
3132
3305
  return directions;
3133
3306
  }, []);
3134
3307
  const updateVisuals = React2.useCallback((directions) => {
3135
- const activeColor = systemColor;
3136
- const inactiveColor = "#1a1a1a";
3137
- if (upRef.current) upRef.current.style.fill = directions.has("up") ? activeColor : inactiveColor;
3138
- if (downRef.current) downRef.current.style.fill = directions.has("down") ? activeColor : inactiveColor;
3139
- if (leftRef.current) leftRef.current.style.fill = directions.has("left") ? activeColor : inactiveColor;
3140
- if (rightRef.current) rightRef.current.style.fill = directions.has("right") ? activeColor : inactiveColor;
3308
+ const activeFill = `${systemColor}80`;
3309
+ const inactiveFill = "rgba(255, 255, 255, 0.05)";
3310
+ const activeStroke = systemColor;
3311
+ const inactiveStroke = "rgba(255, 255, 255, 0.2)";
3312
+ const glow = `0 0 15px ${systemColor}`;
3313
+ const updatePart = (ref, isActive) => {
3314
+ if (ref.current) {
3315
+ ref.current.style.fill = isActive ? activeFill : inactiveFill;
3316
+ ref.current.style.stroke = isActive ? activeStroke : inactiveStroke;
3317
+ ref.current.style.filter = isActive ? `drop-shadow(${glow})` : "none";
3318
+ ref.current.style.transform = isActive ? "scale(0.98)" : "scale(1)";
3319
+ ref.current.style.transformOrigin = "center";
3320
+ }
3321
+ };
3322
+ updatePart(upPathRef, directions.has("up"));
3323
+ updatePart(downPathRef, directions.has("down"));
3324
+ updatePart(leftPathRef, directions.has("left"));
3325
+ updatePart(rightPathRef, directions.has("right"));
3326
+ if (centerCircleRef.current) {
3327
+ const isAny = directions.size > 0;
3328
+ centerCircleRef.current.style.fill = isAny ? systemColor : "rgba(0,0,0,0.5)";
3329
+ centerCircleRef.current.style.stroke = isAny ? "#fff" : "rgba(255,255,255,0.3)";
3330
+ }
3141
3331
  }, [systemColor]);
3142
3332
  const updateDirections = React2.useCallback((newDirections) => {
3143
3333
  const prev = activeDirectionsRef.current;
@@ -3159,48 +3349,107 @@ var Dpad = React2__default.default.memo(function Dpad2({
3159
3349
  activeDirectionsRef.current = newDirections;
3160
3350
  updateVisuals(newDirections);
3161
3351
  }, [getKeyCode, updateVisuals]);
3352
+ const clearDragTimer = React2.useCallback(() => {
3353
+ if (dragTimerRef.current) {
3354
+ clearTimeout(dragTimerRef.current);
3355
+ dragTimerRef.current = null;
3356
+ }
3357
+ }, []);
3358
+ const startDragging = React2.useCallback((touchX, touchY) => {
3359
+ setIsDragging(true);
3360
+ dragStartRef.current = {
3361
+ x: displayX,
3362
+ y: displayY,
3363
+ touchX,
3364
+ touchY
3365
+ };
3366
+ activeDirectionsRef.current.forEach((dir) => {
3367
+ const keyCode = getKeyCode(dir);
3368
+ if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3369
+ });
3370
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3371
+ updateVisuals(/* @__PURE__ */ new Set());
3372
+ if (navigator.vibrate) navigator.vibrate([10, 30, 10]);
3373
+ }, [displayX, displayY, getKeyCode, updateVisuals]);
3162
3374
  const handleTouchStart = React2.useCallback((e) => {
3163
3375
  e.preventDefault();
3164
- e.stopPropagation();
3165
- const touch = e.touches[0];
3376
+ if (activeTouchRef.current !== null) return;
3377
+ const touch = e.changedTouches[0];
3166
3378
  activeTouchRef.current = touch.identifier;
3379
+ touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
3167
3380
  const rect = dpadRef.current?.getBoundingClientRect();
3168
3381
  if (!rect) return;
3169
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3170
- }, [getDirectionsFromTouch, updateDirections]);
3382
+ const centerX = rect.left + rect.width / 2;
3383
+ const centerY = rect.top + rect.height / 2;
3384
+ const distFromCenter = Math.sqrt(
3385
+ Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
3386
+ );
3387
+ const centerRadius = size * CENTER_TOUCH_RADIUS;
3388
+ if (distFromCenter < centerRadius && onPositionChange) {
3389
+ dragTimerRef.current = setTimeout(() => {
3390
+ startDragging(touch.clientX, touch.clientY);
3391
+ }, DRAG_HOLD_DELAY2);
3392
+ }
3393
+ if (!isDragging) {
3394
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3395
+ }
3396
+ }, [getDirectionsFromTouch, updateDirections, isDragging, size, onPositionChange, startDragging]);
3171
3397
  const handleTouchMove = React2.useCallback((e) => {
3172
3398
  e.preventDefault();
3173
3399
  let touch = null;
3174
- for (let i = 0; i < e.touches.length; i++) {
3175
- if (e.touches[i].identifier === activeTouchRef.current) {
3176
- touch = e.touches[i];
3400
+ for (let i = 0; i < e.changedTouches.length; i++) {
3401
+ if (e.changedTouches[i].identifier === activeTouchRef.current) {
3402
+ touch = e.changedTouches[i];
3177
3403
  break;
3178
3404
  }
3179
3405
  }
3180
3406
  if (!touch) return;
3181
- const rect = dpadRef.current?.getBoundingClientRect();
3182
- if (!rect) return;
3183
- updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3184
- }, [getDirectionsFromTouch, updateDirections]);
3407
+ if (isDragging && onPositionChange) {
3408
+ const deltaX = touch.clientX - dragStartRef.current.touchX;
3409
+ const deltaY = touch.clientY - dragStartRef.current.touchY;
3410
+ const newXPercent = dragStartRef.current.x + deltaX / containerWidth * 100;
3411
+ const newYPercent = dragStartRef.current.y + deltaY / containerHeight * 100;
3412
+ const margin = size / 2 / Math.min(containerWidth, containerHeight) * 100;
3413
+ const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
3414
+ const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
3415
+ onPositionChange(constrainedX, constrainedY);
3416
+ } else {
3417
+ const moveDistance = Math.sqrt(
3418
+ Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
3419
+ );
3420
+ if (moveDistance > 15) {
3421
+ clearDragTimer();
3422
+ }
3423
+ const rect = dpadRef.current?.getBoundingClientRect();
3424
+ if (rect) {
3425
+ updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
3426
+ }
3427
+ }
3428
+ }, [isDragging, onPositionChange, containerWidth, containerHeight, size, getDirectionsFromTouch, updateDirections, clearDragTimer]);
3185
3429
  const handleTouchEnd = React2.useCallback((e) => {
3186
3430
  e.preventDefault();
3187
- let touchEnded = true;
3188
- for (let i = 0; i < e.touches.length; i++) {
3189
- if (e.touches[i].identifier === activeTouchRef.current) {
3190
- touchEnded = false;
3431
+ clearDragTimer();
3432
+ let touchEnded = false;
3433
+ for (let i = 0; i < e.changedTouches.length; i++) {
3434
+ if (e.changedTouches[i].identifier === activeTouchRef.current) {
3435
+ touchEnded = true;
3191
3436
  break;
3192
3437
  }
3193
3438
  }
3194
3439
  if (touchEnded) {
3195
3440
  activeTouchRef.current = null;
3196
- activeDirectionsRef.current.forEach((dir) => {
3197
- const keyCode = getKeyCode(dir);
3198
- if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3199
- });
3200
- activeDirectionsRef.current = /* @__PURE__ */ new Set();
3201
- updateVisuals(/* @__PURE__ */ new Set());
3441
+ if (isDragging) {
3442
+ setIsDragging(false);
3443
+ } else {
3444
+ activeDirectionsRef.current.forEach((dir) => {
3445
+ const keyCode = getKeyCode(dir);
3446
+ if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
3447
+ });
3448
+ activeDirectionsRef.current = /* @__PURE__ */ new Set();
3449
+ updateVisuals(/* @__PURE__ */ new Set());
3450
+ }
3202
3451
  }
3203
- }, [getKeyCode, updateVisuals]);
3452
+ }, [getKeyCode, updateVisuals, isDragging, clearDragTimer]);
3204
3453
  React2.useEffect(() => {
3205
3454
  const dpad = dpadRef.current;
3206
3455
  if (!dpad) return;
@@ -3213,37 +3462,57 @@ var Dpad = React2__default.default.memo(function Dpad2({
3213
3462
  dpad.removeEventListener("touchmove", handleTouchMove);
3214
3463
  dpad.removeEventListener("touchend", handleTouchEnd);
3215
3464
  dpad.removeEventListener("touchcancel", handleTouchEnd);
3465
+ clearDragTimer();
3216
3466
  };
3217
- }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
3218
- const leftPx = x / 100 * containerWidth - size / 2;
3219
- const topPx = y / 100 * containerHeight - size / 2;
3220
- return /* @__PURE__ */ jsxRuntime.jsx(
3467
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd, clearDragTimer]);
3468
+ const leftPx = displayX / 100 * containerWidth - size / 2;
3469
+ const topPx = displayY / 100 * containerHeight - size / 2;
3470
+ const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
3471
+ const dRight = "M 65,35 L 95,35 L 95,65 L 65,65 L 50,50 Z";
3472
+ const dDown = "M 65,65 L 65,95 L 35,95 L 35,65 L 50,50 Z";
3473
+ const dLeft = "M 35,65 L 5,65 L 5,35 L 35,35 L 50,50 Z";
3474
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3221
3475
  "div",
3222
3476
  {
3223
3477
  ref: dpadRef,
3224
- className: "absolute pointer-events-auto touch-manipulation",
3478
+ className: `absolute pointer-events-auto touch-manipulation select-none ${isDragging ? "opacity-60" : ""}`,
3225
3479
  style: {
3226
3480
  top: 0,
3227
3481
  left: 0,
3228
- transform: `translate3d(${leftPx}px, ${topPx}px, 0)`,
3482
+ transform: `translate3d(${leftPx}px, ${topPx}px, 0)${isDragging ? " scale(1.05)" : ""}`,
3229
3483
  width: size,
3230
3484
  height: size,
3231
- opacity: isLandscape ? 0.85 : 1,
3485
+ opacity: isLandscape ? 0.75 : 0.9,
3232
3486
  WebkitTouchCallout: "none",
3233
- userSelect: "none"
3487
+ WebkitUserSelect: "none",
3488
+ touchAction: "none",
3489
+ transition: isDragging ? "none" : "transform 0.1s ease-out"
3234
3490
  },
3235
- onContextMenu: (e) => e.preventDefault(),
3236
- children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 100 100", children: [
3237
- /* @__PURE__ */ jsxRuntime.jsx("rect", { ref: upRef, x: "35", y: "5", width: "30", height: "30", rx: "4", fill: "#1a1a1a", stroke: "white", strokeWidth: "2" }),
3238
- /* @__PURE__ */ jsxRuntime.jsx("rect", { ref: downRef, x: "35", y: "65", width: "30", height: "30", rx: "4", fill: "#1a1a1a", stroke: "white", strokeWidth: "2" }),
3239
- /* @__PURE__ */ jsxRuntime.jsx("rect", { ref: leftRef, x: "5", y: "35", width: "30", height: "30", rx: "4", fill: "#1a1a1a", stroke: "white", strokeWidth: "2" }),
3240
- /* @__PURE__ */ jsxRuntime.jsx("rect", { ref: rightRef, x: "65", y: "35", width: "30", height: "30", rx: "4", fill: "#1a1a1a", stroke: "white", strokeWidth: "2" }),
3241
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "35", y: "35", width: "30", height: "30", fill: "#000", stroke: "white", strokeWidth: "2" }),
3242
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: "50", y: "25", textAnchor: "middle", fill: "#fff", fontSize: "14", fontWeight: "bold", children: "\u2191" }),
3243
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: "50", y: "85", textAnchor: "middle", fill: "#fff", fontSize: "14", fontWeight: "bold", children: "\u2193" }),
3244
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: "20", y: "55", textAnchor: "middle", fill: "#fff", fontSize: "14", fontWeight: "bold", children: "\u2190" }),
3245
- /* @__PURE__ */ jsxRuntime.jsx("text", { x: "80", y: "55", textAnchor: "middle", fill: "#fff", fontSize: "14", fontWeight: "bold", children: "\u2192" })
3246
- ] })
3491
+ children: [
3492
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
3493
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
3494
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
3495
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
3496
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ref: downPathRef, d: dDown, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
3497
+ /* @__PURE__ */ jsxRuntime.jsx("path", { ref: leftPathRef, d: dLeft, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
3498
+ /* @__PURE__ */ jsxRuntime.jsx(
3499
+ "circle",
3500
+ {
3501
+ ref: centerCircleRef,
3502
+ cx: "50",
3503
+ cy: "50",
3504
+ r: "12",
3505
+ fill: isDragging ? systemColor : "rgba(0,0,0,0.5)",
3506
+ stroke: isDragging ? "#fff" : "rgba(255,255,255,0.3)",
3507
+ strokeWidth: isDragging ? 2 : 1
3508
+ }
3509
+ ),
3510
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
3511
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,85 L 50,75 M 45,80 L 50,85 L 55,80", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
3512
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 15,50 L 25,50 M 20,45 L 15,50 L 20,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
3513
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 85,50 L 75,50 M 80,45 L 85,50 L 80,55", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" })
3514
+ ] })
3515
+ ]
3247
3516
  }
3248
3517
  );
3249
3518
  });
@@ -3329,6 +3598,72 @@ function useButtonPositions() {
3329
3598
  resetPositions
3330
3599
  };
3331
3600
  }
3601
+ var STORAGE_KEY2 = "koin-controls-hint-shown";
3602
+ function ControlsHint({ isVisible }) {
3603
+ const [show, setShow] = React2.useState(false);
3604
+ React2.useEffect(() => {
3605
+ if (!isVisible) return;
3606
+ try {
3607
+ const wasShown = sessionStorage.getItem(STORAGE_KEY2);
3608
+ if (!wasShown) {
3609
+ const timer = setTimeout(() => setShow(true), 1500);
3610
+ return () => clearTimeout(timer);
3611
+ }
3612
+ } catch {
3613
+ }
3614
+ }, [isVisible]);
3615
+ const handleDismiss = () => {
3616
+ setShow(false);
3617
+ try {
3618
+ sessionStorage.setItem(STORAGE_KEY2, "true");
3619
+ } catch {
3620
+ }
3621
+ };
3622
+ if (!show) return null;
3623
+ return /* @__PURE__ */ jsxRuntime.jsx(
3624
+ "div",
3625
+ {
3626
+ className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm",
3627
+ onClick: handleDismiss,
3628
+ onTouchEnd: handleDismiss,
3629
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3630
+ "div",
3631
+ {
3632
+ className: "bg-black/90 border border-white/30 rounded-2xl p-6 mx-4 max-w-sm text-center shadow-2xl pointer-events-auto",
3633
+ onClick: (e) => e.stopPropagation(),
3634
+ onTouchEnd: (e) => e.stopPropagation(),
3635
+ children: [
3636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 32, className: "text-green-400" }) }) }),
3637
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
3638
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-4", children: [
3639
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "Long-press" }),
3640
+ " any button or the ",
3641
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "D-pad center" }),
3642
+ " to drag and reposition it."
3643
+ ] }),
3644
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-white/50 text-xs mb-4", children: "Your layout is saved separately for portrait and landscape modes." }),
3645
+ /* @__PURE__ */ jsxRuntime.jsx(
3646
+ "button",
3647
+ {
3648
+ type: "button",
3649
+ onClick: (e) => {
3650
+ e.stopPropagation();
3651
+ handleDismiss();
3652
+ },
3653
+ onTouchEnd: (e) => {
3654
+ e.stopPropagation();
3655
+ handleDismiss();
3656
+ },
3657
+ className: "w-full py-3 px-4 bg-green-500 hover:bg-green-400 active:bg-green-600 text-black font-bold rounded-xl transition-colors flex items-center justify-center gap-2 cursor-pointer touch-manipulation",
3658
+ children: "Got it!"
3659
+ }
3660
+ )
3661
+ ]
3662
+ }
3663
+ )
3664
+ }
3665
+ );
3666
+ }
3332
3667
  function VirtualController({
3333
3668
  system,
3334
3669
  isRunning,
@@ -3349,7 +3684,6 @@ function VirtualController({
3349
3684
  return btn.showInLandscape;
3350
3685
  });
3351
3686
  const DPAD_TYPES = ["up", "down", "left", "right"];
3352
- const dpadButtons = visibleButtons.filter((btn) => DPAD_TYPES.includes(btn.type));
3353
3687
  React2.useEffect(() => {
3354
3688
  const updateSize = () => {
3355
3689
  const { width, height } = getViewportSize();
@@ -3399,7 +3733,7 @@ function VirtualController({
3399
3733
  },
3400
3734
  [controls]
3401
3735
  );
3402
- const SYSTEM_BUTTONS2 = ["start", "select"];
3736
+ const SYSTEM_BUTTONS2 = ["start", "select", "menu"];
3403
3737
  const handlePress = React2.useCallback(
3404
3738
  (buttonType) => {
3405
3739
  const isSystemButton = SYSTEM_BUTTONS2.includes(buttonType);
@@ -3487,26 +3821,28 @@ function VirtualController({
3487
3821
  if (!isMobile) {
3488
3822
  return null;
3489
3823
  }
3490
- if (visibleButtons.length === 0) {
3491
- return null;
3492
- }
3824
+ const dpadSize = containerSize.width > containerSize.height ? 160 : 180;
3825
+ const dpadY = containerSize.width > containerSize.height ? 55 : 62;
3826
+ const finalDpadY = system.toUpperCase() === "NEOGEO" ? dpadY - 5 : dpadY;
3493
3827
  return /* @__PURE__ */ jsxRuntime.jsxs(
3494
3828
  "div",
3495
3829
  {
3496
3830
  className: "fixed inset-0 z-30 pointer-events-none",
3497
3831
  style: { touchAction: "none" },
3498
3832
  children: [
3499
- dpadButtons.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3833
+ /* @__PURE__ */ jsxRuntime.jsx(
3500
3834
  Dpad_default,
3501
3835
  {
3502
- size: containerSize.width > containerSize.height ? 120 : 130,
3503
- x: 12,
3504
- y: containerSize.width > containerSize.height ? 52 : 62,
3836
+ size: dpadSize,
3837
+ x: 14,
3838
+ y: finalDpadY,
3505
3839
  containerWidth: containerSize.width || window.innerWidth,
3506
3840
  containerHeight: containerSize.height || window.innerHeight,
3507
3841
  controls,
3508
3842
  systemColor,
3509
- isLandscape
3843
+ isLandscape,
3844
+ customPosition: getPosition("up", isLandscape),
3845
+ onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
3510
3846
  }
3511
3847
  ),
3512
3848
  memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -3522,64 +3858,77 @@ function VirtualController({
3522
3858
  customPosition,
3523
3859
  onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
3524
3860
  isLandscape,
3525
- systemColor
3861
+ console: layout.console
3526
3862
  },
3527
3863
  buttonConfig.type
3528
- ))
3864
+ )),
3865
+ /* @__PURE__ */ jsxRuntime.jsx(ControlsHint, { isVisible: isRunning })
3529
3866
  ]
3530
3867
  }
3531
3868
  );
3532
3869
  }
3533
3870
  function FloatingExitButton({ onClick, disabled = false }) {
3534
- return /* @__PURE__ */ jsxRuntime.jsx(
3871
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3535
3872
  "button",
3536
3873
  {
3537
3874
  onClick,
3538
3875
  disabled,
3539
3876
  className: `
3540
- fixed top-3 left-1/2 -translate-x-1/2 z-50
3541
- w-11 h-11 rounded-lg
3542
- bg-black/90 backdrop-blur-sm
3543
- border-2 border-white/80
3544
- shadow-lg
3545
- flex items-center justify-center
3546
- transition-all duration-200
3877
+ fixed top-3 right-3 z-50
3878
+ px-3 py-2 rounded-xl
3879
+ bg-black/80 backdrop-blur-md
3880
+ border-2 border-red-400/60
3881
+ shadow-xl
3882
+ flex items-center gap-2
3883
+ transition-all duration-300
3547
3884
  hover:bg-red-600/30 hover:border-red-400 hover:scale-105
3548
3885
  active:scale-95
3549
3886
  disabled:opacity-40 disabled:cursor-not-allowed
3550
3887
  touch-manipulation
3551
3888
  `,
3552
3889
  style: {
3553
- paddingTop: "env(safe-area-inset-top, 0px)"
3890
+ paddingTop: "max(env(safe-area-inset-top, 0px), 8px)"
3554
3891
  },
3555
3892
  "aria-label": "Exit fullscreen",
3556
- title: "Exit fullscreen",
3557
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 22, className: "text-white" })
3893
+ children: [
3894
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, className: "text-red-400" }),
3895
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Exit" }),
3896
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minimize2, { size: 14, className: "text-white/60" })
3897
+ ]
3558
3898
  }
3559
3899
  );
3560
3900
  }
3561
3901
  function FloatingFullscreenButton({ onClick, disabled = false }) {
3562
- return /* @__PURE__ */ jsxRuntime.jsx(
3902
+ const [pulse, setPulse] = React2.useState(true);
3903
+ React2.useEffect(() => {
3904
+ const timer = setTimeout(() => setPulse(false), 5e3);
3905
+ return () => clearTimeout(timer);
3906
+ }, []);
3907
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3563
3908
  "button",
3564
3909
  {
3565
3910
  onClick,
3566
3911
  disabled,
3567
3912
  className: `
3568
3913
  absolute top-3 left-3 z-50
3569
- w-9 h-9 rounded-lg
3570
- bg-black/70 backdrop-blur-sm
3571
- border-2 border-white/50
3572
- shadow-md
3573
- flex items-center justify-center
3574
- transition-all duration-200
3575
- hover:bg-white/20 hover:border-white
3914
+ px-3 py-2 rounded-xl
3915
+ bg-black/80 backdrop-blur-md
3916
+ border-2 border-white/60
3917
+ shadow-xl
3918
+ flex items-center gap-2
3919
+ transition-all duration-300
3920
+ hover:bg-white/20 hover:border-white hover:scale-105
3576
3921
  active:scale-95
3577
3922
  disabled:opacity-40 disabled:cursor-not-allowed
3578
3923
  touch-manipulation
3924
+ ${pulse ? "animate-pulse border-green-400/80" : ""}
3579
3925
  `,
3580
- "aria-label": "Enter fullscreen",
3581
- title: "Enter fullscreen",
3582
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, { size: 16, className: "text-white/80" })
3926
+ "aria-label": "Tap for fullscreen controls",
3927
+ children: [
3928
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { size: 18, className: "text-green-400" }),
3929
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider whitespace-nowrap", children: "Tap for Controls" }),
3930
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, { size: 14, className: "text-white/60" })
3931
+ ]
3583
3932
  }
3584
3933
  );
3585
3934
  }
@@ -5906,16 +6255,16 @@ var SYSTEMS = [
5906
6255
  },
5907
6256
  {
5908
6257
  key: "ARCADE",
5909
- label: "Arcade (MAME)",
6258
+ label: "Arcade (FBNeo)",
5910
6259
  fullName: "Arcade",
5911
6260
  slug: "arcade",
5912
- extensions: [],
5913
- core: "mame2003_plus",
5914
- dbNames: ["Arcade", "MAME"],
6261
+ extensions: [".zip"],
6262
+ core: "fbneo",
6263
+ dbNames: ["Arcade", "MAME", "FBNeo", "FinalBurn Neo"],
5915
6264
  iconName: "ARCADE",
5916
6265
  color: "group-hover:text-[#D500F9]",
5917
6266
  accentHex: "#D500F9",
5918
- aliases: ["MAME", "CPS1", "CPS2", "CPS3"],
6267
+ aliases: ["MAME", "CPS1", "CPS2", "CPS3", "FBNEO", "FINALBURN", "SYSTEM16", "SEGA SYSTEM 16"],
5919
6268
  biosNeeded: true
5920
6269
  },
5921
6270
  {
@@ -7895,7 +8244,7 @@ function useGamePlayer(props) {
7895
8244
  recordingSupported
7896
8245
  };
7897
8246
  }
7898
- var STORAGE_KEY2 = "koin-player-settings";
8247
+ var STORAGE_KEY3 = "koin-player-settings";
7899
8248
  var DEFAULT_SETTINGS = {
7900
8249
  volume: 1,
7901
8250
  muted: false,
@@ -7908,7 +8257,7 @@ function usePlayerPersistence(onSettingsChange) {
7908
8257
  const [isLoaded, setIsLoaded] = React2.useState(false);
7909
8258
  React2.useEffect(() => {
7910
8259
  try {
7911
- const stored = localStorage.getItem(STORAGE_KEY2);
8260
+ const stored = localStorage.getItem(STORAGE_KEY3);
7912
8261
  if (stored) {
7913
8262
  const parsed = JSON.parse(stored);
7914
8263
  setSettings((prev) => ({ ...prev, ...parsed }));
@@ -7922,7 +8271,7 @@ function usePlayerPersistence(onSettingsChange) {
7922
8271
  setSettings((prev) => {
7923
8272
  const next = { ...prev, ...updates };
7924
8273
  try {
7925
- localStorage.setItem(STORAGE_KEY2, JSON.stringify(next));
8274
+ localStorage.setItem(STORAGE_KEY3, JSON.stringify(next));
7926
8275
  } catch (e) {
7927
8276
  console.error("Failed to save player settings", e);
7928
8277
  }
@@ -9143,7 +9492,7 @@ exports.CONSOLE_KEYBOARD_OVERRIDES = CONSOLE_KEYBOARD_OVERRIDES;
9143
9492
  exports.DEFAULT_CONTROLS = DEFAULT_CONTROLS;
9144
9493
  exports.DEFAULT_GAMEPAD = DEFAULT_GAMEPAD;
9145
9494
  exports.DEFAULT_KEYBOARD = DEFAULT_KEYBOARD;
9146
- exports.DPAD_BUTTONS = DPAD_BUTTONS;
9495
+ exports.DPAD_BUTTONS = DPAD_BUTTONS2;
9147
9496
  exports.FACE_BUTTONS = FACE_BUTTONS;
9148
9497
  exports.GamePlayer = GamePlayer_default;
9149
9498
  exports.KoinI18nProvider = KoinI18nProvider;