@mazees/mark 5.0.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.
Files changed (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,2197 @@
1
+ /* eslint-disable react/prop-types */
2
+ import { useEffect, useRef, useCallback, memo } from 'react'
3
+
4
+ /**
5
+ * Mapping warna terpadu berdasarkan Mood & Status AI Mark
6
+ */
7
+ const getMoodColor = (mood = 'neutral', status = 'idle') => {
8
+ if (status === 'error' || status === 'bonk_angry' || status === 'shield_block') {
9
+ return { hex: '#ef4444', glow: 'bg-red-500/40' }
10
+ }
11
+ if (status === 'bonk_annoyed') {
12
+ return { hex: '#f97316', glow: 'bg-orange-500/40' }
13
+ }
14
+ if (status === 'bonk_dizzy') {
15
+ return { hex: '#a855f7', glow: 'bg-purple-500/40' }
16
+ }
17
+ if (status === 'music_groove') {
18
+ return { hex: '#06b6d4', glow: 'bg-cyan-400/40' }
19
+ }
20
+ if (status === 'processing') {
21
+ return { hex: '#3b82f6', glow: 'bg-blue-500/40' }
22
+ }
23
+ if (status === 'drag_lift') {
24
+ return { hex: '#38bdf8', glow: 'bg-sky-400/40' }
25
+ }
26
+ if (status === 'tickle') {
27
+ return { hex: '#facc15', glow: 'bg-yellow-400/40' }
28
+ }
29
+ if (status === 'pet_enjoy' || status === 'pet_purr' || status === 'blush_soft') {
30
+ return { hex: '#1fb854', glow: 'bg-emerald-400/40' }
31
+ }
32
+ switch (mood) {
33
+ case 'joy':
34
+ return { hex: '#facc15', glow: 'bg-yellow-400/40' }
35
+ case 'sadness':
36
+ return { hex: '#3b82f6', glow: 'bg-blue-500/40' }
37
+ case 'fear':
38
+ return { hex: '#a855f7', glow: 'bg-purple-500/40' }
39
+ case 'anger':
40
+ return { hex: '#ef4444', glow: 'bg-red-500/40' }
41
+ case 'disgust':
42
+ return { hex: '#84cc16', glow: 'bg-lime-400/40' }
43
+ case 'anxiety':
44
+ return { hex: '#f97316', glow: 'bg-orange-500/40' }
45
+ case 'envy':
46
+ return { hex: '#14b8a6', glow: 'bg-teal-500/40' }
47
+ case 'embarrassment':
48
+ return { hex: '#ec4899', glow: 'bg-pink-500/40' }
49
+ case 'ennui':
50
+ return { hex: '#6b7280', glow: 'bg-gray-500/40' }
51
+ default: // neutral
52
+ return { hex: '#1fb854', glow: 'bg-green-500/40' }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Avatar (2.5D Cyber-Droid Companion)
58
+ * Komponen avatar robot otonom MARK berbasis SVG volumetric 3D transforms.
59
+ * Menyediakan pergerakan hidup mandiri, tatapan mata presisi di tengah saat berbicara,
60
+ * responsivitas terhadap 10 mood, sentuhan interaktif fisik, dan seluruh status operasional AI.
61
+ */
62
+ const Avatar = ({
63
+ status = 'idle',
64
+ intensity = 0,
65
+ mood = 'neutral',
66
+ onClick = null,
67
+ onInteract = null,
68
+ className = ''
69
+ }) => {
70
+ // DOM element references
71
+ const robotRootRef = useRef(null)
72
+ const robotHeadRef = useRef(null)
73
+ const robotBodyRef = useRef(null)
74
+ const handLeftRef = useRef(null)
75
+ const handRightRef = useRef(null)
76
+ const antLeftRef = useRef(null)
77
+ const antRightRef = useRef(null)
78
+ const antBulbLeftRef = useRef(null)
79
+ const antBulbRightRef = useRef(null)
80
+ const earLeftRef = useRef(null)
81
+ const earRightRef = useRef(null)
82
+ const earGlowLeftRef = useRef(null)
83
+ const earGlowRightRef = useRef(null)
84
+ const groundShadowRef = useRef(null)
85
+ const eyesContainerRef = useRef(null)
86
+ const leftEyeRef = useRef(null)
87
+ const rightEyeRef = useRef(null)
88
+ const mouthRef = useRef(null)
89
+ const thrusterRef = useRef(null)
90
+ const desktopReflectionRef = useRef(null)
91
+ const prevStatusRef = useRef(status)
92
+
93
+ // State refs for animation loop
94
+ const stateRef = useRef({
95
+ status: 'idle',
96
+ mood: 'neutral',
97
+ intensity: 0,
98
+ isSleeping: false,
99
+ isBlinking: false,
100
+ eyeScaleX: 1,
101
+ eyeScaleY: 1,
102
+ // Physics variables
103
+ rotX: 0,
104
+ rotY: 0,
105
+ rotZ: 0,
106
+ targetRotX: 0,
107
+ targetRotY: 0,
108
+ targetRotZ: 0,
109
+ velRotX: 0,
110
+ velRotY: 0,
111
+ velRotZ: 0,
112
+ // Saccades
113
+ eyeSaccadeX: 0,
114
+ eyeSaccadeY: 0,
115
+ targetSaccadeX: 0,
116
+ targetSaccadeY: 0,
117
+ // Continuous phase accumulators (prevent frequency jump stutter)
118
+ breathPhase: 0,
119
+ swayPhase: 0,
120
+ currentBreathDepth: 7.5,
121
+ currentSwayDepth: 3.5,
122
+ currentRollDepth: 1.0,
123
+ // Quirks
124
+ lastQuirkTime: 0,
125
+ nextQuirkInterval: 2200,
126
+ quirkPitchOffset: 0,
127
+ quirkRollOffset: 0,
128
+ quirkAltitudeOffset: 0,
129
+ targetAltitudeOffset: 0,
130
+ // Dedicated Listening Mode
131
+ listeningSide: 1, // 1 = look right, -1 = look left
132
+ earScale: 1.0,
133
+ // Antenna
134
+ antLeftRot: 0,
135
+ antRightRot: 0,
136
+ antVelLeft: 0,
137
+ antVelRight: 0,
138
+ // Hands
139
+ handLeftX: 0,
140
+ handLeftY: 0,
141
+ handRightX: 0,
142
+ handRightY: 0,
143
+ // Poke physics
144
+ pokeOffsetY: 0,
145
+ pokeVelocityY: 0,
146
+ // Awareness reading scan
147
+ isSeeingDesktop: false,
148
+ desktopForwardZ: 0,
149
+ desktopDownwardY: 0,
150
+ desktopTimeout: null,
151
+ // --- Tactile Interactive State Engine ---
152
+ tactileState: null,
153
+ tactileTimer: null,
154
+ bonkCount: 0,
155
+ lastBonkTime: 0,
156
+ petAccumulator: 0,
157
+ petStartTime: 0,
158
+ tickleAccumulator: 0,
159
+ isPointerDown: false,
160
+ pointerDownZone: null,
161
+ pointerDownPos: { x: 0, y: 0 },
162
+ lastPointerPos: { x: 0, y: 0 },
163
+ isDragging: false,
164
+ dragOffset: { x: 0, y: 0 },
165
+ dragVelocity: { x: 0, y: 0 },
166
+ dropAnim: { active: false, y: 0, velY: 0, squashX: 1, squashY: 1 },
167
+ dizzyAngle: 0,
168
+ purrJitter: { x: 0, y: 0 },
169
+ shieldProgress: 0,
170
+ corePulseScale: 1
171
+ })
172
+
173
+ // Render face eyes and mouth SVG based on state
174
+ const renderFace = useCallback(() => {
175
+ const s = stateRef.current
176
+ const effectiveStatus = s.tactileState || (s.isSleeping ? 'sleeping' : s.status)
177
+ const color = getMoodColor(s.mood, effectiveStatus).hex
178
+
179
+ const leftEye = leftEyeRef.current
180
+ const rightEye = rightEyeRef.current
181
+ const mouth = mouthRef.current
182
+ if (!leftEye || !rightEye || !mouth) return
183
+
184
+ // 1. DIZZY SPIRAL (Combo bonk 5-7x)
185
+ if (effectiveStatus === 'bonk_dizzy') {
186
+ const rot = Math.round(s.dizzyAngle || 0)
187
+ const dizzyEye = `
188
+ <svg viewBox="0 0 40 40" class="w-full h-full">
189
+ <g style="transform-origin: 20px 20px; transform: rotate(${rot}deg)">
190
+ <path d="M 20 20 m 0 0 a 2.5 2.5 0 0 1 2.5 2.5 a 5 5 0 0 1 -5 2.5 a 7.5 7.5 0 0 1 -5 -7.5 a 10 10 0 0 1 10 -5 a 13 13 0 0 1 11 11 a 16 16 0 0 1 -16 14" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
191
+ </g>
192
+ </svg>
193
+ `
194
+ leftEye.innerHTML = dizzyEye
195
+ rightEye.innerHTML = dizzyEye
196
+ mouth.innerHTML = `
197
+ <svg viewBox="0 0 60 20" class="w-12 h-4">
198
+ <path d="M 18 10 Q 24 4 30 10 T 42 10" fill="none" stroke="${color}" stroke-width="2.8" stroke-linecap="round"/>
199
+ </svg>
200
+ `
201
+ return
202
+ }
203
+
204
+ // 2. SHIELD BLOCK / ANGRY BONK (Combo bonk 8x+)
205
+ if (effectiveStatus === 'shield_block' || effectiveStatus === 'bonk_angry') {
206
+ const angryLeft = `
207
+ <svg viewBox="0 0 40 40" class="w-full h-full">
208
+ <path d="M 6 10 L 34 22 L 28 30 L 6 18 Z" fill="#ef4444"/>
209
+ </svg>
210
+ `
211
+ const angryRight = `
212
+ <svg viewBox="0 0 40 40" class="w-full h-full">
213
+ <path d="M 34 10 L 6 22 L 12 30 L 34 18 Z" fill="#ef4444"/>
214
+ </svg>
215
+ `
216
+ leftEye.innerHTML = angryLeft
217
+ rightEye.innerHTML = angryRight
218
+ mouth.innerHTML = `
219
+ <svg viewBox="0 0 60 20" class="w-12 h-4">
220
+ <rect x="20" y="7" width="20" height="9" rx="2" fill="none" stroke="#ef4444" stroke-width="2"/>
221
+ <line x1="26" y1="7" x2="26" y2="16" stroke="#ef4444" stroke-width="1.5"/>
222
+ <line x1="34" y1="7" x2="34" y2="16" stroke="#ef4444" stroke-width="1.5"/>
223
+ <line x1="20" y1="11.5" x2="40" y2="11.5" stroke="#ef4444" stroke-width="1.5"/>
224
+ </svg>
225
+ `
226
+ return
227
+ }
228
+
229
+ // 3. ANNOYED BONK (Combo bonk 4-6x)
230
+ if (effectiveStatus === 'bonk_annoyed') {
231
+ const annoyedLeft = `
232
+ <svg viewBox="0 0 40 40" class="w-full h-full">
233
+ <line x1="8" y1="11" x2="32" y2="17" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
234
+ <ellipse cx="20" cy="22" rx="7" ry="4" fill="${color}"/>
235
+ </svg>
236
+ `
237
+ const annoyedRight = `
238
+ <svg viewBox="0 0 40 40" class="w-full h-full">
239
+ <line x1="8" y1="17" x2="32" y2="11" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
240
+ <ellipse cx="20" cy="22" rx="7" ry="4" fill="${color}"/>
241
+ </svg>
242
+ `
243
+ leftEye.innerHTML = annoyedLeft
244
+ rightEye.innerHTML = annoyedRight
245
+ mouth.innerHTML = `
246
+ <svg viewBox="0 0 60 20" class="w-10 h-4">
247
+ <path d="M 22 14 L 30 10 L 38 13" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
248
+ </svg>
249
+ `
250
+ return
251
+ }
252
+
253
+ // 3b. GENTLE BONK (Combo bonk 1-3x: ouch wince, squint eyes + wavy mouth)
254
+ if (effectiveStatus === 'bonk_1') {
255
+ const bonkEye = `
256
+ <svg viewBox="0 0 40 40" class="w-full h-full">
257
+ <path d="M 8 22 L 20 14 L 32 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
258
+ </svg>
259
+ `
260
+ leftEye.innerHTML = bonkEye
261
+ rightEye.innerHTML = bonkEye
262
+ mouth.innerHTML = `
263
+ <svg viewBox="0 0 60 20" class="w-10 h-4">
264
+ <path d="M 22 13 Q 30 7 38 13" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
265
+ </svg>
266
+ `
267
+ return
268
+ }
269
+
270
+ // 4. PETTING / PURRING / BLUSHING (Happy eyes + blush lines + cat mouth)
271
+ if (
272
+ effectiveStatus === 'pet_enjoy' ||
273
+ effectiveStatus === 'pet_purr' ||
274
+ effectiveStatus === 'blush_soft'
275
+ ) {
276
+ const petEye = `
277
+ <svg viewBox="0 0 40 40" class="w-full h-full">
278
+ <path d="M 8 20 Q 20 8 32 20" fill="none" stroke="${color}" stroke-width="4.8" stroke-linecap="round"/>
279
+ <line x1="10" y1="28" x2="16" y2="28" stroke="#f43f5e" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
280
+ <line x1="24" y1="28" x2="30" y2="28" stroke="#f43f5e" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
281
+ </svg>
282
+ `
283
+ leftEye.innerHTML = petEye
284
+ rightEye.innerHTML = petEye
285
+ mouth.innerHTML = `
286
+ <svg viewBox="0 0 60 20" class="w-12 h-5">
287
+ <path d="M 20 8 Q 25 15 30 11 Q 35 15 40 8" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
288
+ </svg>
289
+ `
290
+ return
291
+ }
292
+
293
+ // 5. TICKLE (Squint joy laughing)
294
+ if (effectiveStatus === 'tickle') {
295
+ const tickleEye = `
296
+ <svg viewBox="0 0 40 40" class="w-full h-full">
297
+ <path d="M 9 22 Q 20 10 31 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
298
+ </svg>
299
+ `
300
+ leftEye.innerHTML = tickleEye
301
+ rightEye.innerHTML = tickleEye
302
+ mouth.innerHTML = `
303
+ <svg viewBox="0 0 60 26" class="w-12 h-6">
304
+ <path d="M 20 10 Q 30 24 40 10 Z" fill="${color}" opacity="0.9"/>
305
+ </svg>
306
+ `
307
+ return
308
+ }
309
+
310
+ // 6. POKED EYE (Left or Right eye wink)
311
+ if (effectiveStatus === 'poke_eye_l') {
312
+ leftEye.innerHTML = `
313
+ <svg viewBox="0 0 40 40" class="w-full h-full">
314
+ <line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
315
+ </svg>
316
+ `
317
+ rightEye.innerHTML = `
318
+ <svg viewBox="0 0 40 40" class="w-full h-full">
319
+ <circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="4"/>
320
+ <circle cx="20" cy="20" r="4.5" fill="${color}"/>
321
+ </svg>
322
+ `
323
+ mouth.innerHTML = `
324
+ <svg viewBox="0 0 60 20" class="w-8 h-4">
325
+ <ellipse cx="30" cy="11" rx="4" ry="6" fill="${color}"/>
326
+ </svg>
327
+ `
328
+ return
329
+ }
330
+ if (effectiveStatus === 'poke_eye_r') {
331
+ leftEye.innerHTML = `
332
+ <svg viewBox="0 0 40 40" class="w-full h-full">
333
+ <circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="4"/>
334
+ <circle cx="20" cy="20" r="4.5" fill="${color}"/>
335
+ </svg>
336
+ `
337
+ rightEye.innerHTML = `
338
+ <svg viewBox="0 0 40 40" class="w-full h-full">
339
+ <line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
340
+ </svg>
341
+ `
342
+ mouth.innerHTML = `
343
+ <svg viewBox="0 0 60 20" class="w-8 h-4">
344
+ <ellipse cx="30" cy="11" rx="4" ry="6" fill="${color}"/>
345
+ </svg>
346
+ `
347
+ return
348
+ }
349
+
350
+ // 7. POKED ANTENNA
351
+ if (effectiveStatus === 'poke_antenna') {
352
+ const antEye = `
353
+ <svg viewBox="0 0 40 40" class="w-full h-full">
354
+ <path d="M 10 20 Q 15 14 20 20 T 30 20" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
355
+ </svg>
356
+ `
357
+ leftEye.innerHTML = antEye
358
+ rightEye.innerHTML = antEye
359
+ mouth.innerHTML = `
360
+ <svg viewBox="0 0 60 20" class="w-10 h-4">
361
+ <path d="M 22 10 L 26 6 L 30 14 L 34 6 L 38 10" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
362
+ </svg>
363
+ `
364
+ return
365
+ }
366
+
367
+ // 8. POKED CORE OR STARTLE WAKE
368
+ if (effectiveStatus === 'poke_core' || effectiveStatus === 'startle_wake') {
369
+ const shockEye = `
370
+ <svg viewBox="0 0 40 40" class="w-full h-full">
371
+ <circle cx="20" cy="20" r="12" fill="none" stroke="${color}" stroke-width="3.5"/>
372
+ <circle cx="20" cy="20" r="5" fill="#ffffff"/>
373
+ </svg>
374
+ `
375
+ leftEye.innerHTML = shockEye
376
+ rightEye.innerHTML = shockEye
377
+ mouth.innerHTML = `
378
+ <svg viewBox="0 0 60 20" class="w-8 h-5">
379
+ <ellipse cx="30" cy="11" rx="5" ry="7" fill="${color}"/>
380
+ </svg>
381
+ `
382
+ return
383
+ }
384
+
385
+ // 9. DRAGGING / LIFT IN AIR
386
+ if (effectiveStatus === 'drag_lift') {
387
+ const liftEye = `
388
+ <svg viewBox="0 0 40 40" class="w-full h-full">
389
+ <ellipse cx="20" cy="18" rx="8" ry="11" fill="none" stroke="${color}" stroke-width="3.5"/>
390
+ <circle cx="20" cy="23" r="4" fill="${color}"/>
391
+ </svg>
392
+ `
393
+ leftEye.innerHTML = liftEye
394
+ rightEye.innerHTML = liftEye
395
+ mouth.innerHTML = `
396
+ <svg viewBox="0 0 60 20" class="w-8 h-5">
397
+ <ellipse cx="30" cy="11" rx="5" ry="7" fill="${color}"/>
398
+ </svg>
399
+ `
400
+ return
401
+ }
402
+
403
+ // 10. MUSIC GROOVE
404
+ if (effectiveStatus === 'music_groove') {
405
+ const eqEye = `
406
+ <svg viewBox="0 0 40 40" class="w-full h-full">
407
+ <rect x="9" y="14" width="4" height="12" rx="2" fill="${color}"/>
408
+ <rect x="18" y="8" width="4" height="24" rx="2" fill="${color}"/>
409
+ <rect x="27" y="16" width="4" height="8" rx="2" fill="${color}"/>
410
+ </svg>
411
+ `
412
+ leftEye.innerHTML = eqEye
413
+ rightEye.innerHTML = eqEye
414
+ mouth.innerHTML = `
415
+ <svg viewBox="0 0 60 20" class="w-12 h-4">
416
+ <path d="M 20 9 Q 30 18 40 9" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
417
+ </svg>
418
+ `
419
+ return
420
+ }
421
+
422
+ // 11. PROCESSING (Native tool HUD)
423
+ if (effectiveStatus === 'processing') {
424
+ const procEye = `
425
+ <svg viewBox="0 0 40 40" class="w-full h-full">
426
+ <path d="M 12 11 L 8 11 L 8 29 L 12 29" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
427
+ <path d="M 28 11 L 32 11 L 32 29 L 28 29" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
428
+ <circle cx="20" cy="20" r="3.2" fill="${color}"/>
429
+ </svg>
430
+ `
431
+ leftEye.innerHTML = procEye
432
+ rightEye.innerHTML = procEye
433
+ mouth.innerHTML = `
434
+ <div class="flex items-center gap-1 py-1">
435
+ <span class="w-2 h-1 bg-primary animate-pulse"></span>
436
+ <span class="w-2 h-1 bg-primary animate-pulse [animation-delay:0.15s]"></span>
437
+ <span class="w-2 h-1 bg-primary animate-pulse [animation-delay:0.3s]"></span>
438
+ </div>
439
+ `
440
+ return
441
+ }
442
+
443
+ // 12. SLEEPING STATE (Closed calm rounded curves)
444
+ if (effectiveStatus === 'sleeping') {
445
+ const sleepEye = `
446
+ <svg viewBox="0 0 40 40" class="w-full h-full">
447
+ <path d="M 8 22 Q 20 25 32 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
448
+ </svg>
449
+ `
450
+ leftEye.innerHTML = sleepEye
451
+ rightEye.innerHTML = sleepEye
452
+ mouth.innerHTML = `
453
+ <svg viewBox="0 0 60 20" class="w-12 h-5">
454
+ <path d="M 22 10 Q 30 14 38 10" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
455
+ </svg>
456
+ `
457
+ return
458
+ }
459
+
460
+ // 13. ERROR STATE (Sharp digital crosses)
461
+ if (effectiveStatus === 'error') {
462
+ const errEye = `
463
+ <svg viewBox="0 0 40 40" class="w-full h-full">
464
+ <line x1="10" y1="10" x2="30" y2="30" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
465
+ <line x1="30" y1="10" x2="10" y2="30" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
466
+ </svg>
467
+ `
468
+ leftEye.innerHTML = errEye
469
+ rightEye.innerHTML = errEye
470
+ mouth.innerHTML = `
471
+ <svg viewBox="0 0 60 20" class="w-14 h-5">
472
+ <path d="M 15 10 L 22 6 L 30 14 L 38 6 L 45 10" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
473
+ </svg>
474
+ `
475
+ return
476
+ }
477
+
478
+ // 14. AWARENESS / READING DESKTOP EYES (Focused eyes curved slightly downward)
479
+ if (s.isSeeingDesktop || effectiveStatus === 'awareness' || effectiveStatus === 'nudge') {
480
+ const readingEye = `
481
+ <svg viewBox="0 0 40 40" class="w-full h-full">
482
+ <path d="M 11 15 C 11 9, 29 9, 29 15 C 29 27, 11 27, 11 15 Z" fill="${color}"/>
483
+ <circle cx="20" cy="18" r="3.2" fill="#ffffff"/>
484
+ </svg>
485
+ `
486
+ leftEye.innerHTML = readingEye
487
+ rightEye.innerHTML = readingEye
488
+ mouth.innerHTML = `
489
+ <svg viewBox="0 0 60 20" class="w-8 h-3">
490
+ <line x1="25" y1="10" x2="35" y2="10" stroke="${color}" stroke-width="2" stroke-linecap="round"/>
491
+ </svg>
492
+ `
493
+ return
494
+ }
495
+
496
+ // 15. LISTENING / HEARING STATE (Focused attentive eyes, dilated alert pupils, inquisitive brows, quiet mouth)
497
+ if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
498
+ if (s.isBlinking) {
499
+ const blinkEye = `
500
+ <svg viewBox="0 0 40 40" class="w-full h-full">
501
+ <line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
502
+ </svg>
503
+ `
504
+ leftEye.innerHTML = blinkEye
505
+ rightEye.innerHTML = blinkEye
506
+ } else {
507
+ const pupilOffset = (s.listeningSide || 1) * 2.5
508
+ const leftBrowY = s.listeningSide === -1 ? '4' : '7'
509
+ const rightBrowY = s.listeningSide === 1 ? '4' : '7'
510
+ const listeningLeftEye = `
511
+ <svg viewBox="0 0 40 40" class="w-full h-full overflow-visible">
512
+ <path d="M 8 9 Q 20 ${leftBrowY} 32 8" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
513
+ <path d="M 11 14 C 11 8, 29 8, 29 14 C 29 27, 11 27, 11 14 Z" fill="${color}"/>
514
+ <circle cx="${20 + pupilOffset}" cy="16.5" r="4.6" fill="#ffffff" opacity="0.98"/>
515
+ <circle cx="${21 + pupilOffset}" cy="15.2" r="1.8" fill="#ffffff"/>
516
+ </svg>
517
+ `
518
+ const listeningRightEye = `
519
+ <svg viewBox="0 0 40 40" class="w-full h-full overflow-visible">
520
+ <path d="M 8 8 Q 20 ${rightBrowY} 32 9" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
521
+ <path d="M 11 14 C 11 8, 29 8, 29 14 C 29 27, 11 27, 11 14 Z" fill="${color}"/>
522
+ <circle cx="${20 + pupilOffset}" cy="16.5" r="4.6" fill="#ffffff" opacity="0.98"/>
523
+ <circle cx="${21 + pupilOffset}" cy="15.2" r="1.8" fill="#ffffff"/>
524
+ </svg>
525
+ `
526
+ leftEye.innerHTML = listeningLeftEye
527
+ rightEye.innerHTML = listeningRightEye
528
+ }
529
+
530
+ mouth.innerHTML = `
531
+ <svg viewBox="0 0 60 20" class="w-10 h-3">
532
+ <line x1="23" y1="10" x2="37" y2="10" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
533
+ </svg>
534
+ `
535
+ return
536
+ }
537
+
538
+ // 16. ELASTIC BLINKING STATE (Squash down to thin sleek slit)
539
+ if (s.isBlinking) {
540
+ const blinkEye = `
541
+ <svg viewBox="0 0 40 40" class="w-full h-full">
542
+ <line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
543
+ </svg>
544
+ `
545
+ leftEye.innerHTML = blinkEye
546
+ rightEye.innerHTML = blinkEye
547
+ } else if (effectiveStatus === 'speaking' || s.intensity > 0.05) {
548
+ // 17. SPEAKING STATE: Eyes locked forward with dead-center pupil (X:20, Y:17)
549
+ const speakPupilR = 3.8 + Math.min(1.0, s.intensity * 1.5)
550
+ const speakEye = `
551
+ <svg viewBox="0 0 40 40" class="w-full h-full">
552
+ <path d="M 12 13 C 12 6.5, 28 6.5, 28 13 C 28 27.5, 12 27.5, 12 13 Z" fill="${color}"/>
553
+ <circle cx="20" cy="17" r="${speakPupilR}" fill="#ffffff" opacity="0.98"/>
554
+ </svg>
555
+ `
556
+ leftEye.innerHTML = speakEye
557
+ rightEye.innerHTML = speakEye
558
+ } else {
559
+ // 18. MOOD SPECIFIC DIGITAL EYES
560
+ let leftEyeSvg = ''
561
+ let rightEyeSvg = ''
562
+
563
+ switch (s.mood) {
564
+ case 'joy':
565
+ leftEyeSvg = `
566
+ <svg viewBox="0 0 40 40" class="w-full h-full">
567
+ <path d="M 8 26 Q 20 8 32 26" fill="none" stroke="${color}" stroke-width="5" stroke-linecap="round"/>
568
+ </svg>
569
+ `
570
+ rightEyeSvg = leftEyeSvg
571
+ break
572
+ case 'anger':
573
+ leftEyeSvg = `
574
+ <svg viewBox="0 0 40 40" class="w-full h-full">
575
+ <path d="M 6 12 L 34 22 L 30 30 L 6 18 Z" fill="${color}"/>
576
+ </svg>
577
+ `
578
+ rightEyeSvg = `
579
+ <svg viewBox="0 0 40 40" class="w-full h-full">
580
+ <path d="M 34 12 L 6 22 L 10 30 L 34 18 Z" fill="${color}"/>
581
+ </svg>
582
+ `
583
+ break
584
+ case 'sadness':
585
+ leftEyeSvg = `
586
+ <svg viewBox="0 0 40 40" class="w-full h-full">
587
+ <path d="M 8 16 Q 20 28 32 16" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
588
+ <circle cx="20" cy="30" r="3.5" fill="#38bdf8"/>
589
+ </svg>
590
+ `
591
+ rightEyeSvg = `
592
+ <svg viewBox="0 0 40 40" class="w-full h-full">
593
+ <path d="M 8 16 Q 20 28 32 16" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
594
+ <circle cx="20" cy="30" r="3.5" fill="#38bdf8"/>
595
+ </svg>
596
+ `
597
+ break
598
+ case 'fear':
599
+ leftEyeSvg = `
600
+ <svg viewBox="0 0 40 40" class="w-full h-full">
601
+ <circle cx="20" cy="20" r="13" fill="none" stroke="${color}" stroke-width="4"/>
602
+ <circle cx="20" cy="20" r="4.5" fill="${color}"/>
603
+ </svg>
604
+ `
605
+ rightEyeSvg = leftEyeSvg
606
+ break
607
+ case 'anxiety':
608
+ leftEyeSvg = `
609
+ <svg viewBox="0 0 40 40" class="w-full h-full">
610
+ <path d="M 20 20 A 4 4 0 0 1 24 20 A 8 8 0 0 1 16 20 A 12 12 0 0 1 28 20" fill="none" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
611
+ </svg>
612
+ `
613
+ rightEyeSvg = leftEyeSvg
614
+ break
615
+ case 'envy':
616
+ leftEyeSvg = `
617
+ <svg viewBox="0 0 40 40" class="w-full h-full">
618
+ <path d="M 6 18 Q 20 12 34 20 Q 20 28 6 18 Z" fill="${color}" opacity="0.4"/>
619
+ <line x1="18" y1="12" x2="22" y2="28" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
620
+ </svg>
621
+ `
622
+ rightEyeSvg = `
623
+ <svg viewBox="0 0 40 40" class="w-full h-full">
624
+ <path d="M 6 20 Q 20 12 34 18 Q 20 28 6 20 Z" fill="${color}" opacity="0.4"/>
625
+ <line x1="22" y1="12" x2="18" y2="28" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
626
+ </svg>
627
+ `
628
+ break
629
+ case 'embarrassment':
630
+ leftEyeSvg = `
631
+ <svg viewBox="0 0 40 40" class="w-full h-full">
632
+ <path d="M 12 14 L 28 22 L 12 30" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
633
+ </svg>
634
+ `
635
+ rightEyeSvg = `
636
+ <svg viewBox="0 0 40 40" class="w-full h-full">
637
+ <path d="M 28 14 L 12 22 L 28 30" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
638
+ </svg>
639
+ `
640
+ break
641
+ case 'ennui':
642
+ leftEyeSvg = `
643
+ <svg viewBox="0 0 40 40" class="w-full h-full">
644
+ <line x1="8" y1="18" x2="32" y2="18" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
645
+ <circle cx="20" cy="24" r="5" fill="${color}"/>
646
+ </svg>
647
+ `
648
+ rightEyeSvg = leftEyeSvg
649
+ break
650
+ case 'disgust':
651
+ leftEyeSvg = `
652
+ <svg viewBox="0 0 40 40" class="w-full h-full">
653
+ <line x1="10" y1="20" x2="30" y2="16" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
654
+ </svg>
655
+ `
656
+ rightEyeSvg = `
657
+ <svg viewBox="0 0 40 40" class="w-full h-full">
658
+ <circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="3.5"/>
659
+ <circle cx="20" cy="20" r="4" fill="${color}"/>
660
+ </svg>
661
+ `
662
+ break
663
+ default:
664
+ leftEyeSvg = `
665
+ <svg viewBox="0 0 40 40" class="w-full h-full">
666
+ <path d="M 12 13 C 12 6.5, 28 6.5, 28 13 C 28 27.5, 12 27.5, 12 13 Z" fill="${color}"/>
667
+ <circle cx="20" cy="17" r="3.6" fill="#ffffff" opacity="0.95"/>
668
+ </svg>
669
+ `
670
+ rightEyeSvg = leftEyeSvg
671
+ break
672
+ }
673
+
674
+ leftEye.innerHTML = leftEyeSvg
675
+ rightEye.innerHTML = rightEyeSvg
676
+ }
677
+
678
+ // 19. MOUTH / LIP-FLAP
679
+ if (effectiveStatus === 'speaking' || s.intensity > 0.05) {
680
+ const scale = Math.max(0.2, s.intensity)
681
+ const openH = Math.round(5 + scale * 16)
682
+ const openW = Math.round(12 + scale * 20)
683
+
684
+ mouth.innerHTML = `
685
+ <svg viewBox="0 0 60 30" class="w-14 h-6">
686
+ <ellipse cx="30" cy="15" rx="${openW / 2}" ry="${openH / 2}" fill="${color}" opacity="0.3"/>
687
+ <ellipse cx="30" cy="15" rx="${Math.max(2, openW / 2 - 2)}" ry="${Math.max(2, openH / 2 - 2)}" fill="#ffffff"/>
688
+ </svg>
689
+ `
690
+ } else if (effectiveStatus === 'thinking') {
691
+ mouth.innerHTML = `
692
+ <div class="flex items-center gap-1.5 py-1">
693
+ <span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping"></span>
694
+ <span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping [animation-delay:0.2s]"></span>
695
+ <span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping [animation-delay:0.4s]"></span>
696
+ </div>
697
+ `
698
+ } else {
699
+ let mouthSvg = ''
700
+ switch (s.mood) {
701
+ case 'joy':
702
+ mouthSvg = `<path d="M 20 8 Q 30 20 40 8" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`
703
+ break
704
+ case 'anger':
705
+ mouthSvg = `<path d="M 20 18 Q 30 10 40 18" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`
706
+ break
707
+ case 'sadness':
708
+ mouthSvg = `<path d="M 22 18 Q 30 10 38 18" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
709
+ break
710
+ case 'anxiety':
711
+ mouthSvg = `<path d="M 20 14 Q 25 10 30 14 Q 35 18 40 14" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
712
+ break
713
+ case 'envy':
714
+ mouthSvg = `<path d="M 24 16 Q 32 18 40 12" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
715
+ break
716
+ default:
717
+ mouthSvg = `<line x1="24" y1="14" x2="36" y2="14" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
718
+ break
719
+ }
720
+ mouth.innerHTML = `
721
+ <svg viewBox="0 0 60 26" class="w-12 h-4">
722
+ ${mouthSvg}
723
+ </svg>
724
+ `
725
+ }
726
+ }, [])
727
+
728
+ // Update squash & stretch scales on eye boxes
729
+ const updateEyeMesh = useCallback(() => {
730
+ const s = stateRef.current
731
+ if (leftEyeRef.current) {
732
+ leftEyeRef.current.style.transform = `scale(${s.eyeScaleX}, ${s.eyeScaleY})`
733
+ }
734
+ if (rightEyeRef.current) {
735
+ rightEyeRef.current.style.transform = `scale(${s.eyeScaleX}, ${s.eyeScaleY})`
736
+ }
737
+ }, [])
738
+
739
+ // Sync props to ref
740
+ useEffect(() => {
741
+ if (status === 'listening' && prevStatusRef.current !== 'listening') {
742
+ // Pick random side: 1 for right, -1 for left
743
+ stateRef.current.listeningSide = Math.random() > 0.5 ? 1 : -1
744
+ }
745
+ prevStatusRef.current = status
746
+
747
+ stateRef.current.status = status
748
+ stateRef.current.mood = mood
749
+ stateRef.current.intensity = intensity
750
+
751
+ const isAwareness = status === 'awareness' || status === 'nudge'
752
+ stateRef.current.isSeeingDesktop = isAwareness
753
+
754
+ if (desktopReflectionRef.current) {
755
+ desktopReflectionRef.current.style.opacity = isAwareness ? '1' : '0'
756
+ }
757
+
758
+ if (antBulbLeftRef.current && antBulbRightRef.current) {
759
+ if (isAwareness || status === 'listening') {
760
+ antBulbLeftRef.current.classList.add('animate-pulse')
761
+ antBulbRightRef.current.classList.add('animate-pulse')
762
+ } else {
763
+ antBulbLeftRef.current.classList.remove('animate-pulse')
764
+ antBulbRightRef.current.classList.remove('animate-pulse')
765
+ }
766
+ }
767
+
768
+ if (isAwareness) {
769
+ stateRef.current.antVelLeft = -14
770
+ stateRef.current.antVelRight = 14
771
+ stateRef.current.targetSaccadeX = -6
772
+ stateRef.current.targetSaccadeY = 2
773
+ setTimeout(() => {
774
+ stateRef.current.targetSaccadeX = 6
775
+ }, 900)
776
+ setTimeout(() => {
777
+ stateRef.current.targetSaccadeX = 0
778
+ stateRef.current.targetSaccadeY = 0
779
+ }, 2200)
780
+ }
781
+
782
+ // Immediate face re-render upon status / mood changes (colors applied strictly to face)
783
+ renderFace()
784
+ }, [status, mood, renderFace])
785
+
786
+ // Direct high-speed intensity listener (avoids parent React re-renders)
787
+ useEffect(() => {
788
+ const handleIntensityEvent = (e) => {
789
+ stateRef.current.intensity = typeof e.detail === 'number' ? e.detail : 0
790
+ }
791
+ window.addEventListener('mark-intensity', handleIntensityEvent)
792
+ return () => window.removeEventListener('mark-intensity', handleIntensityEvent)
793
+ }, [])
794
+
795
+ // Listen for sleep state from useAwareness
796
+ useEffect(() => {
797
+ const handleSleepingEvent = (e) => {
798
+ const sleeping = Boolean(e.detail?.isSleeping)
799
+ stateRef.current.isSleeping = sleeping
800
+ renderFace()
801
+ }
802
+ window.addEventListener('mark:sleeping', handleSleepingEvent)
803
+ return () => window.removeEventListener('mark:sleeping', handleSleepingEvent)
804
+ }, [renderFace])
805
+
806
+ // Blinking loop with natural squash & stretch
807
+ useEffect(() => {
808
+ let blinkTimer = null
809
+
810
+ const triggerBlink = () => {
811
+ const s = stateRef.current
812
+ if (s.isSleeping || s.status === 'sleeping') {
813
+ blinkTimer = setTimeout(triggerBlink, 3000)
814
+ return
815
+ }
816
+
817
+ s.isBlinking = true
818
+ s.eyeScaleX = 1.15
819
+ s.eyeScaleY = 0.15
820
+ updateEyeMesh()
821
+ renderFace()
822
+
823
+ setTimeout(() => {
824
+ s.isBlinking = false
825
+ s.eyeScaleX = 0.95
826
+ s.eyeScaleY = 1.1
827
+ updateEyeMesh()
828
+ renderFace()
829
+
830
+ setTimeout(() => {
831
+ s.eyeScaleX = 1
832
+ s.eyeScaleY = 1
833
+ updateEyeMesh()
834
+ }, 80)
835
+
836
+ // 20% chance of natural double-blink
837
+ if (Math.random() < 0.2) {
838
+ setTimeout(() => {
839
+ s.isBlinking = true
840
+ s.eyeScaleY = 0.15
841
+ updateEyeMesh()
842
+ renderFace()
843
+ setTimeout(() => {
844
+ s.isBlinking = false
845
+ s.eyeScaleY = 1
846
+ updateEyeMesh()
847
+ renderFace()
848
+ }, 90)
849
+ }, 110)
850
+ }
851
+
852
+ const nextDelay = 2600 + Math.random() * 3200
853
+ blinkTimer = setTimeout(triggerBlink, nextDelay)
854
+ }, 120)
855
+ }
856
+
857
+ blinkTimer = setTimeout(triggerBlink, 1500)
858
+ return () => clearTimeout(blinkTimer)
859
+ }, [renderFace, updateEyeMesh])
860
+
861
+ // Autonomous quirks loop (tilt, nod, stretch, glance)
862
+ const updateAutonomousQuirks = useCallback((now) => {
863
+ const s = stateRef.current
864
+ if (
865
+ s.tactileState ||
866
+ s.isDragging ||
867
+ s.dropAnim.active ||
868
+ s.isSeeingDesktop ||
869
+ s.status === 'speaking' ||
870
+ s.status === 'listening' ||
871
+ s.status === 'hearing' ||
872
+ s.status === 'music_groove' ||
873
+ s.isSleeping ||
874
+ s.status === 'sleeping'
875
+ ) {
876
+ s.targetRotY = 0
877
+ s.targetRotZ = 0
878
+ s.targetSaccadeX = 0
879
+ s.targetSaccadeY = 0
880
+ s.quirkPitchOffset = 0
881
+ s.quirkRollOffset = 0
882
+ s.targetAltitudeOffset = 0
883
+ return
884
+ }
885
+
886
+ if (now - s.lastQuirkTime >= s.nextQuirkInterval) {
887
+ s.lastQuirkTime = now
888
+ s.nextQuirkInterval = 2000 + Math.random() * 1200
889
+
890
+ const roll = Math.random()
891
+ // 1. Curious Head Tilt (Memiringkan kepala penasaran)
892
+ if (roll < 0.3) {
893
+ s.quirkRollOffset = Math.random() > 0.5 ? 10 : -10
894
+ s.quirkPitchOffset = -4
895
+ s.targetRotY = (Math.random() - 0.5) * 8
896
+ s.targetSaccadeX = s.quirkRollOffset * 0.4
897
+ setTimeout(() => {
898
+ s.quirkRollOffset = 0
899
+ s.quirkPitchOffset = 0
900
+ s.targetRotY = 0
901
+ s.targetSaccadeX = 0
902
+ }, 1200)
903
+ }
904
+ // 2. Subtle Listening Nod (Mengangguk kecil menyimak)
905
+ else if (roll < 0.55) {
906
+ s.quirkPitchOffset = -8
907
+ s.quirkRollOffset = 0
908
+ setTimeout(() => {
909
+ s.quirkPitchOffset = 0
910
+ }, 400)
911
+ }
912
+ // 3. Floating Stretch Breath (Helaan nafas apung halus)
913
+ else if (roll < 0.75) {
914
+ s.targetAltitudeOffset = -10
915
+ s.quirkPitchOffset = 3
916
+ setTimeout(() => {
917
+ s.targetAltitudeOffset = 0
918
+ s.quirkPitchOffset = 0
919
+ }, 1000)
920
+ }
921
+ // 4. Casual Glance Around (Menoleh santai)
922
+ else {
923
+ s.targetRotY = Math.random() > 0.5 ? 12 : -12
924
+ s.quirkPitchOffset = -2
925
+ s.quirkRollOffset = (Math.random() - 0.5) * 4
926
+ s.targetSaccadeX = s.targetRotY * 0.35
927
+ setTimeout(() => {
928
+ s.targetRotY = 0
929
+ s.targetSaccadeX = 0
930
+ s.quirkPitchOffset = 0
931
+ s.quirkRollOffset = 0
932
+ }, 1400)
933
+ }
934
+ }
935
+ }, [])
936
+
937
+ // Interactive tactile state setter
938
+ const setTactile = useCallback(
939
+ (newState, durationMs = 1200) => {
940
+ const s = stateRef.current
941
+ if (s.tactileTimer) clearTimeout(s.tactileTimer)
942
+ s.tactileState = newState
943
+ renderFace()
944
+ if (durationMs > 0) {
945
+ s.tactileTimer = setTimeout(() => {
946
+ s.tactileState = null
947
+ renderFace()
948
+ }, durationMs)
949
+ }
950
+ },
951
+ [renderFace]
952
+ )
953
+
954
+ // Pointer Interaction Engine (Down, Move, Up, Cancel)
955
+ const handlePointerUp = useCallback(
956
+ (e) => {
957
+ const s = stateRef.current
958
+ if (!s.isPointerDown) return
959
+ s.isPointerDown = false
960
+
961
+ try {
962
+ if (e.currentTarget?.hasPointerCapture?.(e.pointerId)) {
963
+ e.currentTarget.releasePointerCapture(e.pointerId)
964
+ }
965
+ } catch (_) {}
966
+
967
+ const totalDist = Math.hypot(e.clientX - s.pointerDownPos.x, e.clientY - s.pointerDownPos.y)
968
+
969
+ // 1. Release from vertical lift (drop straight down with physics)
970
+ if (s.isDragging) {
971
+ s.isDragging = false
972
+ s.dropAnim.active = true
973
+ s.dropAnim.y = s.dragOffset.y
974
+ s.dropAnim.velY = Math.min(60, Math.max(-100, s.dragVelocity.y))
975
+ s.dropAnim.squashX = 1
976
+ s.dropAnim.squashY = 1
977
+ s.dragOffset.x = 0
978
+ s.dragOffset.y = 0
979
+ s.tactileState = 'drop_bounce'
980
+ renderFace()
981
+ if (typeof onInteract === 'function') onInteract('drop_bounce')
982
+ return
983
+ }
984
+
985
+ // 2. Finish pet or tickle
986
+ if (s.tactileState === 'pet_enjoy' || s.tactileState === 'pet_purr') {
987
+ setTimeout(() => {
988
+ if (s.tactileState === 'pet_enjoy' || s.tactileState === 'pet_purr') {
989
+ s.tactileState = null
990
+ renderFace()
991
+ }
992
+ }, 1600)
993
+ return
994
+ }
995
+
996
+ if (s.tactileState === 'tickle') {
997
+ setTimeout(() => {
998
+ if (s.tactileState === 'tickle') {
999
+ s.tactileState = null
1000
+ renderFace()
1001
+ }
1002
+ }, 1200)
1003
+ return
1004
+ }
1005
+
1006
+ // 3. Short Click / Bonk Combo on Head / Visor (totalDist < 20)
1007
+ if (totalDist < 20) {
1008
+ const zone = s.pointerDownZone
1009
+ if (zone === 'head' || zone === 'visor') {
1010
+ const now = performance.now()
1011
+ if (now - s.lastBonkTime < 2400) {
1012
+ s.bonkCount++
1013
+ } else {
1014
+ s.bonkCount = 1
1015
+ }
1016
+ s.lastBonkTime = now
1017
+
1018
+ if (s.bonkCount <= 3) {
1019
+ // Stage 1: Gentle Bonk (Ketukan 1-3x)
1020
+ s.pokeVelocityY = 160
1021
+ s.velRotX = 60
1022
+ s.antVelLeft = 40
1023
+ s.antVelRight = -40
1024
+ setTactile('bonk_1', 1200)
1025
+ if (typeof onInteract === 'function') onInteract('bonk_1', { count: s.bonkCount })
1026
+ } else if (s.bonkCount <= 6) {
1027
+ // Stage 2: Annoyed Bonk (Ketukan 4-6x)
1028
+ s.pokeVelocityY = 200
1029
+ s.velRotX = 80
1030
+ s.velRotY = (Math.random() - 0.5) * 50
1031
+ setTactile('bonk_annoyed', 1600)
1032
+ if (typeof onInteract === 'function') onInteract('bonk_annoyed', { count: s.bonkCount })
1033
+ } else if (s.bonkCount <= 9) {
1034
+ // Stage 3: Dizzy Bonk (Ketukan 7-9x)
1035
+ s.pokeVelocityY = 140
1036
+ s.dizzyAngle = 0
1037
+ setTactile('bonk_dizzy', 2800)
1038
+ if (typeof onInteract === 'function') onInteract('bonk_dizzy', { count: s.bonkCount })
1039
+ } else {
1040
+ // Stage 4: Shield Block (Ketukan 10x+)
1041
+ setTactile('shield_block', 3200)
1042
+ if (typeof onInteract === 'function') onInteract('shield_block', { count: s.bonkCount })
1043
+ }
1044
+ } else if (zone === 'body' || zone === 'belly') {
1045
+ s.pokeVelocityY = 140
1046
+ s.velRotY += (Math.random() - 0.5) * 14
1047
+ if (typeof onInteract === 'function') onInteract('poke_body')
1048
+ }
1049
+
1050
+ if (typeof onClick === 'function') {
1051
+ onClick()
1052
+ }
1053
+ }
1054
+ },
1055
+ [onClick, onInteract, renderFace, setTactile]
1056
+ )
1057
+
1058
+ const handlePointerDown = useCallback(
1059
+ (e) => {
1060
+ const s = stateRef.current
1061
+ const zoneEl = e.target.closest('[data-zone]')
1062
+ const zone = zoneEl?.dataset?.zone || (e.clientY < 180 ? 'head' : 'body')
1063
+
1064
+ try {
1065
+ e.currentTarget.setPointerCapture(e.pointerId)
1066
+ } catch (_) {}
1067
+
1068
+ s.isPointerDown = true
1069
+ s.isDragging = false
1070
+ s.dragOffset.x = 0
1071
+ s.dragOffset.y = 0
1072
+ s.pointerDownZone = zone
1073
+ s.pointerDownPos = { x: e.clientX, y: e.clientY }
1074
+ s.lastPointerPos = { x: e.clientX, y: e.clientY }
1075
+ s.pointerDownTime = performance.now()
1076
+ s.petAccumulator = 0
1077
+ s.petStartTime = performance.now()
1078
+ s.tickleAccumulator = 0
1079
+
1080
+ // If Mark was sleeping, wake up with startle!
1081
+ if (s.isSleeping || s.status === 'sleeping') {
1082
+ s.isSleeping = false
1083
+ s.pokeVelocityY = -36
1084
+ s.antVelLeft = -35
1085
+ s.antVelRight = 35
1086
+ setTactile('startle_wake', 1600)
1087
+ if (typeof onInteract === 'function') onInteract('startle_wake')
1088
+ return
1089
+ }
1090
+
1091
+ // Zone-specific immediate tactile reactions
1092
+ if (zone === 'eye-left') {
1093
+ s.rotX = -12
1094
+ s.rotY = -8
1095
+ s.velRotX = -20
1096
+ setTactile('poke_eye_l', 1200)
1097
+ if (typeof onInteract === 'function') onInteract('poke_eye_l')
1098
+ } else if (zone === 'eye-right') {
1099
+ s.rotX = -12
1100
+ s.rotY = 8
1101
+ s.velRotX = -20
1102
+ setTactile('poke_eye_r', 1200)
1103
+ if (typeof onInteract === 'function') onInteract('poke_eye_r')
1104
+ } else if (zone === 'antenna-left') {
1105
+ s.antVelLeft = -80
1106
+ s.velRotZ = -10
1107
+ setTactile('poke_antenna', 1200)
1108
+ if (typeof onInteract === 'function') onInteract('poke_antenna_l')
1109
+ } else if (zone === 'antenna-right') {
1110
+ s.antVelRight = 80
1111
+ s.velRotZ = 10
1112
+ setTactile('poke_antenna', 1200)
1113
+ if (typeof onInteract === 'function') onInteract('poke_antenna_r')
1114
+ } else if (zone === 'core') {
1115
+ s.pokeVelocityY = -14
1116
+ s.corePulseScale = 2.4
1117
+ setTactile('poke_core', 1400)
1118
+ if (typeof onInteract === 'function') onInteract('poke_core')
1119
+ }
1120
+ },
1121
+ [onInteract, setTactile]
1122
+ )
1123
+
1124
+ const handlePointerMove = useCallback(
1125
+ (e) => {
1126
+ const s = stateRef.current
1127
+ if (!s.isPointerDown) return
1128
+
1129
+ // Safety check: if mouse button is not held, release immediately to prevent stuck drag
1130
+ if (e.buttons === 0) {
1131
+ handlePointerUp(e)
1132
+ return
1133
+ }
1134
+
1135
+ const dx = e.clientX - s.pointerDownPos.x
1136
+ const dy = e.clientY - s.pointerDownPos.y
1137
+ const stepDx = e.clientX - s.lastPointerPos.x
1138
+ const stepDy = e.clientY - s.lastPointerPos.y
1139
+ const stepDist = Math.hypot(stepDx, stepDy)
1140
+ s.lastPointerPos = { x: e.clientX, y: e.clientY }
1141
+
1142
+ const zone = s.pointerDownZone
1143
+
1144
+ // 1. Stroke / Petting detection on Head or Visor (only when NOT lifting)
1145
+ if (!s.isDragging && (zone === 'head' || zone === 'visor')) {
1146
+ if (Math.abs(stepDx) > 1.2 && Math.abs(dy) < 50) {
1147
+ s.petAccumulator += stepDist
1148
+ const petDuration = performance.now() - s.petStartTime
1149
+ if (s.petAccumulator > 70) {
1150
+ if (s.tactileTimer) clearTimeout(s.tactileTimer)
1151
+ if (petDuration > 2200) {
1152
+ if (s.tactileState !== 'pet_purr') {
1153
+ s.tactileState = 'pet_purr'
1154
+ renderFace()
1155
+ if (typeof onInteract === 'function') onInteract('pet_purr')
1156
+ }
1157
+ } else {
1158
+ if (s.tactileState !== 'pet_enjoy') {
1159
+ s.tactileState = 'pet_enjoy'
1160
+ renderFace()
1161
+ if (typeof onInteract === 'function') onInteract('petting')
1162
+ }
1163
+ }
1164
+ s.targetRotZ = stepDx * 0.4
1165
+ s.targetRotX = 4
1166
+ return
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ // 2. Tickle detection on Belly / Body (only when NOT lifting)
1172
+ if (!s.isDragging && (zone === 'belly' || zone === 'body')) {
1173
+ if (stepDist > 2.5 && Math.hypot(dx, dy) < 40) {
1174
+ s.tickleAccumulator += stepDist
1175
+ if (s.tickleAccumulator > 85) {
1176
+ if (s.tactileState !== 'tickle') {
1177
+ s.tactileState = 'tickle'
1178
+ renderFace()
1179
+ if (typeof onInteract === 'function') onInteract('tickle')
1180
+ }
1181
+ s.velRotY += (Math.random() - 0.5) * 22
1182
+ return
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ // 3. Vertical Lift only: Dragging UPWARDS (dy < -25) and dominantly vertical
1188
+ if (
1189
+ !s.isDragging &&
1190
+ dy < -25 &&
1191
+ Math.abs(dy) > Math.abs(dx) * 1.1 &&
1192
+ s.tactileState !== 'pet_enjoy' &&
1193
+ s.tactileState !== 'pet_purr' &&
1194
+ s.tactileState !== 'tickle'
1195
+ ) {
1196
+ s.isDragging = true
1197
+ s.tactileState = 'drag_lift'
1198
+ renderFace()
1199
+ if (typeof onInteract === 'function') onInteract('drag_lift')
1200
+ }
1201
+
1202
+ if (s.isDragging) {
1203
+ // Lock X strictly to 0 so avatar NEVER moves sideways or flies away
1204
+ s.dragOffset.x = 0
1205
+ // Clamp vertical lift between -140px and 0px
1206
+ s.dragOffset.y = Math.max(-140, Math.min(0, dy))
1207
+ s.dragVelocity.x = 0
1208
+ s.dragVelocity.y = Math.max(-400, Math.min(400, (stepDy / 0.016) * 0.2))
1209
+ }
1210
+ },
1211
+ [handlePointerUp, onInteract, renderFace]
1212
+ )
1213
+
1214
+ const handlePointerCancel = useCallback(
1215
+ (e) => {
1216
+ handlePointerUp(e)
1217
+ },
1218
+ [handlePointerUp]
1219
+ )
1220
+
1221
+ // Global window pointer listener to guarantee release is never lost
1222
+ useEffect(() => {
1223
+ const handleGlobalUp = (e) => {
1224
+ if (stateRef.current.isPointerDown) {
1225
+ handlePointerUp(e)
1226
+ }
1227
+ }
1228
+ window.addEventListener('pointerup', handleGlobalUp)
1229
+ window.addEventListener('pointercancel', handleGlobalUp)
1230
+ return () => {
1231
+ window.removeEventListener('pointerup', handleGlobalUp)
1232
+ window.removeEventListener('pointercancel', handleGlobalUp)
1233
+ }
1234
+ }, [handlePointerUp])
1235
+
1236
+ // Main 60 FPS requestAnimationFrame physics ticker
1237
+ useEffect(() => {
1238
+ let animId
1239
+ let prevTime = performance.now()
1240
+ stateRef.current.lastQuirkTime = performance.now()
1241
+
1242
+ renderFace()
1243
+
1244
+ const updateFrame = (now) => {
1245
+ const dt = Math.min(0.05, (now - prevTime) / 1000)
1246
+ prevTime = now
1247
+ const t = now * 0.001
1248
+ const s = stateRef.current
1249
+ const effectiveStatus = s.tactileState || (s.isSleeping ? 'sleeping' : s.status)
1250
+
1251
+ // 1. Autonomous quirks
1252
+ updateAutonomousQuirks(now)
1253
+
1254
+ // 2. Eye saccades lerp
1255
+ if (s.tactileState === 'bonk_dizzy') {
1256
+ s.targetSaccadeX = Math.sin(t * 10) * 4
1257
+ s.targetSaccadeY = Math.cos(t * 10) * 4
1258
+ } else if (s.status === 'speaking') {
1259
+ s.targetSaccadeX = 0
1260
+ s.targetSaccadeY = 0
1261
+ } else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
1262
+ s.targetSaccadeX = (s.listeningSide || 1) * 3
1263
+ s.targetSaccadeY = -1
1264
+ }
1265
+ const saccadeSpeed = s.status === 'speaking' ? 0.35 : 0.16
1266
+ s.eyeSaccadeX += (s.targetSaccadeX - s.eyeSaccadeX) * saccadeSpeed
1267
+ s.eyeSaccadeY += (s.targetSaccadeY - s.eyeSaccadeY) * saccadeSpeed
1268
+
1269
+ // 3. Multi-Harmonic organic breathing & sway
1270
+ let breathSpeed = 1.6
1271
+ let breathDepth = 7.5
1272
+ let swayDepth = 3.5
1273
+ let rollDepth = 1.0
1274
+
1275
+ if (s.tactileState === 'pet_purr') {
1276
+ breathSpeed = 3.2
1277
+ breathDepth = 3.5
1278
+ swayDepth = 1.0
1279
+ s.purrJitter.x = (Math.random() - 0.5) * 3.0
1280
+ s.purrJitter.y = (Math.random() - 0.5) * 3.0
1281
+ } else if (effectiveStatus === 'sleeping') {
1282
+ breathSpeed = 0.8
1283
+ breathDepth = 4.5
1284
+ swayDepth = 1.2
1285
+ } else if (effectiveStatus === 'speaking') {
1286
+ breathSpeed = 2.2
1287
+ breathDepth = 6.5
1288
+ swayDepth = 0
1289
+ rollDepth = 0
1290
+ } else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
1291
+ breathSpeed = 1.3
1292
+ breathDepth = 5.5
1293
+ swayDepth = 1.2
1294
+ rollDepth = 0.5
1295
+ } else if (effectiveStatus === 'music_groove') {
1296
+ breathSpeed = 2.4
1297
+ breathDepth = 9.0
1298
+ swayDepth = 8.0
1299
+ rollDepth = 2.5
1300
+ } else if (s.isSeeingDesktop) {
1301
+ breathSpeed = 1.8
1302
+ breathDepth = 5
1303
+ } else if (effectiveStatus === 'thinking') {
1304
+ breathSpeed = 1.2
1305
+ }
1306
+
1307
+ // Continuous phase accumulation
1308
+ s.breathPhase = (s.breathPhase + dt * breathSpeed) % (Math.PI * 2)
1309
+ s.swayPhase = (s.swayPhase + dt * breathSpeed * 0.65) % (Math.PI * 2)
1310
+
1311
+ // Lerp depth amplitudes smoothly
1312
+ s.currentBreathDepth += (breathDepth - s.currentBreathDepth) * dt * 3.5
1313
+ s.currentSwayDepth += (swayDepth - s.currentSwayDepth) * dt * 3.5
1314
+ s.currentRollDepth += (rollDepth - s.currentRollDepth) * dt * 3.5
1315
+
1316
+ const harmonicY = Math.sin(s.breathPhase) * s.currentBreathDepth
1317
+ const harmonicX = Math.cos(s.swayPhase) * s.currentSwayDepth
1318
+ const harmonicRoll = Math.sin(s.breathPhase * 0.5) * s.currentRollDepth
1319
+
1320
+ const breathScaleY = 1 + harmonicY * 0.0016
1321
+ const breathScaleX = 1 - harmonicY * 0.0008
1322
+
1323
+ // Damped quirk altitude offset
1324
+ s.quirkAltitudeOffset += (s.targetAltitudeOffset - s.quirkAltitudeOffset) * dt * 4
1325
+
1326
+ // Poke spring dampening (physically accurate damped harmonic oscillator)
1327
+ const springK = 150
1328
+ const dampC = 14
1329
+ s.pokeVelocityY += (-springK * s.pokeOffsetY - dampC * s.pokeVelocityY) * dt
1330
+ s.pokeOffsetY += s.pokeVelocityY * dt
1331
+
1332
+ // Damped desktop look transitions
1333
+ const targetDesktopForwardZ = s.isSeeingDesktop ? 35 : 0
1334
+ const targetDesktopDownwardY = s.isSeeingDesktop ? -12 : 0
1335
+ s.desktopForwardZ += (targetDesktopForwardZ - s.desktopForwardZ) * dt * 6
1336
+ s.desktopDownwardY += (targetDesktopDownwardY - s.desktopDownwardY) * dt * 6
1337
+
1338
+ // Dizzy spiral rotation
1339
+ if (s.tactileState === 'bonk_dizzy') {
1340
+ s.dizzyAngle = (s.dizzyAngle + dt * 360) % 360
1341
+ renderFace()
1342
+ }
1343
+
1344
+ // Shield progress lerping
1345
+ const targetShield = s.tactileState === 'shield_block' ? 1.0 : 0.0
1346
+ s.shieldProgress += (targetShield - s.shieldProgress) * dt * 10
1347
+
1348
+ // Drop bounce physics (anti-gravity magnetic suspension spring - smooth boing-boing without gepeng deformation)
1349
+ if (s.dropAnim.active) {
1350
+ const springK = 85
1351
+ const dampC = 9
1352
+ const accelY = -springK * s.dropAnim.y - dampC * s.dropAnim.velY
1353
+ s.dropAnim.velY += accelY * dt
1354
+ s.dropAnim.y += s.dropAnim.velY * dt
1355
+
1356
+ // Subtle antenna inertia during drop bounce
1357
+ s.antVelLeft += s.dropAnim.velY * 0.08
1358
+ s.antVelRight -= s.dropAnim.velY * 0.08
1359
+
1360
+ // Check if settled back to hover level
1361
+ if (Math.abs(s.dropAnim.y) < 0.6 && Math.abs(s.dropAnim.velY) < 6) {
1362
+ s.dropAnim.y = 0
1363
+ s.dropAnim.velY = 0
1364
+ s.dropAnim.active = false
1365
+ if (s.tactileState === 'drop_bounce') {
1366
+ s.tactileState = null
1367
+ renderFace()
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ // 4. Spring-damper for head rotation (Pitch, Yaw, Roll)
1373
+ let addPitch = effectiveStatus === 'sleeping' ? 14 : 0
1374
+ let addRoll = effectiveStatus === 'thinking' ? 7 : s.mood === 'joy' ? 3 : 0
1375
+
1376
+ let effectiveTargetX = s.targetRotX + s.quirkPitchOffset + addPitch
1377
+ let effectiveTargetY = s.targetRotY
1378
+ let effectiveTargetZ = s.targetRotZ + s.quirkRollOffset + addRoll
1379
+
1380
+ if (s.tactileState === 'bonk_dizzy') {
1381
+ effectiveTargetX = Math.sin(t * 6) * 12
1382
+ effectiveTargetY = Math.cos(t * 6) * 18
1383
+ effectiveTargetZ = Math.sin(t * 6) * 14
1384
+ } else if (effectiveStatus === 'music_groove') {
1385
+ effectiveTargetX = Math.sin(t * 8) * 10
1386
+ effectiveTargetY = Math.sin(t * 4) * 8
1387
+ effectiveTargetZ = Math.sin(t * 4) * 5
1388
+ } else if (s.isSeeingDesktop) {
1389
+ addPitch = -16
1390
+ effectiveTargetX = -14 + addPitch
1391
+ effectiveTargetY = Math.sin(t * 1.8) * 4
1392
+ effectiveTargetZ = 0
1393
+ } else if (effectiveStatus === 'speaking') {
1394
+ const speakNod = Math.sin(t * 7.5) * (s.intensity * 2.2)
1395
+ addPitch = -1.5 + speakNod
1396
+ effectiveTargetX = addPitch
1397
+ effectiveTargetY = 0
1398
+ effectiveTargetZ = 0
1399
+ } else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
1400
+ const side = s.listeningSide || 1
1401
+ effectiveTargetX = -4
1402
+ effectiveTargetY = side * 20
1403
+ effectiveTargetZ = side * -6
1404
+ }
1405
+
1406
+ // Spring damper calculation
1407
+ s.velRotX += ((effectiveTargetX - s.rotX) * 140 - s.velRotX * 16) * dt
1408
+ s.velRotY += ((effectiveTargetY - s.rotY) * 140 - s.velRotY * 16) * dt
1409
+ s.velRotZ += ((effectiveTargetZ - s.rotZ) * 140 - s.velRotZ * 16) * dt
1410
+ s.rotX += s.velRotX * dt
1411
+ s.rotY += s.velRotY * dt
1412
+ s.rotZ += s.velRotZ * dt
1413
+
1414
+ // 5. Apply Rigid Compound Transforms around Throat Pivot
1415
+ let curHarmonicX = harmonicX
1416
+ let curHarmonicY = harmonicY + s.pokeOffsetY + s.desktopDownwardY + s.quirkAltitudeOffset
1417
+ let scaleX = breathScaleX
1418
+ let scaleY = breathScaleY
1419
+
1420
+ if (s.isDragging) {
1421
+ curHarmonicX = 0
1422
+ curHarmonicY = s.dragOffset.y
1423
+ } else if (s.dropAnim.active) {
1424
+ curHarmonicX = harmonicX
1425
+ curHarmonicY = harmonicY + s.dropAnim.y
1426
+ }
1427
+
1428
+ if (s.tactileState === 'pet_purr') {
1429
+ curHarmonicX += s.purrJitter.x
1430
+ curHarmonicY += s.purrJitter.y
1431
+ }
1432
+
1433
+ if (robotRootRef.current) {
1434
+ robotRootRef.current.style.transform = `translate3d(${curHarmonicX}px, ${curHarmonicY}px, ${s.desktopForwardZ}px) scale3d(${scaleX}, ${scaleY}, 1)`
1435
+ }
1436
+
1437
+ // Head: Rotates smoothly with pitch, yaw, and harmonic roll
1438
+ if (robotHeadRef.current) {
1439
+ robotHeadRef.current.style.transform = `rotateX(${s.rotX}deg) rotateY(${s.rotY}deg) rotateZ(${s.rotZ + harmonicRoll}deg)`
1440
+ }
1441
+
1442
+ // Torso: Follows directly beneath the throat socket
1443
+ if (robotBodyRef.current) {
1444
+ const torsoRotX = s.rotX * 0.35
1445
+ const torsoRotY = effectiveStatus === 'speaking' ? 0 : s.rotY * 0.35
1446
+ robotBodyRef.current.style.transform = `translateZ(-5px) rotateX(${torsoRotX}deg) rotateY(${torsoRotY}deg)`
1447
+ }
1448
+
1449
+ // Ground Shadow
1450
+ if (groundShadowRef.current) {
1451
+ const liftAmount = Math.max(0, -curHarmonicY)
1452
+ const shadowScale = Math.max(0.4, 1.0 - liftAmount * 0.0035)
1453
+ const shadowOpacity = Math.max(0.15, 0.75 - liftAmount * 0.004)
1454
+ groundShadowRef.current.style.transform = `translateZ(-60px) rotateX(75deg) scale(${shadowScale})`
1455
+ groundShadowRef.current.style.opacity = shadowOpacity
1456
+ }
1457
+
1458
+ // 6. Ears Enlarge & Acoustic Pulse Animation
1459
+ const targetEarScale =
1460
+ effectiveStatus === 'listening' || effectiveStatus === 'hearing'
1461
+ ? 1.55 + Math.sin(t * 6) * 0.04 + (s.intensity || 0) * 0.35
1462
+ : 1.0
1463
+ s.earScale += (targetEarScale - s.earScale) * dt * 10
1464
+
1465
+ if (earLeftRef.current) {
1466
+ earLeftRef.current.style.transform = `scale(${s.earScale})`
1467
+ }
1468
+ if (earRightRef.current) {
1469
+ earRightRef.current.style.transform = `scale(${s.earScale})`
1470
+ }
1471
+ if (earGlowLeftRef.current && earGlowRightRef.current) {
1472
+ const earOpacity =
1473
+ effectiveStatus === 'listening' || effectiveStatus === 'hearing'
1474
+ ? Math.min(1.0, 0.75 + (s.intensity || 0) * 0.5)
1475
+ : 0.25
1476
+ earGlowLeftRef.current.style.opacity = earOpacity
1477
+ earGlowRightRef.current.style.opacity = earOpacity
1478
+ }
1479
+
1480
+ // 7. Antenna Spring Damping
1481
+ let antTargetLeft = -s.velRotY * 0.35
1482
+ let antTargetRight = -s.velRotY * 0.35
1483
+
1484
+ if (effectiveStatus === 'speaking') {
1485
+ antTargetLeft = Math.sin(t * 6) * (s.intensity * 6)
1486
+ antTargetRight = -Math.sin(t * 6) * (s.intensity * 6)
1487
+ } else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
1488
+ const side = s.listeningSide || 1
1489
+ if (side === -1) {
1490
+ antTargetLeft = 24
1491
+ antTargetRight = -6
1492
+ } else {
1493
+ antTargetLeft = 6
1494
+ antTargetRight = -24
1495
+ }
1496
+ } else if (effectiveStatus === 'music_groove') {
1497
+ antTargetLeft = Math.sin(t * 8) * 20
1498
+ antTargetRight = -Math.sin(t * 8) * 20
1499
+ } else if (effectiveStatus === 'thinking') {
1500
+ antTargetLeft = -s.velRotY * 0.35 - 15
1501
+ antTargetRight = -s.velRotY * 0.35 + 15
1502
+ }
1503
+
1504
+ s.antVelLeft += (antTargetLeft - s.antLeftRot) * 200 * dt - s.antVelLeft * 14 * dt
1505
+ s.antVelRight += (antTargetRight - s.antRightRot) * 200 * dt - s.antVelRight * 14 * dt
1506
+ s.antLeftRot += s.antVelLeft * dt
1507
+ s.antRightRot += s.antVelRight * dt
1508
+
1509
+ if (antLeftRef.current) antLeftRef.current.style.transform = `rotate(${s.antLeftRot}deg)`
1510
+ if (antRightRef.current) antRightRef.current.style.transform = `rotate(${s.antRightRot}deg)`
1511
+
1512
+ // 8. Floating Hand Physics
1513
+ let targetHandLY = Math.sin(s.breathPhase * 0.9) * 5
1514
+ let targetHandLX = Math.cos(s.breathPhase * 0.9) * 2.5
1515
+ let targetHandRY = Math.sin(s.breathPhase * 0.9 + 1.2) * 5
1516
+ let targetHandRX = Math.cos(s.breathPhase * 0.9 + 1.2) * 2.5
1517
+ let handRotL = -s.rotY * 0.2
1518
+ let handRotR = -s.rotY * 0.2
1519
+
1520
+ if (s.shieldProgress > 0.05) {
1521
+ targetHandLX = 42 * s.shieldProgress
1522
+ targetHandLY = -68 * s.shieldProgress
1523
+ targetHandRX = -42 * s.shieldProgress
1524
+ targetHandRY = -68 * s.shieldProgress
1525
+ handRotL = 42 * s.shieldProgress
1526
+ handRotR = -42 * s.shieldProgress
1527
+ } else if (s.isDragging) {
1528
+ targetHandLY = -30 + Math.sin(t * 14) * 12
1529
+ targetHandLX = 15 + Math.cos(t * 14) * 10
1530
+ targetHandRY = -30 - Math.sin(t * 14) * 12
1531
+ targetHandRX = -15 - Math.cos(t * 14) * 10
1532
+ } else if (effectiveStatus === 'processing') {
1533
+ targetHandLY = -26 + Math.sin(t * 14) * 6
1534
+ targetHandRY = -26 + Math.cos(t * 14) * 6
1535
+ targetHandLX = 12 + Math.sin(t * 10) * 4
1536
+ targetHandRX = -12 - Math.sin(t * 10) * 4
1537
+ } else if (effectiveStatus === 'music_groove') {
1538
+ targetHandLY = -15 + Math.sin(t * 8) * 12
1539
+ targetHandRY = -15 - Math.sin(t * 8) * 12
1540
+ } else if (s.isSeeingDesktop) {
1541
+ targetHandLY = -40
1542
+ targetHandLX = 12
1543
+ targetHandRY = -40
1544
+ targetHandRX = -12
1545
+ } else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
1546
+ const side = s.listeningSide || 1
1547
+ if (side === -1) {
1548
+ targetHandLY = -20
1549
+ targetHandLX = 14
1550
+ targetHandRY = 2
1551
+ targetHandRX = -3
1552
+ } else {
1553
+ targetHandRY = -20
1554
+ targetHandRX = -14
1555
+ targetHandLY = 2
1556
+ targetHandLX = 3
1557
+ }
1558
+ } else if (effectiveStatus === 'thinking') {
1559
+ targetHandLY = -34
1560
+ targetHandLX = 16
1561
+ } else if (effectiveStatus === 'speaking') {
1562
+ const voicePulse = Math.sin(t * 8) * (s.intensity * 12)
1563
+ targetHandLY = -10 + voicePulse
1564
+ targetHandLX = -4
1565
+ targetHandRY = -10 - voicePulse
1566
+ targetHandRX = 4
1567
+ } else if (effectiveStatus === 'sleeping') {
1568
+ targetHandLY = 18
1569
+ targetHandRY = 18
1570
+ } else if (s.mood === 'joy') {
1571
+ targetHandLY -= 8
1572
+ targetHandRY -= 8
1573
+ }
1574
+
1575
+ s.handLeftY += (targetHandLY - s.handLeftY) * 0.2
1576
+ s.handLeftX += (targetHandLX - s.handLeftX) * 0.2
1577
+ s.handRightY += (targetHandRY - s.handRightY) * 0.2
1578
+ s.handRightX += (targetHandRX - s.handRightX) * 0.2
1579
+
1580
+ if (handLeftRef.current) {
1581
+ handLeftRef.current.style.transform = `translate3d(${s.handLeftX}px, ${s.handLeftY}px, ${
1582
+ s.shieldProgress > 0.05 ? 40 : s.isSeeingDesktop ? 30 : 18
1583
+ }px) rotateZ(${handRotL}deg)`
1584
+ }
1585
+ if (handRightRef.current) {
1586
+ handRightRef.current.style.transform = `translate3d(${s.handRightX}px, ${s.handRightY}px, ${
1587
+ s.shieldProgress > 0.05 ? 40 : s.isSeeingDesktop ? 30 : 18
1588
+ }px) rotateZ(${handRotR}deg)`
1589
+ }
1590
+
1591
+ // 9. Eyes Parallax
1592
+ if (eyesContainerRef.current) {
1593
+ const finalEyeX = effectiveStatus === 'speaking' ? 0 : s.rotY * 0.35 + s.eyeSaccadeX
1594
+ const finalEyeY = effectiveStatus === 'speaking' ? 0 : -s.rotX * 0.25 + s.eyeSaccadeY
1595
+ eyesContainerRef.current.style.transform = `translate3d(${finalEyeX}px, ${finalEyeY}px, 10px)`
1596
+ }
1597
+
1598
+ // 10. Thruster visual during dragging
1599
+ if (thrusterRef.current) {
1600
+ if (s.isDragging) {
1601
+ thrusterRef.current.style.opacity = '1'
1602
+ thrusterRef.current.style.transform = `translateZ(-35px) scale(${1.8 + Math.sin(t * 22) * 0.35})`
1603
+ thrusterRef.current.style.backgroundColor = '#38bdf8'
1604
+ } else {
1605
+ thrusterRef.current.style.opacity = '0.25'
1606
+ thrusterRef.current.style.transform = 'translateZ(-35px) scale(1)'
1607
+ thrusterRef.current.style.backgroundColor = 'rgba(255, 255, 255, 0.25)'
1608
+ }
1609
+ }
1610
+
1611
+ // Update face elements if speaking or dynamic
1612
+ if (effectiveStatus === 'speaking') {
1613
+ renderFace()
1614
+ }
1615
+
1616
+ animId = requestAnimationFrame(updateFrame)
1617
+ }
1618
+
1619
+ animId = requestAnimationFrame(updateFrame)
1620
+ return () => cancelAnimationFrame(animId)
1621
+ }, [renderFace, updateAutonomousQuirks])
1622
+
1623
+ return (
1624
+ <div
1625
+ className={`relative w-[320px] h-[440px] [perspective:1000px] select-none touch-none ${className}`}
1626
+ title="MARK AI Companion"
1627
+ >
1628
+ <style>{`
1629
+ @keyframes avatarScanline {
1630
+ 0% { transform: translateY(-100%); }
1631
+ 100% { transform: translateY(160%); }
1632
+ }
1633
+ @keyframes avatarGlitchShake {
1634
+ 0% { transform: translate(0, 0); }
1635
+ 25% { transform: translate(-3px, 2px); }
1636
+ 50% { transform: translate(3px, -2px); }
1637
+ 75% { transform: translate(-2px, -1px); }
1638
+ 100% { transform: translate(0, 0); }
1639
+ }
1640
+ @keyframes avatarCoreHeartbeat {
1641
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
1642
+ 15% { transform: scale(1.2); opacity: 1; }
1643
+ 30% { transform: scale(1.05); opacity: 0.92; }
1644
+ 45% { transform: scale(1.14); opacity: 0.98; }
1645
+ 60% { transform: scale(1); opacity: 0.9; }
1646
+ }
1647
+ .avatar-preserve-3d {
1648
+ transform-style: preserve-3d;
1649
+ }
1650
+ .avatar-core-beat {
1651
+ animation: avatarCoreHeartbeat 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1652
+ }
1653
+ .avatar-eye-mesh {
1654
+ transition: transform 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
1655
+ transform-origin: center center;
1656
+ }
1657
+ `}</style>
1658
+
1659
+ {/* Robot Root Pivot (Handles organic breathing, floating, & tactile interactions) */}
1660
+ <div
1661
+ ref={robotRootRef}
1662
+ className="relative w-full h-full avatar-preserve-3d cursor-pointer"
1663
+ onPointerDown={handlePointerDown}
1664
+ onPointerMove={handlePointerMove}
1665
+ onPointerUp={handlePointerUp}
1666
+ onPointerCancel={handlePointerCancel}
1667
+ >
1668
+ {/* ── 1. UNIFIED 3D HEAD ASSEMBLY (Throat pivot joint 130px 165px) ── */}
1669
+ <div
1670
+ ref={robotHeadRef}
1671
+ className="absolute left-[30px] top-[18px] w-[260px] h-[180px] avatar-preserve-3d"
1672
+ style={{ transformOrigin: '130px 165px' }}
1673
+ >
1674
+ {/* Background Helmet Shell & Antennas (Single unified SVG with 3D gradients) */}
1675
+ <svg
1676
+ viewBox="0 0 260 180"
1677
+ className="absolute inset-0 w-full h-full pointer-events-none drop-shadow-[0_16px_36px_rgba(0,0,0,0.9)]"
1678
+ style={{ overflow: 'visible' }}
1679
+ >
1680
+ <defs>
1681
+ <radialGradient id="avatarHelmetGrad" cx="38%" cy="32%" r="68%">
1682
+ <stop offset="0%" stopColor="#243f30" />
1683
+ <stop offset="45%" stopColor="#14241c" />
1684
+ <stop offset="100%" stopColor="#080e0a" />
1685
+ </radialGradient>
1686
+ <linearGradient id="avatarHelmetRim" x1="0" y1="0" x2="0" y2="1">
1687
+ <stop offset="0%" stopColor="#2a4738" />
1688
+ <stop offset="100%" stopColor="#14241c" />
1689
+ </linearGradient>
1690
+ <filter id="avatar-neon-glow" x="-20%" y="-20%" width="140%" height="140%">
1691
+ <feGaussianBlur stdDeviation="3" result="blur" />
1692
+ <feComposite in="SourceGraphic" in2="blur" operator="over" />
1693
+ </filter>
1694
+ </defs>
1695
+
1696
+ {/* Left Wing Antenna */}
1697
+ <g
1698
+ ref={antLeftRef}
1699
+ data-zone="antenna-left"
1700
+ className="origin-[55px_90px] cursor-pointer pointer-events-auto"
1701
+ >
1702
+ <path
1703
+ d="M 55 90 L 16 40 L 22 24 L 46 48 Z"
1704
+ fill="#13221b"
1705
+ stroke="#19362d"
1706
+ strokeWidth="2.5"
1707
+ />
1708
+ <circle
1709
+ ref={antBulbLeftRef}
1710
+ cx="19"
1711
+ cy="32"
1712
+ r="7"
1713
+ fill="#ffffff"
1714
+ filter="url(#avatar-neon-glow)"
1715
+ opacity="0.92"
1716
+ />
1717
+ <circle cx="19" cy="32" r="2.5" fill="#ffffff" opacity="0.98" />
1718
+ </g>
1719
+
1720
+ {/* Right Wing Antenna */}
1721
+ <g
1722
+ ref={antRightRef}
1723
+ data-zone="antenna-right"
1724
+ className="origin-[205px_90px] cursor-pointer pointer-events-auto"
1725
+ >
1726
+ <path
1727
+ d="M 205 90 L 244 40 L 238 24 L 214 48 Z"
1728
+ fill="#13221b"
1729
+ stroke="#19362d"
1730
+ strokeWidth="2.5"
1731
+ />
1732
+ <circle
1733
+ ref={antBulbRightRef}
1734
+ cx="241"
1735
+ cy="32"
1736
+ r="7"
1737
+ fill="#ffffff"
1738
+ filter="url(#avatar-neon-glow)"
1739
+ opacity="0.92"
1740
+ />
1741
+ <circle cx="241" cy="32" r="2.5" fill="#ffffff" opacity="0.98" />
1742
+ </g>
1743
+
1744
+ {/* Main Helmet Curved 3D Head Shell (Sleek, Clean, No badges/text) */}
1745
+ <ellipse
1746
+ data-zone="head"
1747
+ cx="130"
1748
+ cy="95"
1749
+ rx="78"
1750
+ ry="70"
1751
+ fill="url(#avatarHelmetGrad)"
1752
+ stroke="url(#avatarHelmetRim)"
1753
+ strokeWidth="3"
1754
+ className="cursor-pointer pointer-events-auto"
1755
+ />
1756
+
1757
+ {/* Chin Bevel (Hugs neck seamlessly) */}
1758
+ <path d="M 104 156 Q 130 166 156 156 L 150 164 Q 130 170 110 164 Z" fill="#080e0a" />
1759
+
1760
+ {/* Left Earphone Side Pod (Enlarges & illuminates during listening) */}
1761
+ <g ref={earLeftRef} className="origin-[52px_95px] avatar-preserve-3d">
1762
+ <rect
1763
+ x="47"
1764
+ y="78"
1765
+ width="10"
1766
+ height="34"
1767
+ rx="5"
1768
+ fill="#19362d"
1769
+ stroke="#254a3e"
1770
+ strokeWidth="2"
1771
+ />
1772
+ <rect
1773
+ ref={earGlowLeftRef}
1774
+ x="50"
1775
+ y="84"
1776
+ width="4"
1777
+ height="22"
1778
+ rx="2"
1779
+ fill="#ffffff"
1780
+ className="transition-opacity duration-300"
1781
+ filter="url(#avatar-neon-glow)"
1782
+ opacity="0.35"
1783
+ />
1784
+ </g>
1785
+
1786
+ {/* Right Earphone Side Pod (Enlarges & illuminates during listening) */}
1787
+ <g ref={earRightRef} className="origin-[208px_95px] avatar-preserve-3d">
1788
+ <rect
1789
+ x="203"
1790
+ y="78"
1791
+ width="10"
1792
+ height="34"
1793
+ rx="5"
1794
+ fill="#19362d"
1795
+ stroke="#254a3e"
1796
+ strokeWidth="2"
1797
+ />
1798
+ <rect
1799
+ ref={earGlowRightRef}
1800
+ x="206"
1801
+ y="84"
1802
+ width="4"
1803
+ height="22"
1804
+ rx="2"
1805
+ fill="#ffffff"
1806
+ className="transition-opacity duration-300"
1807
+ filter="url(#avatar-neon-glow)"
1808
+ opacity="0.35"
1809
+ />
1810
+ </g>
1811
+
1812
+ {/* Forehead Ambient Light Sensor Dot */}
1813
+ <circle cx="130" cy="36" r="3" fill="#ffffff" opacity="0.8" />
1814
+ </svg>
1815
+
1816
+ {/* Deep Curved 3D Glass Visor Screen */}
1817
+ <div
1818
+ data-zone="visor"
1819
+ className="absolute left-[56px] top-[40px] w-[148px] h-[108px] rounded-[34px] bg-gradient-to-b from-[#0a140f] to-[#040806] border-2 border-[#19362d] overflow-hidden shadow-[inset_0_4px_22px_rgba(0,0,0,0.98)] avatar-preserve-3d flex items-center justify-center cursor-pointer pointer-events-auto"
1820
+ style={{ transform: 'translateZ(16px)' }}
1821
+ >
1822
+ {/* Minimalist Desktop Wireframe Reflection (Active during Awareness / Melihat Layar) */}
1823
+ <div
1824
+ ref={desktopReflectionRef}
1825
+ className="absolute inset-0 pointer-events-none opacity-0 transition-opacity duration-500 flex flex-col p-3 z-0"
1826
+ >
1827
+ <div className="w-full h-3 border-b border-[#1fb854]/30 flex items-center justify-between px-1 pb-1">
1828
+ <div className="flex gap-1">
1829
+ <div className="w-1.5 h-1.5 rounded-full bg-[#1fb854]/50" />
1830
+ <div className="w-1.5 h-1.5 rounded-full bg-[#1fb854]/30" />
1831
+ </div>
1832
+ <div className="w-6 h-1 rounded-full bg-[#1fb854]/25" />
1833
+ </div>
1834
+ <div className="flex-1 flex flex-col gap-1 pt-1.5 px-0.5 opacity-40">
1835
+ <div className="w-3/4 h-1 rounded bg-[#1fb854]/40" />
1836
+ <div className="w-1/2 h-1 rounded bg-[#1fb854]/30" />
1837
+ <div className="w-5/6 h-1 rounded bg-[#1fb854]/40" />
1838
+ <div className="w-2/3 h-1 rounded bg-[#1fb854]/30" />
1839
+ <div className="w-4/5 h-1 rounded bg-[#1fb854]/40" />
1840
+ </div>
1841
+ <div className="absolute inset-x-0 top-0 h-4 bg-gradient-to-b from-[#1fb854]/15 to-transparent pointer-events-none animate-[avatarScanline_2.5s_linear_infinite]" />
1842
+ </div>
1843
+
1844
+ {/* Specular Curved Highlight */}
1845
+ <div className="absolute -top-10 -left-10 w-56 h-30 bg-gradient-to-b from-white/12 via-white/3 to-transparent rotate-[-22deg] pointer-events-none rounded-full blur-[1px]" />
1846
+
1847
+ {/* LED Cyber Face (Elastic Eyes + Animated Mouth) */}
1848
+ <div
1849
+ className="relative w-full h-full flex flex-col items-center justify-center z-10 avatar-preserve-3d"
1850
+ style={{ transform: 'translateZ(10px)' }}
1851
+ >
1852
+ {/* Eyes Container */}
1853
+ <div
1854
+ ref={eyesContainerRef}
1855
+ className="flex items-center justify-center gap-7 pointer-events-auto"
1856
+ >
1857
+ <div
1858
+ ref={leftEyeRef}
1859
+ data-zone="eye-left"
1860
+ className="w-10 h-10 flex items-center justify-center avatar-eye-mesh cursor-pointer pointer-events-auto"
1861
+ />
1862
+ <div
1863
+ ref={rightEyeRef}
1864
+ data-zone="eye-right"
1865
+ className="w-10 h-10 flex items-center justify-center avatar-eye-mesh cursor-pointer pointer-events-auto"
1866
+ />
1867
+ </div>
1868
+
1869
+ {/* Mouth Container */}
1870
+ <div ref={mouthRef} className="h-5 flex items-center justify-center mt-1" />
1871
+ </div>
1872
+ </div>
1873
+ </div>
1874
+
1875
+ {/* ── 2. BODY CHASSIS & INTEGRATED NECK COLLAR (Centered: X: 95px, Y: 172px) ── */}
1876
+ <div
1877
+ ref={robotBodyRef}
1878
+ className="absolute left-[95px] top-[172px] w-[130px] h-[126px] avatar-preserve-3d"
1879
+ style={{
1880
+ transformOrigin: '65px 12px',
1881
+ transform: 'translateZ(-5px)'
1882
+ }}
1883
+ >
1884
+ <svg
1885
+ viewBox="0 0 130 126"
1886
+ className="w-full h-full drop-shadow-[0_12px_30px_rgba(0,0,0,0.85)]"
1887
+ >
1888
+ <defs>
1889
+ <radialGradient id="avatarBodyGrad" cx="50%" cy="40%" r="65%">
1890
+ <stop offset="0%" stopColor="#1a2e23" />
1891
+ <stop offset="60%" stopColor="#13221b" />
1892
+ <stop offset="100%" stopColor="#080e0a" />
1893
+ </radialGradient>
1894
+ </defs>
1895
+
1896
+ {/* Integrated Cyber Neck Socket */}
1897
+ <ellipse
1898
+ cx="65"
1899
+ cy="14"
1900
+ rx="26"
1901
+ ry="9"
1902
+ fill="#0d1611"
1903
+ stroke="#19362d"
1904
+ strokeWidth="2.5"
1905
+ />
1906
+ <ellipse cx="65" cy="14" rx="16" ry="5" fill="#080e0a" />
1907
+
1908
+ {/* Body Outer Shell */}
1909
+ <path
1910
+ data-zone="body"
1911
+ d="M 32 30 L 98 30 C 114 30, 120 50, 114 90 C 108 113, 88 120, 65 120 C 42 120, 22 113, 16 90 C 10 50, 16 30, 32 30 Z"
1912
+ fill="url(#avatarBodyGrad)"
1913
+ stroke="#19362d"
1914
+ strokeWidth="3"
1915
+ className="cursor-pointer pointer-events-auto"
1916
+ />
1917
+
1918
+ {/* Belly Tactile Zone (for tickle interactions) */}
1919
+ <path
1920
+ data-zone="belly"
1921
+ d="M 38 72 L 92 72 C 86 112, 44 112, 38 72 Z"
1922
+ fill="transparent"
1923
+ className="cursor-pointer pointer-events-auto"
1924
+ />
1925
+
1926
+ {/* Cyber Armor Plates */}
1927
+ <path
1928
+ d="M 30 45 L 56 45 L 52 80 L 28 75 Z"
1929
+ fill="#19362d"
1930
+ stroke="#254a3e"
1931
+ strokeWidth="1.5"
1932
+ />
1933
+ <path
1934
+ d="M 100 45 L 74 45 L 78 80 L 102 75 Z"
1935
+ fill="#19362d"
1936
+ stroke="#254a3e"
1937
+ strokeWidth="1.5"
1938
+ />
1939
+
1940
+ {/* Central Arc Reactor Heartbeat */}
1941
+ <g data-zone="core" className="cursor-pointer pointer-events-auto">
1942
+ <circle cx="65" cy="70" r="14" fill="#09120e" stroke="#19362d" strokeWidth="2.5" />
1943
+ <g className="avatar-core-beat origin-[65px_70px]">
1944
+ <circle
1945
+ cx="65"
1946
+ cy="70"
1947
+ r="9"
1948
+ fill="#ffffff"
1949
+ filter="url(#avatar-neon-glow)"
1950
+ opacity="0.88"
1951
+ />
1952
+ <circle cx="65" cy="70" r="4.5" fill="#ffffff" opacity="0.98" />
1953
+ </g>
1954
+ </g>
1955
+
1956
+ {/* Power Telemetry Bars */}
1957
+ <rect x="50" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
1958
+ <rect x="59" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
1959
+ <rect x="68" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
1960
+ <rect x="77" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
1961
+
1962
+ {/* Bottom Thruster Vent Nozzle */}
1963
+ <path d="M 50 117 L 80 117 L 74 125 L 56 125 Z" fill="#0d1712" />
1964
+ </svg>
1965
+ </div>
1966
+
1967
+ {/* ── 3. FLOATING HANDS (5-Finger Cybernetic Claws) ── */}
1968
+ <div
1969
+ ref={handLeftRef}
1970
+ data-zone="hand-left"
1971
+ className="absolute left-[36px] top-[215px] w-[44px] h-[52px] avatar-preserve-3d cursor-pointer pointer-events-auto"
1972
+ style={{ transform: 'translateZ(18px)' }}
1973
+ >
1974
+ <svg
1975
+ viewBox="0 0 50 60"
1976
+ className="w-full h-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.65)]"
1977
+ >
1978
+ {/* Thumb (Outer Left) */}
1979
+ <g transform="rotate(28, 11, 20)">
1980
+ <rect
1981
+ x="5.5"
1982
+ y="15"
1983
+ width="6.2"
1984
+ height="19"
1985
+ rx="3.1"
1986
+ fill="#14241c"
1987
+ stroke="#19362d"
1988
+ strokeWidth="1.5"
1989
+ />
1990
+ <line x1="6" y1="24.5" x2="11.2" y2="24.5" stroke="#254a3e" strokeWidth="1" />
1991
+ <circle cx="8.6" cy="30.5" r="2.1" fill="#ffffff" opacity="0.95" />
1992
+ </g>
1993
+
1994
+ {/* 4 Lower Fingers */}
1995
+ {/* Index */}
1996
+ <rect
1997
+ x="12.5"
1998
+ y="18"
1999
+ width="5.8"
2000
+ height="26"
2001
+ rx="2.9"
2002
+ fill="#14241c"
2003
+ stroke="#19362d"
2004
+ strokeWidth="1.5"
2005
+ />
2006
+ <line x1="13" y1="31" x2="17.8" y2="31" stroke="#254a3e" strokeWidth="1" />
2007
+ <circle cx="15.4" cy="40.5" r="2" fill="#ffffff" opacity="0.95" />
2008
+
2009
+ {/* Middle (Longest) */}
2010
+ <rect
2011
+ x="19.7"
2012
+ y="18"
2013
+ width="6.2"
2014
+ height="30"
2015
+ rx="3.1"
2016
+ fill="#14241c"
2017
+ stroke="#19362d"
2018
+ strokeWidth="1.5"
2019
+ />
2020
+ <line x1="20.2" y1="33" x2="25.4" y2="33" stroke="#254a3e" strokeWidth="1" />
2021
+ <circle cx="22.8" cy="44.5" r="2.2" fill="#ffffff" opacity="0.95" />
2022
+
2023
+ {/* Ring */}
2024
+ <rect
2025
+ x="27.3"
2026
+ y="18"
2027
+ width="5.8"
2028
+ height="27"
2029
+ rx="2.9"
2030
+ fill="#14241c"
2031
+ stroke="#19362d"
2032
+ strokeWidth="1.5"
2033
+ />
2034
+ <line x1="27.8" y1="31" x2="32.6" y2="31" stroke="#254a3e" strokeWidth="1" />
2035
+ <circle cx="30.2" cy="41.5" r="2" fill="#ffffff" opacity="0.95" />
2036
+
2037
+ {/* Pinky (Shortest) */}
2038
+ <rect
2039
+ x="34.5"
2040
+ y="18"
2041
+ width="5.2"
2042
+ height="22"
2043
+ rx="2.6"
2044
+ fill="#14241c"
2045
+ stroke="#19362d"
2046
+ strokeWidth="1.5"
2047
+ />
2048
+ <line x1="35" y1="29" x2="39.2" y2="29" stroke="#254a3e" strokeWidth="1" />
2049
+ <circle cx="37.1" cy="36.5" r="1.8" fill="#ffffff" opacity="0.95" />
2050
+
2051
+ {/* Palm Base Chassis (Drawn over finger roots) */}
2052
+ <rect
2053
+ x="11"
2054
+ y="7"
2055
+ width="30"
2056
+ height="22"
2057
+ rx="11"
2058
+ fill="#13221b"
2059
+ stroke="#19362d"
2060
+ strokeWidth="2.2"
2061
+ />
2062
+ <ellipse
2063
+ cx="26"
2064
+ cy="17"
2065
+ rx="7"
2066
+ ry="5.5"
2067
+ fill="#0c1611"
2068
+ stroke="#19362d"
2069
+ strokeWidth="1"
2070
+ />
2071
+ <circle cx="26" cy="17" r="2" fill="#ffffff" opacity="0.35" />
2072
+ </svg>
2073
+ </div>
2074
+
2075
+ <div
2076
+ ref={handRightRef}
2077
+ data-zone="hand-right"
2078
+ className="absolute left-[240px] top-[215px] w-[44px] h-[52px] avatar-preserve-3d cursor-pointer pointer-events-auto"
2079
+ style={{ transform: 'translateZ(18px)' }}
2080
+ >
2081
+ <svg
2082
+ viewBox="0 0 50 60"
2083
+ className="w-full h-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.65)]"
2084
+ >
2085
+ {/* Thumb (Outer Right) */}
2086
+ <g transform="rotate(-28, 39, 20)">
2087
+ <rect
2088
+ x="38.3"
2089
+ y="15"
2090
+ width="6.2"
2091
+ height="19"
2092
+ rx="3.1"
2093
+ fill="#14241c"
2094
+ stroke="#19362d"
2095
+ strokeWidth="1.5"
2096
+ />
2097
+ <line x1="38.8" y1="24.5" x2="44" y2="24.5" stroke="#254a3e" strokeWidth="1" />
2098
+ <circle cx="41.4" cy="30.5" r="2.1" fill="#ffffff" opacity="0.95" />
2099
+ </g>
2100
+
2101
+ {/* Pinky (Left side) */}
2102
+ <rect
2103
+ x="10.3"
2104
+ y="18"
2105
+ width="5.2"
2106
+ height="22"
2107
+ rx="2.6"
2108
+ fill="#14241c"
2109
+ stroke="#19362d"
2110
+ strokeWidth="1.5"
2111
+ />
2112
+ <line x1="10.8" y1="29" x2="15" y2="29" stroke="#254a3e" strokeWidth="1" />
2113
+ <circle cx="12.9" cy="36.5" r="1.8" fill="#ffffff" opacity="0.95" />
2114
+
2115
+ {/* Ring */}
2116
+ <rect
2117
+ x="16.9"
2118
+ y="18"
2119
+ width="5.8"
2120
+ height="27"
2121
+ rx="2.9"
2122
+ fill="#14241c"
2123
+ stroke="#19362d"
2124
+ strokeWidth="1.5"
2125
+ />
2126
+ <line x1="17.4" y1="31" x2="22.2" y2="31" stroke="#254a3e" strokeWidth="1" />
2127
+ <circle cx="19.8" cy="41.5" r="2" fill="#ffffff" opacity="0.95" />
2128
+
2129
+ {/* Middle */}
2130
+ <rect
2131
+ x="24.1"
2132
+ y="18"
2133
+ width="6.2"
2134
+ height="30"
2135
+ rx="3.1"
2136
+ fill="#14241c"
2137
+ stroke="#19362d"
2138
+ strokeWidth="1.5"
2139
+ />
2140
+ <line x1="24.6" y1="33" x2="29.8" y2="33" stroke="#254a3e" strokeWidth="1" />
2141
+ <circle cx="27.2" cy="44.5" r="2.2" fill="#ffffff" opacity="0.95" />
2142
+
2143
+ {/* Index */}
2144
+ <rect
2145
+ x="31.7"
2146
+ y="18"
2147
+ width="5.8"
2148
+ height="26"
2149
+ rx="2.9"
2150
+ fill="#14241c"
2151
+ stroke="#19362d"
2152
+ strokeWidth="1.5"
2153
+ />
2154
+ <line x1="32.2" y1="31" x2="37" y2="31" stroke="#254a3e" strokeWidth="1" />
2155
+ <circle cx="34.6" cy="40.5" r="2" fill="#ffffff" opacity="0.95" />
2156
+
2157
+ {/* Palm Base Chassis */}
2158
+ <rect
2159
+ x="9"
2160
+ y="7"
2161
+ width="30"
2162
+ height="22"
2163
+ rx="11"
2164
+ fill="#13221b"
2165
+ stroke="#19362d"
2166
+ strokeWidth="2.2"
2167
+ />
2168
+ <ellipse
2169
+ cx="24"
2170
+ cy="17"
2171
+ rx="7"
2172
+ ry="5.5"
2173
+ fill="#0c1611"
2174
+ stroke="#19362d"
2175
+ strokeWidth="1"
2176
+ />
2177
+ <circle cx="24" cy="17" r="2" fill="#ffffff" opacity="0.35" />
2178
+ </svg>
2179
+ </div>
2180
+
2181
+ {/* ── 4. THRUSTER PARTICLES & SHADOW AT BOTTOM ── */}
2182
+ <div
2183
+ ref={thrusterRef}
2184
+ className="absolute left-[118px] top-[305px] w-[84px] h-[22px] rounded-full bg-white/25 blur-[10px] pointer-events-none"
2185
+ style={{ transform: 'translateZ(-35px)' }}
2186
+ />
2187
+ <div
2188
+ ref={groundShadowRef}
2189
+ className="absolute left-[90px] top-[360px] w-[140px] h-[26px] rounded-full bg-black/75 blur-[8px] pointer-events-none"
2190
+ style={{ transform: 'translateZ(-60px) rotateX(75deg)' }}
2191
+ />
2192
+ </div>
2193
+ </div>
2194
+ )
2195
+ }
2196
+
2197
+ export default memo(Avatar)