sataruz-captcha 0.1.0

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 ADDED
@@ -0,0 +1,1043 @@
1
+ import { useState, useMemo, useRef, useEffect } from 'react';
2
+ import { useReducedMotion, MotionConfig, motion, AnimatePresence } from 'motion/react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/SataruzCaptcha.tsx
6
+
7
+ // src/toys.ts
8
+ var TOY_META = {
9
+ cao_yanbing: { label: "Cao Yanbing", accent: "#C4A055" },
10
+ irelia: { label: "Irelia", accent: "#7B68EE" },
11
+ titan: { label: "Titan", accent: "#A9A9A9" },
12
+ frok: { label: "Frok", accent: "#32CD32" },
13
+ uriel: { label: "Uriel", accent: "#FF6347" },
14
+ jormungandr: { label: "Jormungandr", accent: "#228B22" },
15
+ netherworld: { label: "Netherworld", accent: "#2F4F4F" },
16
+ zhuqing: { label: "Zhuqing", accent: "#FFD700" },
17
+ nine: { label: "9", accent: "#4169E1" },
18
+ samael: { label: "Samael", accent: "#DC143C" },
19
+ ratu_salju: { label: "Ratu Salju", accent: "#B0E0E6" },
20
+ qingluan: { label: "Qingluan", accent: "#FF69B4" }
21
+ };
22
+
23
+ // src/clawArt.ts
24
+ var CLAW_PIVOT = { x: 20.6, y: 22.7 };
25
+ var CLAW_BODY = [{ "fill": "#666D85", "d": "M20.031,24.324c0.086,2.939,0.263,5.874,0.531,8.802c0.016,0.175,0.073,0.395,0.247,0.42\r\n c0.218,0.03,0.311-0.267,0.318-0.487c0.104-2.988,0.207-5.975,0.311-8.963C21.048,24.142,20.657,24.188,20.031,24.324z" }, { "fill": "#3D4459", "d": "M20.222,11.816c0.023,2.291,0.046,4.583,0.069,6.874c0.356,0.036,0.717,0.036,1.073,0\r\n c-0.01-2.339-0.019-4.679-0.028-7.018C20.987,11.656,20.636,11.679,20.222,11.816z" }, { "fill": "#666D85", "d": "M18.924,10.588c0.013,0.395,0.025,0.789,0.038,1.184c0.003,0.101,0.009,0.209,0.073,0.288\r\n c0.084,0.103,0.234,0.116,0.367,0.12c1.017,0.034,2.044,0.068,3.047-0.106c0.054-0.009,0.111-0.021,0.15-0.059\r\n c0.047-0.047,0.054-0.12,0.058-0.187c0.022-0.45,0.009-0.901-0.039-1.349c-0.004-0.04-0.011-0.083-0.04-0.11\r\n c-0.031-0.03-0.079-0.031-0.122-0.031C21.186,10.343,19.916,10.39,18.924,10.588z" }, { "fill": "#DE2121", "d": "M18.282,9.052c-0.062,0-0.13,0.002-0.179,0.04c-0.059,0.045-0.074,0.126-0.084,0.199\r\n c-0.055,0.411-0.06,0.83-0.015,1.242c0.008,0.074,0.02,0.153,0.071,0.208c0.066,0.07,0.172,0.077,0.268,0.079\r\n c1.573,0.031,3.146,0.006,4.717-0.076c0.064-0.003,0.132-0.008,0.185-0.045c0.096-0.066,0.102-0.204,0.098-0.321\r\n c-0.015-0.438-0.031-0.875-0.046-1.313c-0.001-0.039-0.004-0.081-0.031-0.109c-0.029-0.03-0.076-0.032-0.119-0.032\r\n C22.905,8.925,19.701,9.046,18.282,9.052z" }, { "fill": "#666D85", "d": "M19.815,20.079c-0.001-0.526-0.003-1.052-0.004-1.577c0-0.036,0-0.074,0.017-0.105\r\n c0.033-0.061,0.112-0.077,0.18-0.085c0.483-0.058,0.97-0.086,1.456-0.083c0.099,0.001,0.21,0.008,0.274,0.083\r\n c0.053,0.062,0.055,0.151,0.055,0.233c0,0.475-0.001,0.95-0.001,1.425C21.155,19.978,20.517,19.972,19.815,20.079z" }, { "fill": "#B4B9C4", "d": "M19.458,20.771c-0.269,0.791-0.539,1.583-0.808,2.374c-0.016,0.048-0.033,0.098-0.022,0.147\r\n c0.01,0.047,0.045,0.085,0.077,0.121l0.799,0.87c0.083,0.091,0.169,0.184,0.28,0.238c0.134,0.066,0.289,0.068,0.438,0.069\r\n c0.471,0.003,0.942,0.006,1.413,0.01c0.048,0,0.098,0,0.141-0.021c0.052-0.026,0.085-0.078,0.115-0.127\r\n c0.244-0.399,0.487-0.797,0.731-1.196c0.047-0.078,0.096-0.159,0.106-0.249c0.011-0.101-0.027-0.199-0.065-0.293\r\n c-0.231-0.58-0.463-1.159-0.695-1.739c-0.029-0.072-0.061-0.149-0.124-0.194c-0.074-0.053-0.172-0.051-0.262-0.047\r\n C20.836,20.765,20.09,20.808,19.458,20.771z" }, { "fill": "#3D4459", "d": "M21.101,22.267c-0.117-0.169-0.332-0.237-0.531-0.186c-0.182,0.047-0.359,0.12-0.459,0.274\r\n c-0.076,0.116-0.095,0.259-0.101,0.397c-0.004,0.095-0.002,0.192,0.034,0.28c0.061,0.149,0.214,0.245,0.372,0.274\r\n c0.254,0.047,0.53-0.066,0.678-0.278c0.147-0.212,0.158-0.51,0.025-0.732C21.114,22.286,21.107,22.276,21.101,22.267z" }, { "fill": "#DE2121", "d": "M22.585,19.777c-1.368-0.115-2.751-0.055-4.105,0.179c-0.127,0.022-0.272,0.059-0.327,0.176\r\n c-0.028,0.06-0.025,0.129-0.022,0.195c0.009,0.173,0.017,0.345,0.026,0.518c0.003,0.058,0.007,0.118,0.037,0.167\r\n c0.053,0.084,0.164,0.106,0.262,0.118c1.395,0.173,2.808,0.083,4.21-0.007c0.351-0.023,0.703-0.045,1.047-0.121\r\n c0.053-0.012,0.109-0.026,0.145-0.066c0.044-0.048,0.049-0.119,0.05-0.184c0.003-0.209-0.01-0.418-0.039-0.625\r\n c-0.01-0.07-0.025-0.145-0.076-0.193c-0.048-0.045-0.118-0.055-0.183-0.064C23.273,19.826,22.935,19.806,22.585,19.777z" }, { "fill": "#851313", "d": "M20.275,21.326c-0.53,0-1.06-0.021-1.589-0.064c-0.113-0.009-0.267-0.021-0.391-0.104\r\n c-0.281-0.187-0.251-0.584-0.204-0.866c0.007-0.041,0.046-0.068,0.086-0.061c0.041,0.007,0.068,0.045,0.062,0.086\r\n c-0.064,0.383-0.02,0.611,0.139,0.716c0.085,0.056,0.195,0.069,0.32,0.079c1.626,0.13,3.266,0.058,4.875-0.214\r\n c0.041-0.007,0.079,0.021,0.086,0.061c0.007,0.041-0.021,0.08-0.061,0.087C22.501,21.233,21.388,21.326,20.275,21.326z" }, { "fill": "#1F2229", "d": "M20.376,17.8c-0.04,0-0.074-0.032-0.075-0.073l-0.152-5.054c-0.001-0.041,0.031-0.076,0.073-0.077\r\n c0.042,0,0.076,0.031,0.077,0.073l0.152,5.054c0.001,0.042-0.031,0.076-0.073,0.077C20.377,17.8,20.376,17.8,20.376,17.8z" }, { "fill": "#851313", "d": "M17.993,10.472c-0.041,0-0.075-0.033-0.075-0.075l-0.001-0.949c0-0.109,0-0.258,0.087-0.372\r\n c0.126-0.164,0.364-0.165,0.492-0.166l4.541-0.022c0.088-0.001,0.179,0.004,0.25,0.068c0.089,0.081,0.087,0.211,0.086,0.282\r\n l-0.018,1.146c-0.001,0.042-0.033,0.074-0.076,0.074c-0.042-0.001-0.075-0.035-0.074-0.076l0.018-1.146\r\n c0.001-0.064-0.001-0.136-0.037-0.168c-0.031-0.028-0.087-0.03-0.148-0.03l-4.541,0.022c-0.141,0.001-0.298,0.01-0.374,0.108\r\n c-0.052,0.068-0.056,0.167-0.056,0.28l0.001,0.949C18.068,10.438,18.035,10.472,17.993,10.472\r\n C17.993,10.472,17.993,10.472,17.993,10.472z" }];
26
+ var CLAW_ARM_L = [{ "fill": "#808596", "d": "M18.965,20.803c-1.243,0.927-2.486,1.854-3.729,2.781c-0.176,0.131-0.363,0.279-0.417,0.491\r\n c-0.049,0.192,0.023,0.391,0.089,0.577c0.681,1.927,0.824,3.999,1.323,5.981c0.055,0.217,0.118,0.441,0.261,0.613\r\n c0.142,0.17,0.348,0.272,0.542,0.38c0.641,0.359,1.208,0.848,1.658,1.429c0.141,0.182,0.283,0.383,0.499,0.463\r\n c0.216,0.08,0.52-0.043,0.533-0.249c-0.459-0.883-1.167-1.635-2.02-2.147c-0.152-0.091-0.313-0.179-0.413-0.325\r\n c-0.081-0.119-0.113-0.263-0.142-0.404c-0.348-1.641-0.697-3.281-1.045-4.922c-0.102-0.478,0.135-0.964,0.574-1.178\r\n c0.616-0.3,1.225-0.615,1.827-0.943c0.325-0.177,0.656-0.367,0.882-0.66c0.274-0.355,0.358-0.817,0.433-1.259\r\n c0.029-0.17,0.057-0.35-0.007-0.51c-0.096-0.238-0.378-0.358-0.635-0.345C18.921,20.591,18.683,20.709,18.965,20.803z" }, { "fill": "#3D4459", "d": "M19.335,32.718c-0.024,0-0.047-0.011-0.062-0.032c-0.347-0.498-0.802-0.934-1.314-1.26\r\n c-0.033-0.021-0.067-0.042-0.101-0.063c-0.213-0.132-0.432-0.268-0.574-0.485c-0.11-0.169-0.158-0.367-0.2-0.543\r\n c-0.391-1.627-0.752-3.283-1.073-4.922c-0.032-0.163-0.076-0.387,0.009-0.583c0.093-0.216,0.315-0.338,0.477-0.427l1.805-0.99\r\n c0.036-0.02,0.082-0.007,0.102,0.03c0.02,0.036,0.007,0.082-0.03,0.102l-1.805,0.99c-0.185,0.102-0.344,0.199-0.412,0.355\r\n c-0.061,0.141-0.036,0.307,0.001,0.494c0.321,1.638,0.681,3.292,1.072,4.916c0.039,0.163,0.084,0.349,0.18,0.496\r\n c0.123,0.188,0.328,0.316,0.527,0.439c0.034,0.021,0.068,0.042,0.102,0.064c0.529,0.336,0.998,0.786,1.357,1.3\r\n c0.024,0.034,0.015,0.081-0.019,0.104C19.365,32.714,19.35,32.718,19.335,32.718z" }];
27
+ var CLAW_ARM_R = [{ "fill": "#808596", "d": "M22.64,20.803c1.243,0.927,2.486,1.854,3.729,2.781c0.176,0.131,0.363,0.279,0.417,0.491\r\n c0.049,0.192-0.023,0.391-0.089,0.577c-0.681,1.927-0.824,3.999-1.323,5.981c-0.055,0.217-0.118,0.441-0.261,0.613\r\n c-0.142,0.17-0.348,0.272-0.542,0.38c-0.641,0.359-1.208,0.848-1.658,1.429c-0.141,0.182-0.284,0.383-0.499,0.463\r\n c-0.216,0.08-0.52-0.043-0.533-0.249c0.459-0.883,1.167-1.635,2.02-2.147c0.152-0.091,0.313-0.179,0.413-0.325\r\n c0.081-0.119,0.113-0.263,0.142-0.404c0.348-1.641,0.697-3.281,1.045-4.922c0.101-0.478-0.135-0.964-0.574-1.178\r\n c-0.616-0.3-1.225-0.615-1.827-0.943c-0.324-0.177-0.656-0.367-0.882-0.66c-0.274-0.355-0.358-0.817-0.433-1.259\r\n c-0.029-0.17-0.057-0.35,0.007-0.51c0.096-0.238,0.378-0.358,0.635-0.345C22.684,20.591,22.922,20.709,22.64,20.803z" }, { "fill": "#3D4459", "d": "M24.41,30.372c-0.007,0-0.014-0.001-0.021-0.003c-0.04-0.012-0.063-0.053-0.051-0.093\r\n c0.464-1.586,0.83-3.218,1.089-4.85c0.007-0.041,0.045-0.069,0.086-0.062c0.041,0.007,0.069,0.045,0.062,0.086\r\n c-0.26,1.639-0.627,3.277-1.093,4.868C24.472,30.351,24.442,30.372,24.41,30.372z" }, { "fill": "#3D4459", "d": "M22.613,33.409c-0.017,0-0.034-0.006-0.048-0.017c-0.032-0.027-0.036-0.074-0.009-0.106\r\n c0.565-0.675,1.234-1.247,1.99-1.698l0.055-0.033c0.18-0.107,0.365-0.217,0.488-0.377c0.142-0.186,0.197-0.436,0.244-0.656\r\n l1.14-5.256c0.009-0.04,0.049-0.066,0.089-0.057c0.041,0.009,0.066,0.049,0.057,0.089l-1.14,5.256\r\n c-0.051,0.236-0.109,0.503-0.272,0.715c-0.141,0.184-0.339,0.301-0.531,0.415l-0.055,0.032c-0.741,0.443-1.398,1.003-1.952,1.666\r\n C22.656,33.399,22.635,33.409,22.613,33.409z" }];
28
+ var GW = 380;
29
+ var GH = 320;
30
+ var RAIL_Y = 14;
31
+ var HOME_Y = 64;
32
+ var DROP_Y = 198;
33
+ var CLAW_MIN = 46;
34
+ var CLAW_MAX = 334;
35
+ var COIL_LEN = 50;
36
+ var GRAB_RADIUS = 38;
37
+ var GRIP_OFFSET = 46;
38
+ var TRAY = { cx: 232, cy: GH + 56, min: 150, max: 320 };
39
+ var TOY_SET = [
40
+ { toy: "cao_yanbing", w: 90 },
41
+ { toy: "irelia", w: 88 },
42
+ { toy: "titan", w: 94 },
43
+ { toy: "frok", w: 82 },
44
+ { toy: "uriel", w: 92 },
45
+ { toy: "jormungandr", w: 98 },
46
+ { toy: "netherworld", w: 85 },
47
+ { toy: "zhuqing", w: 86 },
48
+ { toy: "nine", w: 76 },
49
+ { toy: "samael", w: 89 },
50
+ { toy: "ratu_salju", w: 84 },
51
+ { toy: "qingluan", w: 80 }
52
+ ];
53
+ var rand = (a, b) => a + Math.random() * (b - a);
54
+ var CONFETTI = [
55
+ { dx: -44, dy: -54, dr: -150, c: "#34c759", d: 0 },
56
+ { dx: -30, dy: -66, dr: 120, c: "#ffd60a", d: 0.05 },
57
+ { dx: -14, dy: -76, dr: -80, c: "#5cd679", d: 0.02 },
58
+ { dx: 2, dy: -80, dr: 60, c: "#5a93c9", d: 0.07 },
59
+ { dx: 16, dy: -74, dr: -130, c: "#ffb340", d: 0.03 },
60
+ { dx: 30, dy: -64, dr: 100, c: "#a8e6b8", d: 0.06 },
61
+ { dx: 44, dy: -52, dr: -110, c: "#34c759", d: 0.01 },
62
+ { dx: -54, dy: -36, dr: 90, c: "#e58ab0", d: 0.09 },
63
+ { dx: 54, dy: -34, dr: -70, c: "#5a93c9", d: 0.08 }
64
+ ];
65
+ var easeInQuad = (p) => p * p;
66
+ var easeOutCubic = (p) => 1 - Math.pow(1 - p, 3);
67
+ var easeInOutCubic = (p) => p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
68
+ var clamp01 = (p) => Math.min(1, Math.max(0, p));
69
+ var T = {
70
+ antic: 0.16,
71
+ // tiny upward anticipation before the dive
72
+ down: 0.78,
73
+ // cable pays out, accelerating
74
+ dwell1: 0.18,
75
+ // momentum carries the claw a touch past the stop, then settles
76
+ close: 0.45,
77
+ // fingers close, decelerating
78
+ dwell2: 0.26,
79
+ // grip settles before the lift
80
+ load: 0.24,
81
+ // the cable takes the toy's weight: a visible strain dip
82
+ up: 0.95,
83
+ // slow ease-in-out retract
84
+ open: 0.4
85
+ // fingers release over the tray
86
+ };
87
+ var ANTIC_RISE = 8;
88
+ var DROP_G = 1150;
89
+ var ENTRANCE_G = 1500;
90
+ var shuffle = (arr) => {
91
+ const a = [...arr];
92
+ for (let i = a.length - 1; i > 0; i--) {
93
+ const j = Math.floor(Math.random() * (i + 1));
94
+ [a[i], a[j]] = [a[j], a[i]];
95
+ }
96
+ return a;
97
+ };
98
+ function scatterPile(target) {
99
+ const order = shuffle(TOY_SET);
100
+ const rest = order.filter((t) => t.toy !== target);
101
+ const tgt = order.find((t) => t.toy === target);
102
+ const nB = 8;
103
+ const nTop = order.length - nB;
104
+ const bottomIdx = 2 + Math.floor(Math.random() * 4);
105
+ const slots = new Array(order.length);
106
+ let r = 0;
107
+ const frontW = [];
108
+ const frontToy = [];
109
+ for (let i = 0; i < nB; i++) {
110
+ const isTarget = i === bottomIdx;
111
+ const t = isTarget ? tgt : rest[r++];
112
+ frontToy.push(t);
113
+ frontW.push(t.w * (isTarget ? rand(1, 1.05) : rand(0.76, 0.86)));
114
+ }
115
+ const xs = [0];
116
+ for (let i = 1; i < nB; i++) {
117
+ xs.push(xs[i - 1] + (frontW[i - 1] + frontW[i]) / 2 * rand(0.62, 0.68));
118
+ }
119
+ const span = xs[nB - 1];
120
+ const fit = Math.min(1, (GW - 80) / span);
121
+ for (let i = 0; i < nB; i++) frontW[i] *= fit;
122
+ const offset = (GW - span * fit) / 2;
123
+ const centers = [];
124
+ for (let i = 0; i < nB; i++) {
125
+ const cx = offset + xs[i] * fit + rand(-3, 3);
126
+ centers.push(cx);
127
+ const isTarget = i === bottomIdx;
128
+ slots[i] = {
129
+ toy: frontToy[i].toy,
130
+ w: frontW[i],
131
+ x: Math.min(GW - 26, Math.max(26, cx)),
132
+ b: rand(0, 2),
133
+ // planted on the floor, not hovering over it
134
+ z: isTarget ? 4 : 2,
135
+ // target drawn in front of its neighbours
136
+ rot: rand(-7, 7),
137
+ dropFrom: -rand(340, 440),
138
+ delay: i * 0.05 + rand(0, 0.1)
139
+ };
140
+ }
141
+ const gaps = [];
142
+ for (let g = 0; g < nB - 1; g++) {
143
+ if (g === bottomIdx - 1 || g === bottomIdx) continue;
144
+ gaps.push(g);
145
+ }
146
+ const useGaps = shuffle(gaps).slice(0, nTop);
147
+ let ti = 0;
148
+ for (const g of useGaps) {
149
+ const t = rest[r++];
150
+ const cx = (centers[g] + centers[g + 1]) / 2 + rand(-3, 3);
151
+ slots[nB + ti] = {
152
+ toy: t.toy,
153
+ w: t.w * rand(0.7, 0.8),
154
+ x: Math.min(GW - 26, Math.max(26, cx)),
155
+ b: rand(6, 16),
156
+ // low: peeking over shoulders, base out of sight
157
+ z: 1,
158
+ // BEHIND the floor row
159
+ rot: rand(-8, 8),
160
+ dropFrom: -rand(360, 470),
161
+ delay: 0.45 + ti * 0.08 + rand(0, 0.1)
162
+ // settle in after the floor row
163
+ };
164
+ ti++;
165
+ }
166
+ const tip = slots[nB + Math.floor(Math.random() * nTop)];
167
+ tip.rot = rand(10, 16) * (Math.random() < 0.5 ? -1 : 1);
168
+ return slots;
169
+ }
170
+ function SataruzCaptcha({
171
+ target: targetProp,
172
+ onVerify,
173
+ title = "Verify you're human",
174
+ assetBase = "/sataruz/",
175
+ className
176
+ }) {
177
+ const reduce = useReducedMotion();
178
+ const [autoTarget] = useState(() => TOY_SET[Math.floor(Math.random() * TOY_SET.length)].toy);
179
+ const target = targetProp ?? autoTarget;
180
+ const [phase, setPhase] = useState("idle");
181
+ const [infoOpen, setInfoOpen] = useState(false);
182
+ const [verified, setVerified] = useState(false);
183
+ const [message, setMessage] = useState(null);
184
+ const [overTray, setOverTray] = useState(false);
185
+ const [trayMode, setTrayMode] = useState("");
186
+ const pile = useMemo(() => scatterPile(target), [target]);
187
+ const rigEl = useRef(null);
188
+ const clawEl = useRef(null);
189
+ const coilEl = useRef(null);
190
+ const fingerL = useRef(null);
191
+ const fingerR = useRef(null);
192
+ const carriedEl = useRef(null);
193
+ const stickEl = useRef(null);
194
+ const trolleyEl = useRef(null);
195
+ const shadowEl = useRef(null);
196
+ const machineEl = useRef(null);
197
+ const trayEl = useRef(null);
198
+ const pileEls = useRef([]);
199
+ const dir = useRef(0);
200
+ const phaseRef = useRef("idle");
201
+ const onVerifyRef = useRef(onVerify);
202
+ onVerifyRef.current = onVerify;
203
+ const sim = useRef({
204
+ x: GW / 2,
205
+ y: HOME_Y,
206
+ vx: 0,
207
+ drive: 0,
208
+ // smoothed steering input (eases abrupt key/stick changes)
209
+ sway: 0,
210
+ swayV: 0,
211
+ breeze: 0,
212
+ // sub-degree ambient sway so the rig never freezes solid
213
+ close: 0,
214
+ carried: -1,
215
+ carry: { x: 0, y: 0 },
216
+ // scripted-sequence bookkeeping
217
+ stage: "",
218
+ st: 0,
219
+ // seconds inside the current stage
220
+ depthY: DROP_Y,
221
+ // how far the cable pays out THIS grab (reaches the toy's head)
222
+ fallV: 0,
223
+ stretch: 0,
224
+ // 0..~0.09 vertical stretch while falling fast (squash-and-stretch)
225
+ xrot: 0,
226
+ // extra rotation on the carried toy (pickup tilt / dangle lag / impact)
227
+ swallow: 0,
228
+ // 0..1 shrink+fade as a WRONG toy is dismissed off the lid
229
+ released: false,
230
+ // the claw has let go this drop (one-shot)
231
+ mouthY: 353
232
+ // the hatch rim line in machine space — measured at release time
233
+ });
234
+ const softRef = useRef(null);
235
+ if (softRef.current === null) {
236
+ softRef.current = pile.map((s) => ({
237
+ dx: 0,
238
+ dy: 0,
239
+ rot: 0,
240
+ sq: 0,
241
+ vdx: 0,
242
+ vdy: 0,
243
+ vrot: 0,
244
+ vsq: 0,
245
+ ey: s.dropFrom,
246
+ evy: 0,
247
+ delay: s.delay,
248
+ landed: false
249
+ }));
250
+ }
251
+ const setPhaseBoth = (p) => {
252
+ phaseRef.current = p;
253
+ setPhase(p);
254
+ };
255
+ const api = useRef({ setPhaseBoth, setMessage, setVerified, setOverTray, setTrayMode });
256
+ api.current = { setPhaseBoth, setMessage, setVerified, setOverTray, setTrayMode };
257
+ const targetIdx = useMemo(() => pile.findIndex((p) => p.toy === target), [pile, target]);
258
+ useEffect(() => {
259
+ const s = sim.current;
260
+ const soft = softRef.current;
261
+ let raf = 0;
262
+ let wasOverTray = false;
263
+ let prevNow = 0;
264
+ const speedMul = reduce ? 2.4 : 1;
265
+ if (reduce) {
266
+ soft.forEach((b) => {
267
+ b.ey = 0;
268
+ b.landed = true;
269
+ });
270
+ }
271
+ const toyCenter = (i) => {
272
+ const p = pile[i];
273
+ return { x: p.x, y: GH - p.b - p.w / 2 * 0.92 };
274
+ };
275
+ const ripple = (x, power, except = -1) => {
276
+ pile.forEach((p, i) => {
277
+ if (i === except || i === s.carried) return;
278
+ const d = Math.abs(p.x - x);
279
+ if (d < 80) {
280
+ const f = (1 - d / 80) * power;
281
+ const side = p.x < x ? -1 : 1;
282
+ const b = soft[i];
283
+ b.vdx += side * f * 1.6;
284
+ b.vdy -= f * 1.1;
285
+ b.vrot += side * f * 2;
286
+ b.vsq += f * 0.02;
287
+ }
288
+ });
289
+ };
290
+ const pend = () => {
291
+ const len = Math.max(2, s.y - RAIL_Y);
292
+ const rad = reduce ? 0 : (s.sway + s.breeze) * Math.PI / 180;
293
+ return { ex: s.x + Math.sin(rad) * len, ey: RAIL_Y + Math.cos(rad) * len };
294
+ };
295
+ const gripY = (ey) => ey + GRIP_OFFSET + (s.carried >= 0 ? pile[s.carried].w : 80) / 2;
296
+ const candidateAt = (x) => {
297
+ let best = -1;
298
+ let bestScore = -Infinity;
299
+ pile.forEach((q, i) => {
300
+ const d = Math.abs(q.x - x);
301
+ if (d < GRAB_RADIUS) {
302
+ const score = q.z * 100 - d;
303
+ if (score > bestScore) {
304
+ bestScore = score;
305
+ best = i;
306
+ }
307
+ }
308
+ });
309
+ return best;
310
+ };
311
+ const render = () => {
312
+ const sway = reduce ? 0 : s.sway + s.breeze;
313
+ const len = Math.max(2, s.y - RAIL_Y);
314
+ if (trolleyEl.current) trolleyEl.current.style.transform = `translateX(${(s.x - 14).toFixed(2)}px)`;
315
+ if (shadowEl.current) {
316
+ const rad = sway * Math.PI / 180;
317
+ const ex = s.x + Math.sin(rad) * len;
318
+ const bottomY = s.carried >= 0 ? s.carry.y + pile[s.carried].w / 2 : s.y + 58;
319
+ const t2 = clamp01(1 - (GH - bottomY) / 210);
320
+ shadowEl.current.style.transform = `translateX(${(ex - 45).toFixed(2)}px) scaleX(${(1.25 - 0.5 * t2).toFixed(3)})`;
321
+ shadowEl.current.style.opacity = (0.1 + 0.3 * t2).toFixed(3);
322
+ }
323
+ if (rigEl.current) {
324
+ const totalH = len + 70;
325
+ rigEl.current.setAttribute("viewBox", `0 0 36 ${totalH.toFixed(1)}`);
326
+ rigEl.current.setAttribute("height", totalH.toFixed(1));
327
+ rigEl.current.style.transform = `translateX(${s.x.toFixed(2)}px) rotate(${sway.toFixed(2)}deg)`;
328
+ }
329
+ coilEl.current?.setAttribute("transform", `translate(9 0) scale(1 ${(len / 100).toFixed(4)})`);
330
+ clawEl.current?.setAttribute("transform", `translate(18 ${len.toFixed(2)}) scale(2.5) translate(-20.6 -8.9)`);
331
+ const pinch = 15 * s.close;
332
+ fingerL.current?.setAttribute("transform", `rotate(${pinch.toFixed(2)} ${CLAW_PIVOT.x} ${CLAW_PIVOT.y})`);
333
+ fingerR.current?.setAttribute("transform", `rotate(${(-pinch).toFixed(2)} ${CLAW_PIVOT.x} ${CLAW_PIVOT.y})`);
334
+ for (let i = 0; i < pile.length; i++) {
335
+ const el = pileEls.current[i];
336
+ if (!el) continue;
337
+ const b = soft[i];
338
+ el.style.transform = `translate(${b.dx.toFixed(2)}px, ${(b.dy + b.ey).toFixed(2)}px) rotate(${(pile[i].rot + b.rot).toFixed(2)}deg) scale(${(1 - b.sq * 0.6).toFixed(3)}, ${(1 + b.sq).toFixed(3)})`;
339
+ }
340
+ if (s.carried >= 0 && carriedEl.current) {
341
+ const w = pile[s.carried].w;
342
+ const sc = 1 - s.swallow * 0.78;
343
+ const sx = (sc * (1 - s.stretch * 0.55)).toFixed(3);
344
+ const sy = (sc * (1 + s.stretch)).toFixed(3);
345
+ carriedEl.current.style.transform = `translate(${s.carry.x - w / 2}px, ${s.carry.y - w / 2}px) rotate(${(sway + s.xrot).toFixed(2)}deg) scale(${sx}, ${sy})`;
346
+ if (s.swallow > 0) carriedEl.current.style.opacity = (1 - s.swallow).toFixed(2);
347
+ }
348
+ };
349
+ const stageP = (dur, dt) => {
350
+ s.st += dt;
351
+ return clamp01(s.st / dur);
352
+ };
353
+ const nextStage = (st) => {
354
+ s.stage = st;
355
+ s.st = 0;
356
+ };
357
+ const step = (now) => {
358
+ const dtRaw = prevNow ? Math.min(0.04, Math.max(4e-3, (now - prevNow) / 1e3)) : 1 / 60;
359
+ prevNow = now;
360
+ const dt = dtRaw * speedMul;
361
+ const f = dt * 60;
362
+ const ph = phaseRef.current;
363
+ const a = api.current;
364
+ if (!reduce) {
365
+ const lean = ph === "idle" || ph === "carry" ? -s.vx * 0.042 : 0;
366
+ s.swayV += (lean - s.sway) * 0.05 * f;
367
+ s.swayV *= Math.pow(0.93, f);
368
+ s.sway += s.swayV * f;
369
+ s.breeze = Math.sin(now / 1500) * 0.45 + Math.sin(now / 521) * 0.12;
370
+ }
371
+ for (let i = 0; i < soft.length; i++) {
372
+ const b = soft[i];
373
+ if (!b.landed) {
374
+ if (b.delay > 0) {
375
+ b.delay -= dt;
376
+ } else {
377
+ b.evy += ENTRANCE_G * dt;
378
+ b.ey += b.evy * dt;
379
+ if (b.ey >= 0) {
380
+ b.ey = 0;
381
+ b.landed = true;
382
+ b.vsq += Math.min(0.055, b.evy * 6e-5);
383
+ b.vrot += rand(-0.8, 0.8);
384
+ ripple(pile[i].x, Math.min(0.22, b.evy * 2e-4), i);
385
+ }
386
+ }
387
+ }
388
+ b.vdx += -b.dx * 0.055 * f;
389
+ b.vdy += -b.dy * 0.055 * f;
390
+ b.vrot += -b.rot * 0.05 * f;
391
+ b.vsq += -b.sq * 0.13 * f;
392
+ const damp = Math.pow(0.9, f);
393
+ b.vdx *= damp;
394
+ b.vdy *= damp;
395
+ b.vrot *= Math.pow(0.91, f);
396
+ b.vsq *= Math.pow(0.84, f);
397
+ b.dx += b.vdx * f;
398
+ b.dy += b.vdy * f;
399
+ b.rot += b.vrot * f;
400
+ b.sq += b.vsq * f;
401
+ }
402
+ if (ph === "idle" || ph === "carry") {
403
+ s.drive += (dir.current - s.drive) * (1 - Math.exp(-13 * dt));
404
+ s.vx += s.drive * 720 * dt;
405
+ s.vx *= Math.exp(-5.5 * dt);
406
+ s.x = Math.min(CLAW_MAX, Math.max(CLAW_MIN, s.x + s.vx * dt));
407
+ if (ph === "carry") {
408
+ s.xrot += (s.sway * 0.5 - s.xrot) * (1 - Math.exp(-6 * dt));
409
+ const { ex, ey } = pend();
410
+ s.carry.x = ex;
411
+ s.carry.y = gripY(ey);
412
+ const over = s.x >= TRAY.min && s.x <= TRAY.max;
413
+ if (over !== wasOverTray) {
414
+ wasOverTray = over;
415
+ a.setOverTray(over);
416
+ }
417
+ }
418
+ } else if (ph === "seq") {
419
+ if (s.stage === "antic") {
420
+ const p = stageP(T.antic, dt);
421
+ s.y = HOME_Y - ANTIC_RISE * easeOutCubic(p);
422
+ s.close = -0.55 * easeOutCubic(p);
423
+ if (p >= 1) {
424
+ const cand = candidateAt(s.x);
425
+ if (cand >= 0) {
426
+ const c = toyCenter(cand);
427
+ s.depthY = Math.min(GH - 46, Math.max(HOME_Y + 50, c.y - GRIP_OFFSET - pile[cand].w / 2));
428
+ } else {
429
+ s.depthY = DROP_Y;
430
+ }
431
+ nextStage("down");
432
+ }
433
+ } else if (s.stage === "down") {
434
+ const p = stageP(T.down, dt);
435
+ s.y = HOME_Y - ANTIC_RISE + (s.depthY - HOME_Y + ANTIC_RISE) * easeInQuad(p);
436
+ if (s.y > 130 && !reduce) {
437
+ pile.forEach((q, i) => {
438
+ const d = Math.abs(q.x - s.x);
439
+ if (d < 56) {
440
+ const push = (1 - d / 56) * 7 * dt;
441
+ const side = q.x < s.x ? -1 : 1;
442
+ soft[i].vdx += side * push;
443
+ soft[i].vsq += push * 0.012;
444
+ }
445
+ });
446
+ }
447
+ if (p >= 1) nextStage("dwell1");
448
+ } else if (s.stage === "dwell1") {
449
+ const p = stageP(T.dwell1, dt);
450
+ s.y = s.depthY + (reduce ? 0 : 3.5 * Math.sin(Math.PI * p));
451
+ if (p >= 1) {
452
+ s.y = s.depthY;
453
+ nextStage("close");
454
+ }
455
+ } else if (s.stage === "close") {
456
+ const p = stageP(T.close, dt);
457
+ s.close = -0.55 + 1.55 * easeOutCubic(p);
458
+ if (p >= 1) {
459
+ const best = candidateAt(s.x);
460
+ s.carried = best;
461
+ if (best >= 0) {
462
+ s.carry = { ...toyCenter(best) };
463
+ s.xrot = pile[best].rot + soft[best].rot;
464
+ const el = pileEls.current[best];
465
+ if (el) el.style.visibility = "hidden";
466
+ ripple(pile[best].x, 0.35, best);
467
+ if (carriedEl.current) {
468
+ carriedEl.current.src = el?.src ?? carriedEl.current.src;
469
+ carriedEl.current.style.width = `${pile[best].w}px`;
470
+ carriedEl.current.style.visibility = "visible";
471
+ carriedEl.current.style.opacity = "";
472
+ }
473
+ } else {
474
+ ripple(s.x, 0.2);
475
+ }
476
+ nextStage("dwell2");
477
+ }
478
+ } else if (s.stage === "dwell2") {
479
+ if (stageP(T.dwell2, dt) >= 1) nextStage(s.carried >= 0 && !reduce ? "load" : "up");
480
+ if (s.carried >= 0) {
481
+ s.xrot += -s.xrot * (1 - Math.exp(-3.5 * dt));
482
+ const { ex, ey } = pend();
483
+ s.carry.x = ex;
484
+ s.carry.y = gripY(ey);
485
+ }
486
+ } else if (s.stage === "load") {
487
+ const p = stageP(T.load, dt);
488
+ const bell = Math.sin(Math.PI * p);
489
+ s.y = s.depthY + 6 * bell;
490
+ s.close = 1 + 0.12 * bell;
491
+ s.xrot += -s.xrot * (1 - Math.exp(-3.5 * dt));
492
+ const { ex, ey } = pend();
493
+ s.carry.x = ex;
494
+ s.carry.y = gripY(ey);
495
+ if (p >= 1) {
496
+ s.y = s.depthY;
497
+ nextStage("up");
498
+ }
499
+ } else if (s.stage === "up") {
500
+ const p = stageP(T.up, dt);
501
+ s.y = s.depthY + (HOME_Y - s.depthY) * easeInOutCubic(p);
502
+ if (s.carried >= 0) {
503
+ s.xrot += -s.xrot * (1 - Math.exp(-3.5 * dt));
504
+ const { ex, ey } = pend();
505
+ s.carry.x = ex;
506
+ s.carry.y = gripY(ey);
507
+ }
508
+ if (p >= 1) {
509
+ if (s.carried >= 0) {
510
+ a.setPhaseBoth("carry");
511
+ a.setMessage(null);
512
+ } else {
513
+ a.setPhaseBoth("idle");
514
+ a.setMessage("Came up empty. Try again.");
515
+ }
516
+ }
517
+ }
518
+ } else if (ph === "toTray") {
519
+ const right = s.carried === targetIdx;
520
+ if (s.stage === "open") {
521
+ const p = stageP(T.open, dt);
522
+ s.close = 1 - easeOutCubic(p);
523
+ if (s.st > 0.12 && !s.released) {
524
+ s.released = true;
525
+ if (right) {
526
+ a.setTrayMode("open");
527
+ if (reduce) {
528
+ if (carriedEl.current) carriedEl.current.style.visibility = "hidden";
529
+ s.carried = -1;
530
+ a.setOverTray(false);
531
+ a.setTrayMode("win");
532
+ nextStage("beat");
533
+ a.setPhaseBoth("celebrate");
534
+ } else {
535
+ const m = machineEl.current?.getBoundingClientRect();
536
+ const tr = trayEl.current?.getBoundingClientRect();
537
+ if (m && tr) s.mouthY = tr.top - m.top + 2;
538
+ s.fallV = 30;
539
+ }
540
+ } else {
541
+ s.fallV = 40;
542
+ }
543
+ }
544
+ }
545
+ if (right && s.released && s.carried >= 0) {
546
+ s.fallV = Math.min(s.fallV + DROP_G * dt, 460);
547
+ s.carry.y += s.fallV * dt;
548
+ s.carry.x += (TRAY.cx - s.carry.x) * (1 - Math.exp(-2.2 * dt));
549
+ s.xrot += -s.xrot * (1 - Math.exp(-4 * dt));
550
+ s.stretch = Math.abs(s.fallV) / 460 * 0.09;
551
+ const w = pile[s.carried].w;
552
+ const sunk = s.carry.y + w / 2 - s.mouthY;
553
+ if (sunk > 0 && carriedEl.current) {
554
+ carriedEl.current.style.clipPath = `inset(0 0 ${sunk.toFixed(1)}px 0)`;
555
+ carriedEl.current.style.filter = `brightness(${Math.max(0.4, 1 - sunk / w * 0.75).toFixed(3)})`;
556
+ }
557
+ if (sunk >= w + 4) {
558
+ if (carriedEl.current) {
559
+ carriedEl.current.style.visibility = "hidden";
560
+ carriedEl.current.style.clipPath = "";
561
+ carriedEl.current.style.filter = "";
562
+ }
563
+ s.carried = -1;
564
+ a.setOverTray(false);
565
+ a.setTrayMode("win");
566
+ nextStage("beat");
567
+ a.setPhaseBoth("celebrate");
568
+ }
569
+ }
570
+ if (!right && s.fallV !== 0) {
571
+ s.fallV = Math.min(s.fallV + DROP_G * dt, 360);
572
+ s.carry.y += s.fallV * dt;
573
+ s.carry.x += (TRAY.cx - s.carry.x) * (1 - Math.exp(-4 * dt));
574
+ s.xrot += -s.xrot * (1 - Math.exp(-3 * dt));
575
+ s.stretch = Math.abs(s.fallV) / 460 * 0.09;
576
+ if (s.fallV > 0 && s.carry.y >= TRAY.cy) {
577
+ if (s.fallV > 200) {
578
+ s.carry.y = TRAY.cy;
579
+ s.fallV = -s.fallV * 0.28;
580
+ s.xrot += rand(-7, 7);
581
+ } else {
582
+ s.carry.y = TRAY.cy;
583
+ s.fallV = 0;
584
+ s.stretch = 0;
585
+ a.setOverTray(false);
586
+ a.setTrayMode("no");
587
+ a.setMessage(
588
+ `That\u2019s the ${TOY_META[pile[s.carried].toy].label}! Find the ${TOY_META[target].label}.`
589
+ );
590
+ nextStage("beat");
591
+ a.setPhaseBoth("deny");
592
+ }
593
+ }
594
+ }
595
+ } else if (ph === "celebrate") {
596
+ if (s.stage === "beat") {
597
+ if (stageP(0.28, dt) >= 1) {
598
+ nextStage("shine");
599
+ api.current.setVerified(true);
600
+ onVerifyRef.current?.();
601
+ }
602
+ } else if (s.stage === "shine") {
603
+ if (stageP(0.7, dt) >= 1) api.current.setPhaseBoth("done");
604
+ }
605
+ } else if (ph === "deny") {
606
+ if (s.stage === "beat") {
607
+ const p = stageP(0.46, dt);
608
+ s.swallow = easeOutCubic(p);
609
+ s.carry.y -= 46 * dt;
610
+ if (p >= 1) {
611
+ const idx = s.carried;
612
+ api.current.setTrayMode("");
613
+ const el = pileEls.current[idx];
614
+ if (el) el.style.visibility = "";
615
+ if (carriedEl.current) {
616
+ carriedEl.current.style.visibility = "hidden";
617
+ carriedEl.current.style.opacity = "";
618
+ carriedEl.current.style.clipPath = "";
619
+ carriedEl.current.style.filter = "";
620
+ }
621
+ s.swallow = 0;
622
+ const b = soft[idx];
623
+ b.vsq += 0.05;
624
+ b.vrot += rand(-1, 1);
625
+ ripple(pile[idx].x, 0.25, idx);
626
+ s.carried = -1;
627
+ a.setPhaseBoth("idle");
628
+ }
629
+ }
630
+ }
631
+ render();
632
+ raf = requestAnimationFrame(step);
633
+ };
634
+ render();
635
+ raf = requestAnimationFrame(step);
636
+ return () => cancelAnimationFrame(raf);
637
+ }, [reduce, targetIdx, target, pile]);
638
+ const action = () => {
639
+ const s = sim.current;
640
+ if (verified) return;
641
+ if (phaseRef.current === "idle") {
642
+ setMessage(null);
643
+ s.close = 0;
644
+ s.stage = "antic";
645
+ s.st = 0;
646
+ setPhaseBoth("seq");
647
+ } else if (phaseRef.current === "carry") {
648
+ if (s.x >= TRAY.min && s.x <= TRAY.max) {
649
+ if (carriedEl.current) {
650
+ carriedEl.current.style.visibility = "visible";
651
+ carriedEl.current.style.opacity = "";
652
+ carriedEl.current.style.clipPath = "";
653
+ carriedEl.current.style.filter = "";
654
+ }
655
+ s.stage = "open";
656
+ s.st = 0;
657
+ s.fallV = 0;
658
+ s.swallow = 0;
659
+ s.stretch = 0;
660
+ s.released = false;
661
+ setOverTray(false);
662
+ setPhaseBoth("toTray");
663
+ } else {
664
+ setMessage("Move the toy over the drop zone first.");
665
+ }
666
+ }
667
+ };
668
+ const stickDrag = useRef(null);
669
+ const onStickDown = (e) => {
670
+ if (verified) return;
671
+ e.target.setPointerCapture(e.pointerId);
672
+ stickDrag.current = { id: e.pointerId, startX: e.clientX };
673
+ if (stickEl.current) stickEl.current.style.transition = "none";
674
+ };
675
+ const onStickMove = (e) => {
676
+ const d = stickDrag.current;
677
+ if (!d || e.pointerId !== d.id) return;
678
+ const dx = Math.max(-26, Math.min(26, e.clientX - d.startX));
679
+ dir.current = dx / 26;
680
+ if (stickEl.current) stickEl.current.style.transform = `rotate(${(dx * 1.05).toFixed(1)}deg)`;
681
+ };
682
+ const onStickUp = (e) => {
683
+ if (stickDrag.current?.id !== e.pointerId) return;
684
+ stickDrag.current = null;
685
+ dir.current = 0;
686
+ if (stickEl.current) {
687
+ stickEl.current.style.transition = "transform 0.25s cubic-bezier(0.2, 1.6, 0.4, 1)";
688
+ stickEl.current.style.transform = "";
689
+ }
690
+ };
691
+ const onKeyDown = (e) => {
692
+ if (infoOpen) {
693
+ if (e.key === "Escape") setInfoOpen(false);
694
+ return;
695
+ }
696
+ if (verified) return;
697
+ if (e.key === "ArrowLeft") {
698
+ e.preventDefault();
699
+ dir.current = -1;
700
+ } else if (e.key === "ArrowRight") {
701
+ e.preventDefault();
702
+ dir.current = 1;
703
+ } else if ((e.key === " " || e.key === "Enter") && !e.repeat) {
704
+ e.preventDefault();
705
+ action();
706
+ }
707
+ };
708
+ const onKeyUp = (e) => {
709
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") dir.current = 0;
710
+ };
711
+ const t = TOY_META[target];
712
+ const busy = phase !== "idle" && phase !== "carry";
713
+ const stepNo = verified || phase === "carry" || phase === "toTray" || phase === "celebrate" ? 3 : phase === "seq" ? 2 : 1;
714
+ const carried = sim.current.carried;
715
+ const carriedW = carried >= 0 ? pile[carried].w : 80;
716
+ return (
717
+ // app-wide reduced-motion safety net: under prefers-reduced-motion, Motion
718
+ // drops transform/position animation and keeps opacity — meaningful state
719
+ // still reads, nothing slides.
720
+ /* @__PURE__ */ jsx(MotionConfig, { reducedMotion: "user", children: /* @__PURE__ */ jsxs(
721
+ motion.div,
722
+ {
723
+ className: className ? `clawcap ${className}` : "clawcap",
724
+ role: "group",
725
+ "aria-label": "Claw machine verification",
726
+ tabIndex: 0,
727
+ onKeyDown,
728
+ onKeyUp,
729
+ initial: reduce ? false : { opacity: 0, y: 16, scale: 0.985 },
730
+ animate: { opacity: 1, y: 0, scale: 1 },
731
+ transition: { duration: 0.55, ease: [0.2, 0.8, 0.2, 1] },
732
+ children: [
733
+ /* @__PURE__ */ jsxs("header", { className: "clawcap-top", children: [
734
+ /* @__PURE__ */ jsx(
735
+ motion.span,
736
+ {
737
+ className: verified ? "clawcap-shield clawcap-shield--ok" : "clawcap-shield",
738
+ "aria-hidden": "true",
739
+ initial: verified ? { scale: 0.55 } : false,
740
+ animate: { scale: 1 },
741
+ transition: { type: "spring", stiffness: 420, damping: 20 },
742
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 20", width: "14", height: "14", children: [
743
+ /* @__PURE__ */ jsx(
744
+ "path",
745
+ {
746
+ d: "M10 2.2 4 4.6v4.6c0 4 2.6 6.7 6 8.2 3.4-1.5 6-4.2 6-8.2V4.6Z",
747
+ fill: "none",
748
+ stroke: "currentColor",
749
+ strokeWidth: "1.5",
750
+ strokeLinejoin: "round"
751
+ }
752
+ ),
753
+ verified && /* @__PURE__ */ jsx("path", { d: "m7 9.8 2.2 2.2L13.4 7.6", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round" })
754
+ ] })
755
+ },
756
+ verified ? "ok" : "idle"
757
+ ),
758
+ /* @__PURE__ */ jsx(
759
+ "button",
760
+ {
761
+ type: "button",
762
+ className: "clawcap-help",
763
+ "aria-label": "About PlayCaptcha",
764
+ "aria-haspopup": "dialog",
765
+ onClick: () => setInfoOpen(true),
766
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 20", width: "14", height: "14", "aria-hidden": "true", children: [
767
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "7.4", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }),
768
+ /* @__PURE__ */ jsx("path", { d: "M8 8.2c.2-1.2 1-1.9 2.1-1.9 1.2 0 2 .8 2 1.8 0 1.6-2.1 1.7-2.1 3.2", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }),
769
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "13.9", r: "0.9", fill: "currentColor" })
770
+ ] })
771
+ }
772
+ )
773
+ ] }),
774
+ /* @__PURE__ */ jsx(AnimatePresence, { children: infoOpen && /* @__PURE__ */ jsx(
775
+ motion.div,
776
+ {
777
+ className: "clawcap-info",
778
+ role: "dialog",
779
+ "aria-modal": "true",
780
+ "aria-label": "About PlayCaptcha",
781
+ onClick: () => setInfoOpen(false),
782
+ initial: { opacity: 0 },
783
+ animate: { opacity: 1 },
784
+ exit: { opacity: 0 },
785
+ transition: { duration: 0.18 },
786
+ children: /* @__PURE__ */ jsxs(
787
+ motion.div,
788
+ {
789
+ className: "clawcap-info-card",
790
+ onClick: (e) => e.stopPropagation(),
791
+ initial: reduce ? false : { opacity: 0, scale: 0.92, y: 10 },
792
+ animate: { opacity: 1, scale: 1, y: 0 },
793
+ exit: { opacity: 0, scale: 0.97, transition: { duration: 0.13, ease: "easeOut" } },
794
+ transition: { type: "spring", stiffness: 360, damping: 28 },
795
+ children: [
796
+ /* @__PURE__ */ jsx(
797
+ "button",
798
+ {
799
+ type: "button",
800
+ className: "clawcap-info-x",
801
+ "aria-label": "Close",
802
+ onClick: () => setInfoOpen(false),
803
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M5 5l10 10M15 5 5 15", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" }) })
804
+ }
805
+ ),
806
+ /* @__PURE__ */ jsxs("div", { className: "clawcap-info-head", children: [
807
+ /* @__PURE__ */ jsx("span", { className: "clawcap-info-tile", children: /* @__PURE__ */ jsx("img", { src: "/sataruz-captcha.svg", alt: "", "aria-hidden": "true" }) }),
808
+ /* @__PURE__ */ jsxs("h4", { className: "clawcap-info-title", children: [
809
+ "SataruzCaptcha ",
810
+ /* @__PURE__ */ jsx("span", { className: "clawcap-info-ver", children: "v1" })
811
+ ] }),
812
+ /* @__PURE__ */ jsx("p", { className: "clawcap-info-tag", children: "Catch the right toy to prove you\u2019re human." })
813
+ ] }),
814
+ /* @__PURE__ */ jsx("ol", { className: "clawcap-info-list", children: [
815
+ ["Move", /* @__PURE__ */ jsxs(Fragment, { children: [
816
+ "Line the claw up right over your prize \u2014 joystick or ",
817
+ /* @__PURE__ */ jsx("kbd", { children: "\u2190" }),
818
+ " ",
819
+ /* @__PURE__ */ jsx("kbd", { children: "\u2192" })
820
+ ] })],
821
+ ["Grab", /* @__PURE__ */ jsxs(Fragment, { children: [
822
+ "Commit. The claw dives, bites and hauls it up \u2014 red button or ",
823
+ /* @__PURE__ */ jsx("kbd", { children: "Space" })
824
+ ] })],
825
+ ["Drop", /* @__PURE__ */ jsx(Fragment, { children: "Ferry it to the hatch and let go. Wrong toy? Straight back on the pile" })]
826
+ ].map(([label, desc], i) => /* @__PURE__ */ jsxs("li", { children: [
827
+ /* @__PURE__ */ jsx("span", { className: "clawcap-info-n", children: i + 1 }),
828
+ /* @__PURE__ */ jsxs("span", { children: [
829
+ /* @__PURE__ */ jsx("strong", { children: label }),
830
+ /* @__PURE__ */ jsx("span", { className: "clawcap-info-d", children: desc })
831
+ ] })
832
+ ] }, label)) }),
833
+ /* @__PURE__ */ jsx("button", { type: "button", className: "clawcap-info-done", onClick: () => setInfoOpen(false), children: "Got it" })
834
+ ]
835
+ }
836
+ )
837
+ }
838
+ ) }),
839
+ /* @__PURE__ */ jsx("h3", { className: "clawcap-title", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
840
+ motion.span,
841
+ {
842
+ style: { display: "inline-block" },
843
+ initial: { opacity: 0, y: 5 },
844
+ animate: { opacity: 1, y: 0 },
845
+ exit: { opacity: 0, y: -5 },
846
+ transition: { duration: 0.24, ease: "easeOut" },
847
+ children: verified ? "Verified" : title
848
+ },
849
+ verified ? "verified" : "title"
850
+ ) }) }),
851
+ /* @__PURE__ */ jsx("p", { className: "clawcap-sub", "aria-live": "polite", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
852
+ motion.span,
853
+ {
854
+ style: { display: "inline-block" },
855
+ initial: { opacity: 0, y: 4 },
856
+ animate: { opacity: 1, y: 0 },
857
+ exit: { opacity: 0, y: -4 },
858
+ transition: { duration: 0.2, ease: "easeOut" },
859
+ children: verified ? "You\u2019re human. Nice catch." : message ? message : /* @__PURE__ */ jsxs(Fragment, { children: [
860
+ "Use the claw to pick up the",
861
+ " ",
862
+ /* @__PURE__ */ jsxs("em", { style: { color: t.accent }, children: [
863
+ /* @__PURE__ */ jsx("img", { className: "clawcap-sub-toy", src: `${assetBase}${target}.png`, alt: "", draggable: false }),
864
+ t.label
865
+ ] })
866
+ ] })
867
+ },
868
+ verified ? "done" : message ?? "challenge"
869
+ ) }) }),
870
+ /* @__PURE__ */ jsxs("ol", { className: "clawcap-steps", "aria-hidden": "true", children: [
871
+ /* @__PURE__ */ jsx("span", { className: "clawcap-steps-pill", style: { transform: `translateX(${(stepNo - 1) * 100}%)` } }),
872
+ ["Move", "Grab", "Drop"].map((label, i) => /* @__PURE__ */ jsxs("li", { className: stepNo === i + 1 ? "is-active" : void 0, children: [
873
+ /* @__PURE__ */ jsx("span", { className: "clawcap-step-n", children: i + 1 }),
874
+ " ",
875
+ label
876
+ ] }, label))
877
+ ] }),
878
+ /* @__PURE__ */ jsxs("div", { ref: machineEl, className: "clawcap-machine", children: [
879
+ /* @__PURE__ */ jsxs("div", { className: "clawcap-case", children: [
880
+ /* @__PURE__ */ jsxs("div", { className: verified ? "clawcap-glass clawcap-glass--dim" : "clawcap-glass", children: [
881
+ /* @__PURE__ */ jsx("div", { className: "cc-rail" }),
882
+ /* @__PURE__ */ jsx("div", { ref: trolleyEl, className: "cc-trolley", "aria-hidden": "true" }),
883
+ /* @__PURE__ */ jsx("div", { ref: shadowEl, className: "cc-claw-shadow", "aria-hidden": "true" }),
884
+ pile.map((p, i) => /* @__PURE__ */ jsx(
885
+ "img",
886
+ {
887
+ ref: (el) => {
888
+ pileEls.current[i] = el;
889
+ },
890
+ className: "cc-toy",
891
+ src: `${assetBase}${p.toy}.png`,
892
+ alt: "",
893
+ draggable: false,
894
+ style: {
895
+ left: p.x - p.w / 2,
896
+ bottom: p.b,
897
+ width: p.w,
898
+ zIndex: p.z,
899
+ transform: `translateY(${p.dropFrom}px)`,
900
+ transformOrigin: "50% 100%"
901
+ }
902
+ },
903
+ p.toy
904
+ )),
905
+ /* @__PURE__ */ jsx("div", { className: "cc-pile-shadow" }),
906
+ /* @__PURE__ */ jsxs("svg", { ref: rigEl, className: "cc-rig", width: "36", height: COIL_LEN + 70, viewBox: `0 0 36 ${COIL_LEN + 70}`, "aria-hidden": "true", children: [
907
+ /* @__PURE__ */ jsx("g", { ref: coilEl, transform: `translate(9 0) scale(1 ${COIL_LEN / 100})`, children: /* @__PURE__ */ jsx(
908
+ "path",
909
+ {
910
+ d: "M9 0 L9 5 C 15 7.5 15 9.5 9 12 C 3 14.5 3 16.5 9 19 C 15 21.5 15 23.5 9 26 C 3 28.5 3 30.5 9 33 C 15 35.5 15 37.5 9 40 C 3 42.5 3 44.5 9 47 C 15 49.5 15 51.5 9 54 C 3 56.5 3 58.5 9 61 C 15 63.5 15 65.5 9 68 C 3 70.5 3 72.5 9 75 C 15 77.5 15 79.5 9 82 C 3 84.5 3 86.5 9 89 C 15 91.5 15 93.5 9 95 L 9 100",
911
+ fill: "none",
912
+ stroke: "#9A9FA8",
913
+ strokeWidth: "2.2",
914
+ strokeLinecap: "round",
915
+ strokeLinejoin: "round",
916
+ vectorEffect: "non-scaling-stroke"
917
+ }
918
+ ) }),
919
+ /* @__PURE__ */ jsxs("g", { ref: clawEl, transform: `translate(18 ${COIL_LEN}) scale(2.5) translate(-20.6 -8.9)`, children: [
920
+ /* @__PURE__ */ jsx("g", { ref: fingerL, children: CLAW_ARM_L.map((p, i) => /* @__PURE__ */ jsx("path", { fill: p.fill, d: p.d }, i)) }),
921
+ /* @__PURE__ */ jsx("g", { ref: fingerR, children: CLAW_ARM_R.map((p, i) => /* @__PURE__ */ jsx("path", { fill: p.fill, d: p.d }, i)) }),
922
+ CLAW_BODY.map((p, i) => /* @__PURE__ */ jsx("path", { fill: p.fill, d: p.d }, i))
923
+ ] })
924
+ ] }),
925
+ /* @__PURE__ */ jsx("div", { className: "cc-glass-shine" })
926
+ ] }),
927
+ /* @__PURE__ */ jsxs("div", { className: "clawcap-panel", children: [
928
+ /* @__PURE__ */ jsxs(
929
+ "div",
930
+ {
931
+ className: "cc-joy",
932
+ role: "slider",
933
+ "aria-label": "Move the claw",
934
+ "aria-valuemin": 0,
935
+ "aria-valuemax": 100,
936
+ "aria-valuenow": Math.round((sim.current.x - CLAW_MIN) / (CLAW_MAX - CLAW_MIN) * 100),
937
+ onPointerDown: onStickDown,
938
+ onPointerMove: onStickMove,
939
+ onPointerUp: onStickUp,
940
+ onPointerCancel: onStickUp,
941
+ children: [
942
+ /* @__PURE__ */ jsx("div", { className: "cc-joy-base" }),
943
+ /* @__PURE__ */ jsxs("div", { ref: stickEl, className: "cc-joy-stick", children: [
944
+ /* @__PURE__ */ jsx("div", { className: "cc-joy-shaft" }),
945
+ /* @__PURE__ */ jsx("div", { className: "cc-joy-ball" })
946
+ ] })
947
+ ]
948
+ }
949
+ ),
950
+ /* @__PURE__ */ jsxs(
951
+ "div",
952
+ {
953
+ ref: trayEl,
954
+ className: "cc-tray" + (trayMode === "open" ? " cc-tray--open" : trayMode === "win" ? " cc-tray--win" : trayMode === "no" ? " cc-tray--no" : overTray ? " cc-tray--hot" : ""),
955
+ children: [
956
+ /* @__PURE__ */ jsxs("span", { className: "cc-tray-hatch", "aria-hidden": "true", children: [
957
+ /* @__PURE__ */ jsx("span", { className: "cc-tray-mouth" }),
958
+ /* @__PURE__ */ jsx("span", { className: "cc-tray-door cc-tray-door--l" }),
959
+ /* @__PURE__ */ jsx("span", { className: "cc-tray-door cc-tray-door--r" }),
960
+ /* @__PURE__ */ jsx("span", { className: "cc-tray-skin" })
961
+ ] }),
962
+ trayMode === "win" && !reduce && /* @__PURE__ */ jsx("span", { className: "cc-confetti", "aria-hidden": "true", children: CONFETTI.map((p, i) => /* @__PURE__ */ jsx(
963
+ "i",
964
+ {
965
+ style: {
966
+ background: p.c,
967
+ animationDelay: `${p.d}s`,
968
+ "--dx": `${p.dx}px`,
969
+ "--dy": `${p.dy}px`,
970
+ "--dr": `${p.dr}deg`
971
+ }
972
+ },
973
+ i
974
+ )) }),
975
+ /* @__PURE__ */ jsxs("span", { className: "cc-tray-label", children: [
976
+ trayMode === "win" ? (
977
+ // a clean check — the catch is in
978
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "m3.6 8.6 2.9 2.9 6-6.8", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })
979
+ ) : trayMode === "no" ? (
980
+ // try-again loop — it goes back to the pile
981
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: [
982
+ /* @__PURE__ */ jsx("path", { d: "M13.2 8A5.2 5.2 0 1 1 11.6 4.25", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }),
983
+ /* @__PURE__ */ jsx("path", { d: "M11.7 1.5v2.9h2.9", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" })
984
+ ] })
985
+ ) : (
986
+ // a toy over the parted slot — this is where the catch goes in
987
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: [
988
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "4.9", r: "2.7", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }),
989
+ /* @__PURE__ */ jsx("path", { d: "M2.4 12.1h3.7M9.9 12.1h3.7", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
990
+ ] })
991
+ ),
992
+ /* @__PURE__ */ jsx("span", { children: trayMode === "win" ? "Nice catch!" : trayMode === "no" ? "Hmm, wrong toy" : overTray ? "Release!" : "Drop here" })
993
+ ] })
994
+ ]
995
+ }
996
+ ),
997
+ /* @__PURE__ */ jsx(
998
+ "button",
999
+ {
1000
+ type: "button",
1001
+ className: phase === "carry" && overTray ? "cc-action cc-action--ready" : "cc-action",
1002
+ onClick: action,
1003
+ disabled: busy || verified,
1004
+ "aria-label": phase === "carry" ? "Drop the toy" : "Grab",
1005
+ children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
1006
+ motion.span,
1007
+ {
1008
+ style: { display: "inline-block" },
1009
+ initial: { opacity: 0, y: 6 },
1010
+ animate: { opacity: 1, y: 0 },
1011
+ exit: { opacity: 0, y: -6 },
1012
+ transition: { duration: 0.16, ease: "easeOut" },
1013
+ children: phase === "carry" ? "Drop" : "Grab"
1014
+ },
1015
+ phase === "carry" ? "drop" : "grab"
1016
+ ) })
1017
+ }
1018
+ )
1019
+ ] })
1020
+ ] }),
1021
+ /* @__PURE__ */ jsx(
1022
+ "img",
1023
+ {
1024
+ ref: carriedEl,
1025
+ className: "cc-carried",
1026
+ src: carried >= 0 ? `${assetBase}${pile[carried].toy}.png` : `${assetBase}${target}.png`,
1027
+ alt: "",
1028
+ draggable: false,
1029
+ style: {
1030
+ width: carriedW,
1031
+ visibility: carried >= 0 && phase !== "idle" ? "visible" : "hidden"
1032
+ }
1033
+ }
1034
+ )
1035
+ ] }),
1036
+ /* @__PURE__ */ jsx("p", { className: "clawcap-hint", children: "Joystick or \u2190 \u2192 to move \xB7 Space to grab & drop" })
1037
+ ]
1038
+ }
1039
+ ) })
1040
+ );
1041
+ }
1042
+
1043
+ export { SataruzCaptcha, TOY_META };