@kibee/renderer-three 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.mjs ADDED
@@ -0,0 +1,1875 @@
1
+ // src/ThreeBeeRenderer.ts
2
+ import * as THREE2 from "three";
3
+ import { gsap } from "gsap";
4
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
5
+
6
+ // src/gltf/registerPbrSpecularGlossinessGLTFPlugin.ts
7
+ import { Color, LinearSRGBColorSpace, SRGBColorSpace } from "three";
8
+ var EXT = "KHR_materials_pbrSpecularGlossiness";
9
+ function registerPbrSpecularGlossinessGLTFPlugin(loader) {
10
+ loader.register((parser) => ({
11
+ name: EXT,
12
+ extendMaterialParams(materialIndex, materialParams) {
13
+ const materialDef = parser.json.materials?.[materialIndex];
14
+ const ext = materialDef?.extensions?.[EXT];
15
+ if (!ext) return Promise.resolve();
16
+ const pending = [];
17
+ if (Array.isArray(ext.diffuseFactor) && ext.diffuseFactor.length >= 3) {
18
+ const c = new Color().setRGB(
19
+ ext.diffuseFactor[0],
20
+ ext.diffuseFactor[1],
21
+ ext.diffuseFactor[2],
22
+ LinearSRGBColorSpace
23
+ );
24
+ materialParams.color = c;
25
+ if (ext.diffuseFactor[3] !== void 0) {
26
+ materialParams.opacity = ext.diffuseFactor[3];
27
+ materialParams.transparent = ext.diffuseFactor[3] < 1;
28
+ }
29
+ }
30
+ if (ext.diffuseTexture !== void 0) {
31
+ pending.push(
32
+ parser.assignTexture(
33
+ materialParams,
34
+ "map",
35
+ ext.diffuseTexture,
36
+ SRGBColorSpace
37
+ )
38
+ );
39
+ }
40
+ const gloss = ext.glossinessFactor ?? 1;
41
+ materialParams.roughness = Math.min(1, Math.max(0.04, 1 - gloss));
42
+ materialParams.metalness = 0;
43
+ return Promise.all(pending);
44
+ }
45
+ }));
46
+ }
47
+
48
+ // src/legacyBeeMaterial.ts
49
+ import * as THREE from "three";
50
+ function toMeshArray(node) {
51
+ const meshes = [];
52
+ node.traverse((child) => {
53
+ if (child instanceof THREE.Mesh) {
54
+ meshes.push(child);
55
+ }
56
+ });
57
+ return meshes;
58
+ }
59
+ async function applyLegacyBeeMaterialCompat(gltf) {
60
+ const parser = gltf.parser;
61
+ const materialDef = parser?.json?.materials?.[0];
62
+ const legacyDef = materialDef?.extensions?.KHR_materials_pbrSpecularGlossiness;
63
+ if (!parser?.loadTexture || !legacyDef?.diffuseTexture) {
64
+ return;
65
+ }
66
+ const diffuseMap = await parser.loadTexture(legacyDef.diffuseTexture.index);
67
+ const normalMap = materialDef?.normalTexture ? await parser.loadTexture(materialDef.normalTexture.index) : null;
68
+ const aoMap = materialDef?.occlusionTexture ? await parser.loadTexture(materialDef.occlusionTexture.index) : null;
69
+ diffuseMap.colorSpace = THREE.SRGBColorSpace;
70
+ const meshes = toMeshArray(gltf.scene);
71
+ for (const mesh of meshes) {
72
+ const previousMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
73
+ const replacement = new THREE.MeshStandardMaterial({
74
+ map: diffuseMap,
75
+ normalMap,
76
+ aoMap,
77
+ roughness: 0.76,
78
+ metalness: 0.02,
79
+ transparent: true,
80
+ alphaTest: 0.08,
81
+ side: materialDef?.doubleSided ? THREE.DoubleSide : THREE.FrontSide
82
+ });
83
+ if (mesh.geometry.getAttribute("uv") && !mesh.geometry.getAttribute("uv2")) {
84
+ mesh.geometry.setAttribute("uv2", mesh.geometry.getAttribute("uv"));
85
+ }
86
+ mesh.material = replacement;
87
+ if (previousMaterial && "dispose" in previousMaterial) {
88
+ previousMaterial.dispose();
89
+ }
90
+ }
91
+ }
92
+
93
+ // src/ThreeBeeRenderer.ts
94
+ var CLIP_BY_STATE = {
95
+ idle: "idle",
96
+ guiding: "hover",
97
+ thinking: "hover",
98
+ celebrating: "take_off_and_land"
99
+ };
100
+ var ThreeBeeRenderer = class {
101
+ constructor(options = {}) {
102
+ this.options = options;
103
+ this.dockClickBehavior = options.dockClickBehavior ?? "undock";
104
+ this.restorePersistedDockPosition = options.restorePersistedDockPosition !== false;
105
+ if (this.restorePersistedDockPosition) {
106
+ try {
107
+ const saved = localStorage.getItem("kibee:dock_pos");
108
+ if (saved) {
109
+ const pos = JSON.parse(saved);
110
+ if (typeof pos.x === "number" && typeof pos.y === "number") {
111
+ this.customDockPos = pos;
112
+ }
113
+ }
114
+ } catch {
115
+ }
116
+ }
117
+ }
118
+ options;
119
+ root;
120
+ canvasWrap;
121
+ tooltip;
122
+ hoverZone;
123
+ dockButton;
124
+ scene;
125
+ camera;
126
+ renderer;
127
+ beeGroup = new THREE2.Group();
128
+ visualRig = new THREE2.Group();
129
+ mixer = null;
130
+ actions = /* @__PURE__ */ new Map();
131
+ activeActionName = null;
132
+ runtimeWings = null;
133
+ mounted = false;
134
+ /** Deduplicates concurrent mount() calls (React Strict Mode, double init, etc.) */
135
+ mountPromise = null;
136
+ disposed = false;
137
+ animationFrameId = null;
138
+ clock = new THREE2.Clock();
139
+ state = "idle";
140
+ currentX = 120;
141
+ currentY = 120;
142
+ currentFacing = "right";
143
+ docked = true;
144
+ dockVisible = true;
145
+ hoveringHome = false;
146
+ reducedMotion = false;
147
+ renderingAvailable = false;
148
+ idleTween = null;
149
+ celebrateTween = null;
150
+ tooltipPulseTimer = null;
151
+ tooltipMessage = "";
152
+ actionHandler = null;
153
+ dockClickBehavior;
154
+ autoDockTimer = null;
155
+ customDockPos = null;
156
+ // Radial action buttons (individually positioned, not wrapped)
157
+ radialButtons = [];
158
+ // Drag state
159
+ isDragging = false;
160
+ dragStartX = 0;
161
+ dragStartY = 0;
162
+ dragMoved = false;
163
+ /** Pointer went down while docked — used to distinguish dock tap vs drag-out */
164
+ dragArmedFromDock = false;
165
+ // Debounce timer — prevents hiding buttons while cursor travels to them
166
+ hideButtonsTimer = null;
167
+ // Whether the bee is currently parked at the chat panel header
168
+ panelOpen = false;
169
+ /** When false, constructor skips LS restore and setCustomDockPosition skips LS writes by default. */
170
+ restorePersistedDockPosition;
171
+ // Tooltip nav elements (created in initDOM)
172
+ tooltipText;
173
+ tooltipNav;
174
+ navPrevBtn;
175
+ navLabel;
176
+ navNextBtn;
177
+ tooltipHasNav = false;
178
+ // Inline prompt (used when capturing a 1-2 line note for honey/sting)
179
+ tooltipPrompt;
180
+ tooltipPromptInput;
181
+ tooltipPromptSaveBtn;
182
+ tooltipPromptSkipBtn;
183
+ tooltipPromptHint;
184
+ tooltipPromptActive = false;
185
+ tooltipPromptMaxLen = 240;
186
+ async mount(container = document.body) {
187
+ if (this.mounted) return;
188
+ if (this.mountPromise) {
189
+ await this.mountPromise;
190
+ return;
191
+ }
192
+ this.mountPromise = this.performMount(container);
193
+ try {
194
+ await this.mountPromise;
195
+ } finally {
196
+ this.mountPromise = null;
197
+ }
198
+ }
199
+ async performMount(container) {
200
+ this.reducedMotion = this.shouldReduceMotion();
201
+ this.disposed = false;
202
+ this.createRoot(container);
203
+ this.createScene();
204
+ this.createCamera();
205
+ this.createRenderer();
206
+ this.scene.add(this.beeGroup);
207
+ this.beeGroup.add(this.visualRig);
208
+ if (this.renderingAvailable) {
209
+ this.createLights();
210
+ const loaded = await this.loadBeeModel();
211
+ if (!loaded) {
212
+ this.createFallbackBee();
213
+ }
214
+ }
215
+ this.resize();
216
+ this.initializeDockedHome();
217
+ if (this.renderingAvailable) {
218
+ this.startRenderLoop();
219
+ }
220
+ this.mounted = true;
221
+ document.body.dataset.kibeeRendererActive = "true";
222
+ this.state = "guiding";
223
+ this.setState("idle");
224
+ window.addEventListener("resize", this.handleResize, { passive: true });
225
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
226
+ window.addEventListener("pointermove", this.handlePointerMove, {
227
+ passive: true
228
+ });
229
+ window.addEventListener("pointerup", this.handlePointerUp);
230
+ window.addEventListener("kibee-panel-open", this.handlePanelOpen);
231
+ window.addEventListener("kibee-panel-close", this.handlePanelClose);
232
+ }
233
+ async moveTo(x, y, options = {}) {
234
+ const duration = options.duration ?? 0.9;
235
+ const offsetX = options.offsetX ?? 16;
236
+ const offsetY = options.offsetY ?? -18;
237
+ const targetX = x + offsetX;
238
+ const targetY = y + offsetY;
239
+ await this.undock();
240
+ this.hoveringHome = false;
241
+ this.applyFacing(targetX >= this.currentX ? "right" : "left");
242
+ this.setState("guiding");
243
+ return new Promise((resolve) => {
244
+ gsap.to(this.beeGroup.scale, {
245
+ x: this.getActiveScale(),
246
+ y: this.getActiveScale(),
247
+ z: this.getActiveScale(),
248
+ duration: 0.24,
249
+ ease: "power2.out",
250
+ overwrite: "auto"
251
+ });
252
+ gsap.to(this, {
253
+ currentX: targetX,
254
+ currentY: targetY,
255
+ duration,
256
+ ease: "power2.out",
257
+ overwrite: "auto",
258
+ onUpdate: () => {
259
+ this.updateBeeScreenPosition();
260
+ this.positionTooltipNearBee();
261
+ },
262
+ onComplete: () => {
263
+ this.setState("idle");
264
+ resolve();
265
+ }
266
+ });
267
+ });
268
+ }
269
+ async dock() {
270
+ if (!this.isDockEnabled()) return;
271
+ if (this.docked) return;
272
+ this.clearAutoDockTimer();
273
+ this.hideMessage();
274
+ this.clearHideButtonsTimer();
275
+ this.hideRadialButtons();
276
+ const dock = this.getDockCenter();
277
+ this.dockVisible = true;
278
+ this.hoveringHome = false;
279
+ this.stopStateTweens();
280
+ this.applyHomeFacing();
281
+ this.syncDockState();
282
+ if (this.reducedMotion) {
283
+ this.currentX = dock.x;
284
+ this.currentY = dock.y;
285
+ this.docked = true;
286
+ this.beeGroup.scale.setScalar(this.getDockedScale());
287
+ this.updateBeeScreenPosition();
288
+ this.syncDockState();
289
+ if (this.state === "idle") {
290
+ this.startIdleMotion();
291
+ }
292
+ return;
293
+ }
294
+ return new Promise((resolve) => {
295
+ gsap.timeline({
296
+ onComplete: () => {
297
+ this.currentX = dock.x;
298
+ this.currentY = dock.y;
299
+ this.docked = true;
300
+ this.syncDockState();
301
+ if (this.state === "idle") {
302
+ this.startIdleMotion();
303
+ }
304
+ resolve();
305
+ }
306
+ }).to(
307
+ this,
308
+ {
309
+ currentX: dock.x,
310
+ currentY: dock.y,
311
+ duration: 0.58,
312
+ ease: "power2.inOut",
313
+ overwrite: "auto",
314
+ onUpdate: () => {
315
+ this.updateBeeScreenPosition();
316
+ this.positionTooltipNearBee();
317
+ }
318
+ },
319
+ 0
320
+ ).to(
321
+ this.beeGroup.scale,
322
+ {
323
+ x: this.getDockedScale(),
324
+ y: this.getDockedScale(),
325
+ z: this.getDockedScale(),
326
+ duration: 0.58,
327
+ ease: "power2.inOut",
328
+ overwrite: "auto"
329
+ },
330
+ 0
331
+ ).to(
332
+ this.beeGroup.rotation,
333
+ {
334
+ z: 0.02,
335
+ duration: 0.2,
336
+ ease: "sine.out",
337
+ overwrite: "auto"
338
+ },
339
+ 0
340
+ );
341
+ });
342
+ }
343
+ resetAutoDockTimer() {
344
+ if (this.autoDockTimer) clearTimeout(this.autoDockTimer);
345
+ this.autoDockTimer = setTimeout(() => {
346
+ if (!this.docked && this.state === "idle") {
347
+ void this.dock();
348
+ }
349
+ }, 1e4);
350
+ }
351
+ clearAutoDockTimer() {
352
+ if (this.autoDockTimer) {
353
+ clearTimeout(this.autoDockTimer);
354
+ this.autoDockTimer = null;
355
+ }
356
+ }
357
+ async undock(options = {}) {
358
+ if (!this.isDockEnabled()) return;
359
+ this.resetAutoDockTimer();
360
+ const hideDock = options.hideDock ?? true;
361
+ this.docked = false;
362
+ this.dockVisible = !hideDock;
363
+ this.hoveringHome = !hideDock;
364
+ this.syncDockState();
365
+ if (hideDock) {
366
+ if (this.reducedMotion) {
367
+ this.beeGroup.scale.setScalar(this.getActiveScale());
368
+ return;
369
+ }
370
+ return new Promise((resolve) => {
371
+ gsap.to(this.beeGroup.scale, {
372
+ x: this.getActiveScale(),
373
+ y: this.getActiveScale(),
374
+ z: this.getActiveScale(),
375
+ duration: 0.24,
376
+ ease: "power2.out",
377
+ overwrite: "auto",
378
+ onComplete: resolve
379
+ });
380
+ });
381
+ }
382
+ const wake = this.getWakeHoverPosition();
383
+ this.stopStateTweens();
384
+ this.applyHomeFacing();
385
+ if (this.reducedMotion) {
386
+ this.currentX = wake.x;
387
+ this.currentY = wake.y;
388
+ this.updateBeeScreenPosition();
389
+ this.beeGroup.scale.setScalar(this.getHomeHoverScale());
390
+ if (this.state === "idle") {
391
+ this.startIdleMotion();
392
+ }
393
+ return;
394
+ }
395
+ return new Promise((resolve) => {
396
+ gsap.timeline({
397
+ onComplete: () => {
398
+ this.currentX = wake.x;
399
+ this.currentY = wake.y;
400
+ if (this.state === "idle") {
401
+ this.startIdleMotion();
402
+ }
403
+ resolve();
404
+ }
405
+ }).to(
406
+ this,
407
+ {
408
+ currentX: wake.x,
409
+ currentY: wake.y,
410
+ duration: 0.36,
411
+ ease: "power2.out",
412
+ overwrite: "auto",
413
+ onUpdate: () => {
414
+ this.updateBeeScreenPosition();
415
+ this.positionTooltipNearBee();
416
+ }
417
+ },
418
+ 0
419
+ ).to(
420
+ this.beeGroup.scale,
421
+ {
422
+ x: this.getHomeHoverScale(),
423
+ y: this.getHomeHoverScale(),
424
+ z: this.getHomeHoverScale(),
425
+ duration: 0.36,
426
+ ease: "power2.out",
427
+ overwrite: "auto"
428
+ },
429
+ 0
430
+ );
431
+ });
432
+ }
433
+ isDocked() {
434
+ return this.docked;
435
+ }
436
+ setState(state) {
437
+ if (!this.mounted) return;
438
+ if (this.state === state) return;
439
+ this.state = state;
440
+ this.syncDockState();
441
+ this.stopStateTweens();
442
+ if (state !== "idle") {
443
+ this.clearHideButtonsTimer();
444
+ this.hideRadialButtons();
445
+ if (this.panelOpen) {
446
+ window.dispatchEvent(new CustomEvent("kibee-panel-close"));
447
+ this.panelOpen = false;
448
+ }
449
+ }
450
+ switch (state) {
451
+ case "idle":
452
+ this.startIdleMotion();
453
+ break;
454
+ case "guiding":
455
+ this.playClipForState("guiding");
456
+ gsap.to(this.beeGroup.rotation, {
457
+ z: this.reducedMotion ? 0.04 : 0.08,
458
+ duration: 0.22,
459
+ ease: "power1.out",
460
+ overwrite: "auto"
461
+ });
462
+ break;
463
+ case "thinking":
464
+ this.startThinkingMotion();
465
+ break;
466
+ case "celebrating":
467
+ this.startCelebrateMotion();
468
+ break;
469
+ }
470
+ }
471
+ speak(message, options = {}) {
472
+ if (!this.mounted) return;
473
+ if (this.docked) return;
474
+ if (this.panelOpen) {
475
+ this.tooltipMessage = message;
476
+ return;
477
+ }
478
+ this.clearTooltipPulse();
479
+ this.tooltipMessage = message;
480
+ this.tooltipText.textContent = message;
481
+ this.tooltip.style.display = "block";
482
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
483
+ this.tooltip.classList.add("kibee-tooltip--visible");
484
+ const hasNav = options.onPrev !== void 0 || options.onNext !== void 0;
485
+ this.tooltipHasNav = hasNav;
486
+ if (hasNav) {
487
+ this.navPrevBtn.onclick = options.onPrev ?? null;
488
+ this.navPrevBtn.style.visibility = options.onPrev ? "visible" : "hidden";
489
+ this.navNextBtn.onclick = options.onNext ?? null;
490
+ this.navNextBtn.style.visibility = options.onNext ? "visible" : "hidden";
491
+ this.navLabel.textContent = options.stepLabel ?? "";
492
+ this.tooltipNav.style.display = "flex";
493
+ this.tooltip.style.pointerEvents = "auto";
494
+ } else {
495
+ this.tooltipHasNav = false;
496
+ this.tooltipNav.style.display = "none";
497
+ this.tooltip.style.pointerEvents = "";
498
+ }
499
+ if (options.maxWidth) {
500
+ this.tooltip.style.maxWidth = `${options.maxWidth}px`;
501
+ }
502
+ if (typeof options.x === "number" && typeof options.y === "number") {
503
+ this.tooltip.style.left = `${options.x}px`;
504
+ this.tooltip.style.top = `${options.y}px`;
505
+ if (options.arrowEdge) {
506
+ this.tooltip.dataset.arrowEdge = options.arrowEdge;
507
+ } else {
508
+ this.inferTooltipArrowEdgeForPlacedTooltip(options.x);
509
+ }
510
+ } else {
511
+ this.positionTooltipNearBee();
512
+ }
513
+ this.startTooltipPulse();
514
+ }
515
+ hideMessage() {
516
+ if (!this.mounted) return;
517
+ this.clearTooltipPulse();
518
+ this.tooltipMessage = "";
519
+ this.tooltip.classList.remove("kibee-tooltip--visible");
520
+ this.tooltip.classList.add("kibee-tooltip--hidden");
521
+ this.tooltip.style.display = "none";
522
+ this.tooltip.style.pointerEvents = "";
523
+ this.tooltipHasNav = false;
524
+ this.tooltipNav.style.display = "none";
525
+ this.navPrevBtn.onclick = null;
526
+ this.navNextBtn.onclick = null;
527
+ delete this.tooltip.dataset.arrowEdge;
528
+ this.tooltipPrompt.style.display = "none";
529
+ this.tooltipPromptActive = false;
530
+ this.tooltipPromptSaveBtn.onclick = null;
531
+ this.tooltipPromptSkipBtn.onclick = null;
532
+ this.tooltipPromptInput.value = "";
533
+ this.tooltip.classList.remove("kibee-tooltip--prompt");
534
+ }
535
+ /**
536
+ * Show an inline 1-2 line prompt inside the bee's speech bubble. Used by
537
+ * the bookmark + issue chips to capture a short note before saving.
538
+ * Resolves to the user's note (trimmed) or null if they skipped / dismissed.
539
+ */
540
+ prompt(message, options = {}) {
541
+ if (!this.mounted) return Promise.resolve(null);
542
+ return new Promise((resolve) => {
543
+ this.speak(message, {});
544
+ this.clearTooltipPulse();
545
+ this.tooltipHasNav = true;
546
+ this.tooltip.style.pointerEvents = "auto";
547
+ this.tooltip.classList.add("kibee-tooltip--prompt");
548
+ this.tooltipPrompt.style.display = "flex";
549
+ this.tooltipPromptActive = true;
550
+ this.tooltipPromptInput.value = "";
551
+ if (options.placeholder) {
552
+ this.tooltipPromptInput.placeholder = options.placeholder;
553
+ }
554
+ this.updatePromptHint();
555
+ this.positionTooltipNearBee();
556
+ const finish = (note) => {
557
+ if (!this.tooltipPromptActive) return;
558
+ this.tooltipPromptActive = false;
559
+ this.tooltipPromptSaveBtn.onclick = null;
560
+ this.tooltipPromptSkipBtn.onclick = null;
561
+ this.tooltipPrompt.style.display = "none";
562
+ this.tooltipPromptInput.value = "";
563
+ this.tooltip.classList.remove("kibee-tooltip--prompt");
564
+ resolve(note);
565
+ };
566
+ this.tooltipPromptSaveBtn.onclick = () => {
567
+ const value = this.tooltipPromptInput.value.trim();
568
+ finish(value || "");
569
+ };
570
+ this.tooltipPromptSkipBtn.onclick = () => finish(null);
571
+ requestAnimationFrame(() => this.tooltipPromptInput.focus());
572
+ });
573
+ }
574
+ /** Swap the prompt's bottom-left hint between the keyboard hint (when empty)
575
+ * and a tabular char counter (once the user starts typing). */
576
+ updatePromptHint() {
577
+ const len = this.tooltipPromptInput.value.length;
578
+ if (len === 0) {
579
+ this.tooltipPromptHint.innerHTML = `<kbd>\u23CE</kbd> save \xB7 <kbd>esc</kbd> skip`;
580
+ return;
581
+ }
582
+ const max = this.tooltipPromptMaxLen;
583
+ this.tooltipPromptHint.textContent = `${len} / ${max}`;
584
+ }
585
+ inferTooltipArrowEdgeForPlacedTooltip(tooltipLeft) {
586
+ requestAnimationFrame(() => {
587
+ if (!this.tooltip || this.tooltip.style.display === "none") return;
588
+ const tw = this.tooltip.offsetWidth || 220;
589
+ const midX = tooltipLeft + tw / 2;
590
+ this.tooltip.dataset.arrowEdge = midX >= this.currentX ? "left" : "right";
591
+ });
592
+ }
593
+ showActions(handler) {
594
+ if (!this.mounted) return;
595
+ this.actionHandler = handler;
596
+ this.positionHoverZone();
597
+ }
598
+ hideActions() {
599
+ if (!this.mounted) return;
600
+ this.clearHideButtonsTimer();
601
+ this.actionHandler = null;
602
+ this.hideRadialButtons();
603
+ this.positionHoverZone();
604
+ }
605
+ resize() {
606
+ if (!this.root) return;
607
+ const width = window.innerWidth;
608
+ const height = window.innerHeight;
609
+ if (this.renderingAvailable && this.renderer && this.camera) {
610
+ this.renderer.setSize(width, height, false);
611
+ this.camera.left = 0;
612
+ this.camera.right = width;
613
+ this.camera.top = 0;
614
+ this.camera.bottom = height;
615
+ this.camera.near = -1e3;
616
+ this.camera.far = 1e3;
617
+ this.camera.position.set(0, 0, 10);
618
+ this.camera.lookAt(0, 0, 0);
619
+ this.camera.updateProjectionMatrix();
620
+ }
621
+ if (this.isDockEnabled()) {
622
+ if (this.docked) {
623
+ const dock = this.getDockCenter();
624
+ this.currentX = dock.x;
625
+ this.currentY = dock.y;
626
+ } else if (this.hoveringHome) {
627
+ const wake = this.getWakeHoverPosition();
628
+ this.currentX = wake.x;
629
+ this.currentY = wake.y;
630
+ }
631
+ this.syncDockState();
632
+ }
633
+ this.updateBeeScreenPosition();
634
+ this.positionTooltipNearBee();
635
+ }
636
+ setDockClickBehavior(mode) {
637
+ this.dockClickBehavior = mode;
638
+ this.updateDockAria();
639
+ this.syncDockState();
640
+ }
641
+ destroy() {
642
+ this.mountPromise = null;
643
+ window.removeEventListener("resize", this.handleResize);
644
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
645
+ window.removeEventListener("pointermove", this.handlePointerMove);
646
+ window.removeEventListener("pointerup", this.handlePointerUp);
647
+ window.removeEventListener("kibee-panel-open", this.handlePanelOpen);
648
+ window.removeEventListener("kibee-panel-close", this.handlePanelClose);
649
+ delete document.body.dataset.kibeeRendererActive;
650
+ this.clearHideButtonsTimer();
651
+ this.clearAutoDockTimer();
652
+ this.disposed = true;
653
+ if (this.animationFrameId !== null) {
654
+ cancelAnimationFrame(this.animationFrameId);
655
+ this.animationFrameId = null;
656
+ }
657
+ this.stopStateTweens();
658
+ gsap.killTweensOf(this);
659
+ this.dockButton?.removeEventListener("click", this.handleDockClick);
660
+ if (this.renderer) {
661
+ this.renderer.dispose();
662
+ }
663
+ this.disposeSceneObjects();
664
+ if (this.root?.parentNode) {
665
+ this.root.parentNode.removeChild(this.root);
666
+ }
667
+ this.mounted = false;
668
+ }
669
+ createRoot(container) {
670
+ document.querySelectorAll(".kibee-root").forEach((el) => {
671
+ el.remove();
672
+ });
673
+ this.root = document.createElement("div");
674
+ this.root.className = "kibee-root";
675
+ const pos = this.options.dockPosition ?? "bottom-right";
676
+ if (typeof pos === "object" && "inline" in pos) {
677
+ this.root.classList.add("kibee-root--inline");
678
+ }
679
+ this.root.style.setProperty("--kibee-dock-size", `${this.getDockSize()}px`);
680
+ const cssVars = this.resolveDockCSSVars();
681
+ this.root.style.setProperty("--kibee-dock-right", cssVars.right);
682
+ this.root.style.setProperty("--kibee-dock-bottom", cssVars.bottom);
683
+ this.root.style.setProperty("--kibee-dock-left", cssVars.left);
684
+ this.root.style.setProperty("--kibee-dock-top", cssVars.top);
685
+ this.canvasWrap = document.createElement("div");
686
+ this.canvasWrap.className = "kibee-canvas-wrap";
687
+ this.tooltip = document.createElement("div");
688
+ this.tooltip.className = "kibee-tooltip";
689
+ this.tooltip.style.display = "none";
690
+ const closeBtn = document.createElement("button");
691
+ closeBtn.type = "button";
692
+ closeBtn.className = "kibee-tooltip-close";
693
+ closeBtn.setAttribute("aria-label", "Dismiss");
694
+ closeBtn.innerHTML = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M9 1 1 9M1 1l8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`;
695
+ closeBtn.addEventListener("click", () => {
696
+ this.hideMessage();
697
+ void this.dock();
698
+ });
699
+ this.tooltip.appendChild(closeBtn);
700
+ this.tooltipText = document.createElement("div");
701
+ this.tooltipText.className = "kibee-tooltip-body";
702
+ this.tooltip.appendChild(this.tooltipText);
703
+ this.tooltipNav = document.createElement("div");
704
+ this.tooltipNav.className = "kibee-tooltip-nav";
705
+ this.tooltipNav.style.display = "none";
706
+ this.navPrevBtn = document.createElement("button");
707
+ this.navPrevBtn.type = "button";
708
+ this.navPrevBtn.className = "kibee-nav-btn";
709
+ this.navPrevBtn.setAttribute("aria-label", "Previous step");
710
+ this.navPrevBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M8 2 4 6l4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
711
+ this.navLabel = document.createElement("span");
712
+ this.navLabel.className = "kibee-nav-label";
713
+ this.navNextBtn = document.createElement("button");
714
+ this.navNextBtn.type = "button";
715
+ this.navNextBtn.className = "kibee-nav-btn";
716
+ this.navNextBtn.setAttribute("aria-label", "Next step");
717
+ this.navNextBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
718
+ this.tooltipNav.appendChild(this.navPrevBtn);
719
+ this.tooltipNav.appendChild(this.navLabel);
720
+ this.tooltipNav.appendChild(this.navNextBtn);
721
+ this.tooltip.appendChild(this.tooltipNav);
722
+ this.tooltipPrompt = document.createElement("div");
723
+ this.tooltipPrompt.className = "kibee-tooltip-prompt";
724
+ this.tooltipPrompt.style.display = "none";
725
+ this.tooltipPromptInput = document.createElement("textarea");
726
+ this.tooltipPromptInput.className = "kibee-tooltip-prompt-input";
727
+ this.tooltipPromptInput.rows = 2;
728
+ this.tooltipPromptInput.maxLength = this.tooltipPromptMaxLen;
729
+ this.tooltipPromptInput.placeholder = "One line or two\u2026";
730
+ this.tooltipPromptInput.spellcheck = true;
731
+ this.tooltipPromptInput.addEventListener("pointerdown", (e) => e.stopPropagation());
732
+ this.tooltipPromptInput.addEventListener("input", () => this.updatePromptHint());
733
+ this.tooltipPromptInput.addEventListener("keydown", (e) => {
734
+ e.stopPropagation();
735
+ if (e.key === "Enter" && !e.shiftKey) {
736
+ e.preventDefault();
737
+ this.tooltipPromptSaveBtn.click();
738
+ } else if (e.key === "Escape") {
739
+ e.preventDefault();
740
+ this.tooltipPromptSkipBtn.click();
741
+ }
742
+ });
743
+ this.tooltipPrompt.appendChild(this.tooltipPromptInput);
744
+ const promptRow = document.createElement("div");
745
+ promptRow.className = "kibee-tooltip-prompt-row";
746
+ this.tooltipPromptHint = document.createElement("span");
747
+ this.tooltipPromptHint.className = "kibee-tooltip-prompt-hint";
748
+ this.tooltipPromptHint.innerHTML = `<kbd>\u23CE</kbd> save \xB7 <kbd>esc</kbd> skip`;
749
+ promptRow.appendChild(this.tooltipPromptHint);
750
+ const promptActions = document.createElement("div");
751
+ promptActions.className = "kibee-tooltip-prompt-actions";
752
+ this.tooltipPromptSkipBtn = document.createElement("button");
753
+ this.tooltipPromptSkipBtn.type = "button";
754
+ this.tooltipPromptSkipBtn.className = "kibee-tooltip-prompt-skip";
755
+ this.tooltipPromptSkipBtn.textContent = "Skip";
756
+ this.tooltipPromptSkipBtn.addEventListener("click", (e) => e.stopPropagation());
757
+ this.tooltipPromptSkipBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
758
+ this.tooltipPromptSaveBtn = document.createElement("button");
759
+ this.tooltipPromptSaveBtn.type = "button";
760
+ this.tooltipPromptSaveBtn.className = "kibee-tooltip-prompt-save";
761
+ this.tooltipPromptSaveBtn.textContent = "Save";
762
+ this.tooltipPromptSaveBtn.addEventListener("click", (e) => e.stopPropagation());
763
+ this.tooltipPromptSaveBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
764
+ promptActions.appendChild(this.tooltipPromptSkipBtn);
765
+ promptActions.appendChild(this.tooltipPromptSaveBtn);
766
+ promptRow.appendChild(promptActions);
767
+ this.tooltipPrompt.appendChild(promptRow);
768
+ this.tooltip.appendChild(this.tooltipPrompt);
769
+ const radialDefs = [
770
+ {
771
+ cls: "kibee-action--honey",
772
+ label: "Save bookmark",
773
+ name: "Bookmark",
774
+ icon: `<svg width="17" height="17" viewBox="0 0 13 13" fill="none" aria-hidden="true">
775
+ <path d="M2.5 2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v9.5l-4-2.5-4 2.5V2Z"
776
+ stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
777
+ </svg>`,
778
+ action: () => this.actionHandler?.("honey")
779
+ },
780
+ {
781
+ cls: "kibee-action--sting",
782
+ label: "Flag issue",
783
+ name: "Issue",
784
+ icon: `<svg width="17" height="17" viewBox="0 0 13 13" fill="none" aria-hidden="true">
785
+ <circle cx="6.5" cy="6.5" r="5.25" stroke="currentColor" stroke-width="1.4"/>
786
+ <path d="M6.5 4v3M6.5 9h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
787
+ </svg>`,
788
+ action: () => this.actionHandler?.("sting")
789
+ },
790
+ {
791
+ cls: "kibee-action--chat",
792
+ label: "Open assistant",
793
+ name: "Chat",
794
+ icon: `<svg width="17" height="17" viewBox="0 0 17 17" fill="none" aria-hidden="true">
795
+ <path d="M3 2.5 H14 C14.6 2.5 15 2.9 15 3.5 V10.5 C15 11.1 14.6 11.5 14 11.5
796
+ H6.5 L3 14.5 V11.5 H3 C2.4 11.5 2 11.1 2 10.5 V3.5 C2 2.9 2.4 2.5 3 2.5 Z"
797
+ stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
798
+ </svg>`,
799
+ action: () => {
800
+ if (this.state !== "idle") return;
801
+ window.dispatchEvent(
802
+ new CustomEvent("kibee-assist-toggle", {
803
+ detail: { x: this.currentX, y: this.currentY }
804
+ })
805
+ );
806
+ }
807
+ },
808
+ {
809
+ cls: "kibee-action--dock",
810
+ label: "Set dock point here",
811
+ name: "Dock here",
812
+ icon: `<svg width="17" height="17" viewBox="0 0 17 17" fill="none" aria-hidden="true">
813
+ <circle cx="8.5" cy="8.5" r="3" stroke="currentColor" stroke-width="1.4"/>
814
+ <path d="M8.5 2v3M8.5 12v3M2 8.5h3M12 8.5h3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
815
+ </svg>`,
816
+ action: () => {
817
+ this.setCustomDockPosition(this.currentX, this.currentY, { persist: true });
818
+ void this.dock();
819
+ }
820
+ }
821
+ ];
822
+ this.radialButtons = radialDefs.map((def, i) => {
823
+ const btn = document.createElement("button");
824
+ btn.type = "button";
825
+ btn.className = `kibee-action ${def.cls}`;
826
+ btn.setAttribute("aria-label", def.label);
827
+ btn.dataset.label = def.name;
828
+ btn.innerHTML = def.icon;
829
+ btn.style.setProperty("--i", String(i));
830
+ btn.style.display = "none";
831
+ btn.addEventListener("pointerdown", (e) => e.stopPropagation());
832
+ btn.addEventListener("mousedown", (e) => e.stopPropagation());
833
+ btn.addEventListener("click", (e) => {
834
+ e.stopPropagation();
835
+ e.preventDefault();
836
+ def.action();
837
+ });
838
+ btn.addEventListener("mouseenter", () => this.clearHideButtonsTimer());
839
+ btn.addEventListener("mouseleave", () => this.scheduleHideButtons());
840
+ return btn;
841
+ });
842
+ this.hoverZone = document.createElement("div");
843
+ this.hoverZone.className = "kibee-hover-zone";
844
+ this.hoverZone.addEventListener("mouseenter", this.handleBeeHoverEnter);
845
+ this.hoverZone.addEventListener("mouseleave", this.handleBeeHoverLeave);
846
+ this.hoverZone.addEventListener("pointerdown", this.handlePointerDown);
847
+ this.tooltip.addEventListener("mouseenter", this.handleBeeHoverEnter);
848
+ this.tooltip.addEventListener("mouseleave", this.handleBeeHoverLeave);
849
+ this.dockButton = document.createElement("button");
850
+ this.dockButton.type = "button";
851
+ this.dockButton.className = "kibee-dock";
852
+ this.dockButton.setAttribute("aria-label", "Wake KiBee");
853
+ this.dockButton.innerHTML = '<span class="kibee-dock__core" aria-hidden="true"></span><span class="kibee-dock__spark" aria-hidden="true"></span>';
854
+ this.dockButton.addEventListener("click", this.handleDockClick);
855
+ this.updateDockAria();
856
+ this.root.appendChild(this.canvasWrap);
857
+ this.root.appendChild(this.hoverZone);
858
+ this.root.appendChild(this.tooltip);
859
+ for (const btn of this.radialButtons) {
860
+ this.root.appendChild(btn);
861
+ }
862
+ this.root.appendChild(this.dockButton);
863
+ container.appendChild(this.root);
864
+ this.root.dataset.rendererReady = "false";
865
+ this.syncDockState();
866
+ }
867
+ createScene() {
868
+ this.scene = new THREE2.Scene();
869
+ this.scene.fog = new THREE2.FogExp2(16117212, 16e-4);
870
+ }
871
+ createCamera() {
872
+ this.camera = new THREE2.OrthographicCamera(
873
+ 0,
874
+ window.innerWidth,
875
+ 0,
876
+ window.innerHeight,
877
+ -1e3,
878
+ 1e3
879
+ );
880
+ this.camera.position.z = 10;
881
+ }
882
+ createRenderer() {
883
+ try {
884
+ this.renderer = new THREE2.WebGLRenderer({
885
+ alpha: true,
886
+ antialias: true,
887
+ powerPreference: "high-performance"
888
+ });
889
+ } catch {
890
+ this.renderingAvailable = false;
891
+ return;
892
+ }
893
+ const performanceCap = this.resolvePixelRatioCap();
894
+ this.renderer.setPixelRatio(
895
+ Math.min(window.devicePixelRatio, performanceCap)
896
+ );
897
+ this.renderer.outputColorSpace = THREE2.SRGBColorSpace;
898
+ this.renderer.toneMapping = THREE2.ACESFilmicToneMapping;
899
+ this.renderer.toneMappingExposure = 2.1;
900
+ this.renderer.shadowMap.enabled = this.options.enableShadows ?? false;
901
+ this.renderer.shadowMap.type = THREE2.PCFSoftShadowMap;
902
+ this.renderer.domElement.className = "kibee-canvas";
903
+ this.canvasWrap.appendChild(this.renderer.domElement);
904
+ this.renderingAvailable = true;
905
+ this.root.dataset.rendererReady = "true";
906
+ }
907
+ createLights() {
908
+ const ambient = new THREE2.AmbientLight(4210752, 1.7);
909
+ const hemisphere = new THREE2.HemisphereLight(16249575, 11187132, 1.35);
910
+ const key = new THREE2.DirectionalLight(16777215, 1.8);
911
+ const fill = new THREE2.DirectionalLight(16777215, 1);
912
+ const rim = new THREE2.DirectionalLight(16773327, 0.85);
913
+ key.position.set(140, -180, 260);
914
+ fill.position.set(-140, -18, 140);
915
+ rim.position.set(-80, -120, 220);
916
+ if (this.renderer.shadowMap.enabled) {
917
+ [key, fill, rim].forEach((light) => {
918
+ light.castShadow = true;
919
+ light.shadow.mapSize.set(1024, 1024);
920
+ });
921
+ }
922
+ this.scene.add(ambient, hemisphere, key, fill, rim);
923
+ }
924
+ async loadBeeModel() {
925
+ try {
926
+ const loader = new GLTFLoader();
927
+ registerPbrSpecularGlossinessGLTFPlugin(loader);
928
+ const gltf = await loader.loadAsync(
929
+ this.options.assetUrl ?? "/models/bee-runtime.glb"
930
+ );
931
+ if (this.disposed) {
932
+ return false;
933
+ }
934
+ await applyLegacyBeeMaterialCompat(gltf);
935
+ const model = gltf.scene;
936
+ const anchor = new THREE2.Group();
937
+ const box = new THREE2.Box3().setFromObject(model);
938
+ const center = box.getCenter(new THREE2.Vector3());
939
+ const size = box.getSize(new THREE2.Vector3());
940
+ const targetHeight = this.options.assetVariant === "hero" ? 220 : 68;
941
+ const scale = targetHeight / Math.max(size.y, 1e-3);
942
+ model.position.copy(center.multiplyScalar(-1));
943
+ model.traverse((node) => {
944
+ if (!(node instanceof THREE2.Mesh)) {
945
+ return;
946
+ }
947
+ node.castShadow = Boolean(this.options.enableShadows);
948
+ node.receiveShadow = Boolean(this.options.enableShadows);
949
+ const materials = Array.isArray(node.material) ? node.material : [node.material];
950
+ for (const material of materials) {
951
+ if ("roughness" in material && typeof material.roughness === "number") {
952
+ material.roughness = Math.min(
953
+ 1,
954
+ Math.max(0.2, material.roughness * 1.02)
955
+ );
956
+ }
957
+ if ("metalness" in material && typeof material.metalness === "number") {
958
+ material.metalness = Math.min(material.metalness, 0.08);
959
+ }
960
+ if ("envMapIntensity" in material) {
961
+ material.envMapIntensity = 1;
962
+ }
963
+ if ("map" in material && material.map instanceof THREE2.Texture) {
964
+ material.map.colorSpace = THREE2.SRGBColorSpace;
965
+ }
966
+ material.needsUpdate = true;
967
+ }
968
+ });
969
+ anchor.scale.setScalar(scale);
970
+ anchor.rotation.x = Math.PI;
971
+ anchor.add(model);
972
+ this.visualRig.add(anchor);
973
+ this.visualRig.position.set(0, -10, 0);
974
+ this.applyFacing("right", true);
975
+ if (gltf.animations.length > 0) {
976
+ this.mixer = new THREE2.AnimationMixer(model);
977
+ for (const clip of gltf.animations) {
978
+ this.actions.set(clip.name, this.mixer.clipAction(clip));
979
+ }
980
+ }
981
+ this.playClipForState("idle");
982
+ this.updateBeeScreenPosition();
983
+ return true;
984
+ } catch {
985
+ return false;
986
+ }
987
+ }
988
+ createFallbackBee() {
989
+ this.runtimeWings = null;
990
+ const bodyMaterial = new THREE2.MeshStandardMaterial({ color: 13867825 });
991
+ const stripeMaterial = new THREE2.MeshStandardMaterial({ color: 2760726 });
992
+ const wingMaterial = new THREE2.MeshStandardMaterial({
993
+ color: 13483943,
994
+ transparent: true,
995
+ opacity: 0.52
996
+ });
997
+ const body = new THREE2.Mesh(
998
+ new THREE2.SphereGeometry(14, 24, 24),
999
+ bodyMaterial
1000
+ );
1001
+ body.scale.set(1.2, 0.8, 0.8);
1002
+ const stripe1 = new THREE2.Mesh(
1003
+ new THREE2.BoxGeometry(6, 24, 16),
1004
+ stripeMaterial
1005
+ );
1006
+ stripe1.position.x = -6;
1007
+ const stripe2 = new THREE2.Mesh(
1008
+ new THREE2.BoxGeometry(6, 24, 16),
1009
+ stripeMaterial
1010
+ );
1011
+ stripe2.position.x = 6;
1012
+ const wingL = new THREE2.Mesh(
1013
+ new THREE2.SphereGeometry(8, 16, 16),
1014
+ wingMaterial
1015
+ );
1016
+ wingL.scale.set(1.2, 0.5, 0.2);
1017
+ wingL.position.set(-4, -12, 2);
1018
+ wingL.rotation.z = -0.4;
1019
+ const wingR = wingL.clone();
1020
+ wingR.position.set(-4, 12, 2);
1021
+ wingR.rotation.z = 0.4;
1022
+ const head = new THREE2.Mesh(
1023
+ new THREE2.SphereGeometry(8, 20, 20),
1024
+ stripeMaterial
1025
+ );
1026
+ head.position.set(-18, 0, 0);
1027
+ this.visualRig.add(body, stripe1, stripe2, wingL, wingR, head);
1028
+ this.runtimeWings = {
1029
+ left: wingL,
1030
+ right: wingR
1031
+ };
1032
+ this.applyHomeFacing(true);
1033
+ this.updateBeeScreenPosition();
1034
+ }
1035
+ initializeDockedHome() {
1036
+ if (!this.isDockEnabled()) {
1037
+ return;
1038
+ }
1039
+ const dock = this.getDockCenter();
1040
+ this.currentX = dock.x;
1041
+ this.currentY = dock.y;
1042
+ this.docked = true;
1043
+ this.dockVisible = true;
1044
+ this.hoveringHome = false;
1045
+ this.applyHomeFacing(true);
1046
+ this.beeGroup.scale.setScalar(this.getDockedScale());
1047
+ this.updateBeeScreenPosition();
1048
+ this.syncDockState();
1049
+ }
1050
+ startRenderLoop() {
1051
+ if (!this.renderingAvailable) {
1052
+ return;
1053
+ }
1054
+ const render = () => {
1055
+ this.animationFrameId = requestAnimationFrame(render);
1056
+ const delta = this.clock.getDelta();
1057
+ const t = this.clock.getElapsedTime();
1058
+ if (this.mixer) {
1059
+ this.mixer.update(this.reducedMotion ? delta * 0.6 : delta);
1060
+ }
1061
+ if (this.runtimeWings?.left && this.runtimeWings.right) {
1062
+ const wingSpeed = this.reducedMotion ? 12 : 24;
1063
+ const wingArc = this.reducedMotion ? 0.12 : 0.25;
1064
+ this.runtimeWings.left.rotation.y = Math.sin(t * wingSpeed) * wingArc;
1065
+ this.runtimeWings.right.rotation.y = -Math.sin(t * wingSpeed) * wingArc;
1066
+ }
1067
+ this.renderer.render(this.scene, this.camera);
1068
+ };
1069
+ render();
1070
+ }
1071
+ updateBeeScreenPosition() {
1072
+ this.beeGroup.position.set(this.currentX, this.currentY, 0);
1073
+ this.positionHoverZone();
1074
+ this.publishBeePosition();
1075
+ }
1076
+ /**
1077
+ * Notify any listeners (the assist panel, debugging overlays) where the
1078
+ * bee currently sits in viewport coords. Throttled to ~60fps via the
1079
+ * existing render loop — every callsite that moves the bee already calls
1080
+ * updateBeeScreenPosition, so this rides along with no extra timer.
1081
+ */
1082
+ publishBeePosition() {
1083
+ if (typeof window === "undefined") return;
1084
+ window.dispatchEvent(
1085
+ new CustomEvent("kibee-bee-move", {
1086
+ detail: { x: this.currentX, y: this.currentY, docked: this.docked }
1087
+ })
1088
+ );
1089
+ }
1090
+ /** Public position read for callers that prefer pull over event subscription. */
1091
+ getBeePosition() {
1092
+ return { x: this.currentX, y: this.currentY, docked: this.docked };
1093
+ }
1094
+ startIdleMotion() {
1095
+ this.playClipForState("idle");
1096
+ const amplitude = this.docked ? 1 : this.hoveringHome ? 1.5 : 2;
1097
+ const tilt = this.docked ? 0.01 : 0.02;
1098
+ if (this.reducedMotion) {
1099
+ gsap.set(this.beeGroup.position, { y: this.currentY });
1100
+ gsap.to(this.beeGroup.rotation, {
1101
+ z: tilt,
1102
+ duration: 0.2,
1103
+ ease: "sine.out",
1104
+ overwrite: "auto"
1105
+ });
1106
+ return;
1107
+ }
1108
+ this.idleTween = gsap.to(this.beeGroup.position, {
1109
+ y: this.currentY + amplitude,
1110
+ duration: this.docked ? 2 : 1.8,
1111
+ yoyo: true,
1112
+ repeat: -1,
1113
+ ease: "sine.inOut"
1114
+ });
1115
+ gsap.to(this.beeGroup.rotation, {
1116
+ z: tilt,
1117
+ duration: 0.6,
1118
+ ease: "sine.inOut",
1119
+ overwrite: "auto"
1120
+ });
1121
+ }
1122
+ startThinkingMotion() {
1123
+ this.playClipForState("thinking");
1124
+ if (this.reducedMotion) {
1125
+ gsap.to(this.beeGroup.rotation, {
1126
+ z: 0.1,
1127
+ duration: 0.2,
1128
+ ease: "sine.out",
1129
+ overwrite: "auto"
1130
+ });
1131
+ return;
1132
+ }
1133
+ this.idleTween = gsap.to(this.beeGroup.rotation, {
1134
+ z: 0.18,
1135
+ duration: 0.5,
1136
+ yoyo: true,
1137
+ repeat: -1,
1138
+ ease: "sine.inOut"
1139
+ });
1140
+ gsap.to(this.beeGroup.position, {
1141
+ x: this.currentX + 8,
1142
+ duration: 0.5,
1143
+ yoyo: true,
1144
+ repeat: -1,
1145
+ ease: "sine.inOut"
1146
+ });
1147
+ }
1148
+ startCelebrateMotion() {
1149
+ this.playClipForState("celebrating");
1150
+ if (this.reducedMotion) {
1151
+ gsap.to(this.beeGroup.rotation, {
1152
+ z: 0.12,
1153
+ duration: 0.18,
1154
+ yoyo: true,
1155
+ repeat: 1,
1156
+ ease: "power1.out",
1157
+ overwrite: "auto"
1158
+ });
1159
+ return;
1160
+ }
1161
+ this.celebrateTween = gsap.timeline();
1162
+ this.celebrateTween.to(this.beeGroup.position, {
1163
+ y: this.currentY - 18,
1164
+ duration: 0.22,
1165
+ ease: "power2.out"
1166
+ }).to(this.beeGroup.rotation, {
1167
+ z: 0.42,
1168
+ duration: 0.18,
1169
+ ease: "power1.out"
1170
+ }).to(this.beeGroup.position, {
1171
+ y: this.currentY,
1172
+ duration: 0.28,
1173
+ ease: "bounce.out"
1174
+ }).to(
1175
+ this.beeGroup.rotation,
1176
+ {
1177
+ z: 0,
1178
+ duration: 0.25,
1179
+ ease: "power1.out"
1180
+ },
1181
+ "<"
1182
+ );
1183
+ }
1184
+ stopStateTweens() {
1185
+ if (this.idleTween) {
1186
+ this.idleTween.kill();
1187
+ this.idleTween = null;
1188
+ }
1189
+ if (this.celebrateTween) {
1190
+ this.celebrateTween.kill();
1191
+ this.celebrateTween = null;
1192
+ }
1193
+ gsap.killTweensOf(this.beeGroup.position);
1194
+ gsap.killTweensOf(this.beeGroup.rotation);
1195
+ gsap.killTweensOf(this.beeGroup.scale);
1196
+ }
1197
+ positionTooltipNearBee() {
1198
+ if (!this.tooltip || this.tooltip.style.display === "none") return;
1199
+ const tw = this.tooltip.offsetWidth || 220;
1200
+ const th = this.tooltip.offsetHeight || 70;
1201
+ const layout = this.computeRadialLayout(tw, th);
1202
+ this.tooltip.style.left = `${layout.tooltip.x}px`;
1203
+ this.tooltip.style.top = `${layout.tooltip.y}px`;
1204
+ this.tooltip.dataset.arrowEdge = layout.tooltip.arrowEdge;
1205
+ }
1206
+ /**
1207
+ * Compute viewport-aware radial positions for radial action buttons and the
1208
+ * tooltip. All returned coordinates are CSS `left`/`top` pixel values.
1209
+ * The tooltip's `top` value is the *rendered center Y* (because CSS applies
1210
+ * `transform: translate(0, -50%)` to the tooltip element).
1211
+ */
1212
+ computeRadialLayout(tooltipW, tooltipH) {
1213
+ const bx = this.currentX;
1214
+ const by = this.currentY;
1215
+ const vw = window.innerWidth;
1216
+ const vh = window.innerHeight;
1217
+ const RADIUS = 54;
1218
+ const BTN = 30;
1219
+ const MARGIN = 8;
1220
+ const TOOLTIP_GAP = 70;
1221
+ const ARC = 2 * Math.PI / 3;
1222
+ const midAngle = Math.atan2(vh / 2 - by, vw / 2 - bx);
1223
+ const n = Math.max(1, this.radialButtons.length);
1224
+ const step = n <= 1 ? 0 : ARC / (n - 1);
1225
+ const startAngle = midAngle - ARC / 2;
1226
+ const angles = Array.from({ length: n }, (_, i) => startAngle + i * step);
1227
+ const buttons = angles.map((a) => ({
1228
+ x: Math.max(
1229
+ MARGIN,
1230
+ Math.min(vw - BTN - MARGIN, bx + RADIUS * Math.cos(a) - BTN / 2)
1231
+ ),
1232
+ y: Math.max(
1233
+ MARGIN,
1234
+ Math.min(vh - BTN - MARGIN, by + RADIUS * Math.sin(a) - BTN / 2)
1235
+ )
1236
+ }));
1237
+ const oppAngle = midAngle + Math.PI;
1238
+ const cos = Math.cos(oppAngle);
1239
+ const sin = Math.sin(oppAngle);
1240
+ let tooltipLeft;
1241
+ let tooltipCssTop;
1242
+ if (Math.abs(cos) >= Math.abs(sin)) {
1243
+ tooltipLeft = cos > 0 ? bx + TOOLTIP_GAP : bx - TOOLTIP_GAP - tooltipW;
1244
+ tooltipCssTop = by;
1245
+ } else {
1246
+ tooltipLeft = bx - tooltipW / 2;
1247
+ tooltipCssTop = sin > 0 ? by + TOOLTIP_GAP + tooltipH / 2 : by - TOOLTIP_GAP - tooltipH / 2;
1248
+ }
1249
+ tooltipLeft = Math.max(MARGIN, Math.min(vw - tooltipW - MARGIN, tooltipLeft));
1250
+ tooltipCssTop = Math.max(
1251
+ MARGIN + tooltipH / 2,
1252
+ Math.min(vh - MARGIN - tooltipH / 2, tooltipCssTop)
1253
+ );
1254
+ const tipTop = tooltipCssTop - tooltipH / 2;
1255
+ const tipBottom = tooltipCssTop + tooltipH / 2;
1256
+ const tipRight = tooltipLeft + tooltipW;
1257
+ for (const btn of buttons) {
1258
+ const overlapX = tooltipLeft < btn.x + BTN && tipRight > btn.x;
1259
+ const overlapY = tipTop < btn.y + BTN && tipBottom > btn.y;
1260
+ if (overlapX && overlapY) {
1261
+ const nudge = BTN + MARGIN;
1262
+ tooltipLeft = Math.max(
1263
+ MARGIN,
1264
+ Math.min(vw - tooltipW - MARGIN, tooltipLeft + cos * nudge)
1265
+ );
1266
+ tooltipCssTop = Math.max(
1267
+ MARGIN + tooltipH / 2,
1268
+ Math.min(
1269
+ vh - MARGIN - tooltipH / 2,
1270
+ tooltipCssTop + sin * nudge
1271
+ )
1272
+ );
1273
+ }
1274
+ }
1275
+ const BEE_HW = 62;
1276
+ const BEE_HH = 34;
1277
+ const tipFinalTop = tooltipCssTop - tooltipH / 2;
1278
+ const tipFinalBottom = tooltipCssTop + tooltipH / 2;
1279
+ const tipFinalRight = tooltipLeft + tooltipW;
1280
+ const beeLeft = bx - BEE_HW;
1281
+ const beeRight = bx + BEE_HW;
1282
+ const beeTop = by - BEE_HH;
1283
+ const beeBottom = by + BEE_HH;
1284
+ const overlapBeeX = tooltipLeft < beeRight && tipFinalRight > beeLeft;
1285
+ const overlapBeeY = tipFinalTop < beeBottom && tipFinalBottom > beeTop;
1286
+ if (overlapBeeX && overlapBeeY) {
1287
+ const spaceLeft = tooltipLeft - beeLeft;
1288
+ const spaceRight = beeRight - tipFinalRight;
1289
+ if (cos <= 0) {
1290
+ tooltipLeft = Math.max(MARGIN, beeLeft - tooltipW - MARGIN);
1291
+ } else {
1292
+ tooltipLeft = Math.min(vw - tooltipW - MARGIN, beeRight + MARGIN);
1293
+ }
1294
+ void spaceLeft;
1295
+ void spaceRight;
1296
+ }
1297
+ const relX = tooltipLeft + tooltipW / 2 - bx;
1298
+ const relY = tooltipCssTop - by;
1299
+ let arrowEdge;
1300
+ if (Math.abs(relX) >= Math.abs(relY)) {
1301
+ arrowEdge = relX > 0 ? "left" : "right";
1302
+ } else {
1303
+ arrowEdge = relY > 0 ? "top" : "bottom";
1304
+ }
1305
+ return {
1306
+ buttons,
1307
+ tooltip: { x: tooltipLeft, y: tooltipCssTop, arrowEdge }
1308
+ };
1309
+ }
1310
+ showRadialButtons() {
1311
+ if (!this.radialButtons.length || this.isDragging) return;
1312
+ const tw = this.tooltip.offsetWidth || 220;
1313
+ const th = this.tooltip.offsetHeight || 70;
1314
+ const layout = this.computeRadialLayout(tw, th);
1315
+ this.radialButtons.forEach((btn, i) => {
1316
+ const pos = layout.buttons[i];
1317
+ if (!pos) return;
1318
+ btn.style.left = `${pos.x}px`;
1319
+ btn.style.top = `${pos.y}px`;
1320
+ btn.style.display = "grid";
1321
+ void btn.offsetWidth;
1322
+ btn.classList.remove("kibee-action--hidden");
1323
+ btn.classList.add("kibee-action--visible");
1324
+ });
1325
+ if (this.tooltip.style.display !== "none") {
1326
+ this.tooltip.style.left = `${layout.tooltip.x}px`;
1327
+ this.tooltip.style.top = `${layout.tooltip.y}px`;
1328
+ this.tooltip.dataset.arrowEdge = layout.tooltip.arrowEdge;
1329
+ }
1330
+ }
1331
+ hideRadialButtons() {
1332
+ for (const btn of this.radialButtons) {
1333
+ btn.classList.remove("kibee-action--visible");
1334
+ btn.classList.add("kibee-action--hidden");
1335
+ }
1336
+ setTimeout(() => {
1337
+ for (const btn of this.radialButtons) {
1338
+ if (!btn.classList.contains("kibee-action--visible")) {
1339
+ btn.style.display = "none";
1340
+ }
1341
+ }
1342
+ }, 260);
1343
+ }
1344
+ positionHoverZone() {
1345
+ if (!this.hoverZone) return;
1346
+ const size = this.actionHandler ? 100 : 72;
1347
+ this.hoverZone.style.left = `${this.currentX - size / 2}px`;
1348
+ this.hoverZone.style.top = `${this.currentY - size / 2}px`;
1349
+ this.hoverZone.style.width = `${size}px`;
1350
+ this.hoverZone.style.height = `${size}px`;
1351
+ }
1352
+ startTooltipPulse() {
1353
+ if (this.tooltipHasNav) return;
1354
+ this.clearTooltipPulse();
1355
+ this.tooltipPulseTimer = window.setTimeout(() => {
1356
+ if (!this.tooltipMessage) return;
1357
+ this.tooltip.classList.remove("kibee-tooltip--visible");
1358
+ this.tooltip.classList.add("kibee-tooltip--hidden");
1359
+ }, 3400);
1360
+ }
1361
+ clearTooltipPulse() {
1362
+ if (this.tooltipPulseTimer !== null) {
1363
+ clearTimeout(this.tooltipPulseTimer);
1364
+ this.tooltipPulseTimer = null;
1365
+ }
1366
+ }
1367
+ handleBeeHoverEnter = () => {
1368
+ if (this.isDragging) return;
1369
+ this.clearHideButtonsTimer();
1370
+ if (this.tooltipMessage && !this.docked) {
1371
+ this.tooltip.style.display = "block";
1372
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
1373
+ this.tooltip.classList.add("kibee-tooltip--visible");
1374
+ this.clearTooltipPulse();
1375
+ }
1376
+ if (this.actionHandler && this.state === "idle") {
1377
+ this.showRadialButtons();
1378
+ }
1379
+ };
1380
+ handleBeeHoverLeave = () => {
1381
+ if (this.tooltipMessage && !this.docked) {
1382
+ this.startTooltipPulse();
1383
+ }
1384
+ if (this.actionHandler) {
1385
+ this.scheduleHideButtons();
1386
+ }
1387
+ };
1388
+ scheduleHideButtons() {
1389
+ this.clearHideButtonsTimer();
1390
+ this.hideButtonsTimer = window.setTimeout(() => {
1391
+ this.hideButtonsTimer = null;
1392
+ this.hideRadialButtons();
1393
+ }, 320);
1394
+ }
1395
+ clearHideButtonsTimer() {
1396
+ if (this.hideButtonsTimer !== null) {
1397
+ clearTimeout(this.hideButtonsTimer);
1398
+ this.hideButtonsTimer = null;
1399
+ }
1400
+ }
1401
+ // ── Drag handlers ───────────────────────────────────────────────────────
1402
+ /** Instant undock used when the user drags the bee off the dock / launcher */
1403
+ fastUndockForDrag() {
1404
+ if (!this.isDockEnabled() || !this.docked) return;
1405
+ this.docked = false;
1406
+ this.dockVisible = false;
1407
+ this.hoveringHome = false;
1408
+ this.stopStateTweens();
1409
+ gsap.killTweensOf(this);
1410
+ gsap.killTweensOf(this.beeGroup.position);
1411
+ gsap.killTweensOf(this.beeGroup.rotation);
1412
+ gsap.killTweensOf(this.beeGroup.scale);
1413
+ this.beeGroup.scale.setScalar(this.getActiveScale());
1414
+ this.syncDockState();
1415
+ }
1416
+ handlePointerDown = (event) => {
1417
+ if (event.button !== 0) return;
1418
+ event.preventDefault();
1419
+ this.isDragging = true;
1420
+ this.dragMoved = false;
1421
+ this.dragArmedFromDock = this.docked;
1422
+ this.dragStartX = this.currentX - event.clientX;
1423
+ this.dragStartY = this.currentY - event.clientY;
1424
+ if (this.idleTween) {
1425
+ this.idleTween.kill();
1426
+ this.idleTween = null;
1427
+ }
1428
+ if (!this.docked) {
1429
+ gsap.killTweensOf(this);
1430
+ gsap.killTweensOf(this.beeGroup.position);
1431
+ gsap.killTweensOf(this.beeGroup.rotation);
1432
+ }
1433
+ this.hoverZone.setPointerCapture(event.pointerId);
1434
+ this.root.classList.add("kibee-root--dragging");
1435
+ this.hideRadialButtons();
1436
+ };
1437
+ handlePointerMove = (event) => {
1438
+ if (!this.isDragging) return;
1439
+ const newX = event.clientX + this.dragStartX;
1440
+ const newY = event.clientY + this.dragStartY;
1441
+ if (this.docked) {
1442
+ if (!this.dragMoved && (Math.abs(newX - this.currentX) > 4 || Math.abs(newY - this.currentY) > 4)) {
1443
+ this.dragMoved = true;
1444
+ this.fastUndockForDrag();
1445
+ }
1446
+ if (this.docked) return;
1447
+ }
1448
+ if (!this.dragMoved && (Math.abs(newX - this.currentX) > 4 || Math.abs(newY - this.currentY) > 4)) {
1449
+ this.dragMoved = true;
1450
+ }
1451
+ const pad = 36;
1452
+ this.currentX = Math.max(pad, Math.min(window.innerWidth - pad, newX));
1453
+ this.currentY = Math.max(pad, Math.min(window.innerHeight - pad, newY));
1454
+ this.updateBeeScreenPosition();
1455
+ if (this.tooltipMessage && this.tooltip.style.display !== "none") {
1456
+ this.positionTooltipNearBee();
1457
+ }
1458
+ };
1459
+ handlePointerUp = (event) => {
1460
+ if (!this.isDragging) return;
1461
+ const armedFromDock = this.dragArmedFromDock;
1462
+ const moved = this.dragMoved;
1463
+ if (armedFromDock && !moved && this.docked) {
1464
+ this.handleDockClick();
1465
+ }
1466
+ if (!moved && !this.docked && this.dockClickBehavior === "assist-panel" && this.state === "idle") {
1467
+ window.dispatchEvent(
1468
+ new CustomEvent("kibee-assist-toggle", {
1469
+ detail: { x: this.currentX, y: this.currentY }
1470
+ })
1471
+ );
1472
+ }
1473
+ this.isDragging = false;
1474
+ this.dragArmedFromDock = false;
1475
+ this.hoverZone.releasePointerCapture(event.pointerId);
1476
+ this.root.classList.remove("kibee-root--dragging");
1477
+ if (this.state === "idle") {
1478
+ this.startIdleMotion();
1479
+ }
1480
+ if (!this.docked && this.actionHandler && this.state === "idle") {
1481
+ if (moved || !armedFromDock) {
1482
+ this.clearHideButtonsTimer();
1483
+ this.showRadialButtons();
1484
+ }
1485
+ }
1486
+ if (!this.docked && this.tooltipMessage && this.state === "idle" && (moved || !armedFromDock)) {
1487
+ this.tooltip.style.display = "block";
1488
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
1489
+ this.tooltip.classList.add("kibee-tooltip--visible");
1490
+ this.positionTooltipNearBee();
1491
+ this.clearTooltipPulse();
1492
+ }
1493
+ };
1494
+ handleResize = () => {
1495
+ this.resize();
1496
+ };
1497
+ handleVisibilityChange = () => {
1498
+ if (document.hidden) {
1499
+ if (this.animationFrameId !== null) {
1500
+ cancelAnimationFrame(this.animationFrameId);
1501
+ this.animationFrameId = null;
1502
+ }
1503
+ } else if (this.animationFrameId === null && this.renderingAvailable) {
1504
+ this.clock.getDelta();
1505
+ this.startRenderLoop();
1506
+ }
1507
+ };
1508
+ handlePanelOpen = (_event) => {
1509
+ this.panelOpen = true;
1510
+ if (this.tooltip.style.display !== "none") {
1511
+ this.tooltip.classList.remove("kibee-tooltip--visible");
1512
+ this.tooltip.classList.add("kibee-tooltip--hidden");
1513
+ this.tooltip.style.display = "none";
1514
+ this.clearTooltipPulse();
1515
+ }
1516
+ if (!this.docked) return;
1517
+ requestAnimationFrame(() => {
1518
+ if (!this.panelOpen) return;
1519
+ const panel = document.querySelector(".kbp-panel");
1520
+ if (!panel) return;
1521
+ const rect = panel.getBoundingClientRect();
1522
+ if (!rect.width) return;
1523
+ void this.moveTo(rect.left + rect.width / 2, rect.top, {
1524
+ offsetX: 0,
1525
+ offsetY: 0,
1526
+ duration: 0.55
1527
+ });
1528
+ });
1529
+ };
1530
+ handlePanelClose = () => {
1531
+ this.panelOpen = false;
1532
+ if (this.dockClickBehavior !== "assist-panel") {
1533
+ void this.dock();
1534
+ }
1535
+ };
1536
+ handleDockClick = () => {
1537
+ if (document.body.dataset.kibeeEmptyDock === "true") {
1538
+ return;
1539
+ }
1540
+ if (this.dockClickBehavior === "assist-panel") {
1541
+ if (this.state !== "idle") return;
1542
+ window.dispatchEvent(
1543
+ new CustomEvent("kibee-assist-toggle", {
1544
+ detail: { x: this.currentX, y: this.currentY }
1545
+ })
1546
+ );
1547
+ return;
1548
+ }
1549
+ if (!this.docked) {
1550
+ return;
1551
+ }
1552
+ this.hideMessage();
1553
+ void this.undock({ hideDock: false }).then(() => {
1554
+ this.speak("How can I help? Hover to bookmark or flag an issue.");
1555
+ });
1556
+ };
1557
+ updateDockAria() {
1558
+ if (!this.dockButton) {
1559
+ return;
1560
+ }
1561
+ if (this.dockClickBehavior === "assist-panel") {
1562
+ this.dockButton.setAttribute(
1563
+ "aria-label",
1564
+ "Open or close KiBee assistant"
1565
+ );
1566
+ } else {
1567
+ this.dockButton.setAttribute("aria-label", "Wake KiBee");
1568
+ }
1569
+ }
1570
+ shouldReduceMotion() {
1571
+ if (this.options.motionMode === "reduced") {
1572
+ return true;
1573
+ }
1574
+ if (this.options.motionMode === "full") {
1575
+ return false;
1576
+ }
1577
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
1578
+ }
1579
+ resolvePixelRatioCap() {
1580
+ const memoryAwareNavigator = navigator;
1581
+ if (this.options.qualityTier === "high") {
1582
+ return 2;
1583
+ }
1584
+ if (this.options.qualityTier === "low") {
1585
+ return 1.05;
1586
+ }
1587
+ if (this.options.qualityTier === "balanced") {
1588
+ return 1.45;
1589
+ }
1590
+ const lowCpu = navigator.hardwareConcurrency !== void 0 && navigator.hardwareConcurrency <= 4;
1591
+ const lowMemory = memoryAwareNavigator.deviceMemory !== void 0 && memoryAwareNavigator.deviceMemory <= 4;
1592
+ return lowCpu || lowMemory ? 1.05 : 1.45;
1593
+ }
1594
+ playClipForState(state) {
1595
+ if (!this.mixer || this.actions.size === 0) {
1596
+ return;
1597
+ }
1598
+ const preferredName = CLIP_BY_STATE[state];
1599
+ const nextAction = this.actions.get(preferredName) ?? this.actions.values().next().value;
1600
+ if (!nextAction) {
1601
+ return;
1602
+ }
1603
+ const previousAction = this.activeActionName ? this.actions.get(this.activeActionName) ?? null : null;
1604
+ if (previousAction === nextAction) {
1605
+ nextAction.timeScale = this.getClipTimeScale(state);
1606
+ return;
1607
+ }
1608
+ if (previousAction) {
1609
+ previousAction.fadeOut(0.24);
1610
+ }
1611
+ nextAction.reset();
1612
+ nextAction.enabled = true;
1613
+ nextAction.timeScale = this.getClipTimeScale(state);
1614
+ nextAction.clampWhenFinished = preferredName === "take_off_and_land";
1615
+ nextAction.setLoop(
1616
+ preferredName === "take_off_and_land" ? THREE2.LoopOnce : THREE2.LoopRepeat,
1617
+ preferredName === "take_off_and_land" ? 1 : Infinity
1618
+ );
1619
+ nextAction.fadeIn(0.28).play();
1620
+ this.activeActionName = nextAction.getClip().name;
1621
+ }
1622
+ getClipTimeScale(state) {
1623
+ if (state === "guiding") {
1624
+ return this.reducedMotion ? 0.7 : 1.12;
1625
+ }
1626
+ if (state === "thinking") {
1627
+ return this.reducedMotion ? 0.5 : 0.82;
1628
+ }
1629
+ return this.reducedMotion ? 0.65 : 1;
1630
+ }
1631
+ applyFacing(facing, immediate = false) {
1632
+ this.currentFacing = facing;
1633
+ const targetY = facing === "right" ? -Math.PI / 1.56 : Math.PI / 1.56;
1634
+ const tiltZ = facing === "right" ? -0.02 : 0.02;
1635
+ if (immediate) {
1636
+ this.visualRig.rotation.set(0.12, targetY, tiltZ);
1637
+ return;
1638
+ }
1639
+ gsap.to(this.visualRig.rotation, {
1640
+ x: 0.12,
1641
+ y: targetY,
1642
+ z: tiltZ,
1643
+ duration: 0.28,
1644
+ ease: "power2.out",
1645
+ overwrite: "auto"
1646
+ });
1647
+ }
1648
+ applyHomeFacing(immediate = false) {
1649
+ const facesLeft = this.isDockOnRight();
1650
+ this.currentFacing = facesLeft ? "left" : "right";
1651
+ const yRot = facesLeft ? Math.PI / 1.6 : -Math.PI / 1.6;
1652
+ if (immediate) {
1653
+ this.visualRig.rotation.set(0.16, yRot, 0.12);
1654
+ return;
1655
+ }
1656
+ gsap.to(this.visualRig.rotation, {
1657
+ x: 0.16,
1658
+ y: yRot,
1659
+ z: 0.12,
1660
+ duration: 0.28,
1661
+ ease: "power2.out",
1662
+ overwrite: "auto"
1663
+ });
1664
+ }
1665
+ syncDockState() {
1666
+ if (!this.root) {
1667
+ return;
1668
+ }
1669
+ this.root.dataset.docked = String(this.docked);
1670
+ this.root.dataset.idle = String(this.state === "idle");
1671
+ this.root.dataset.dockVisible = String(this.dockVisible);
1672
+ this.root.style.setProperty("--kibee-dock-size", `${this.getDockSize()}px`);
1673
+ const cssVars = this.resolveDockCSSVars();
1674
+ this.root.style.setProperty("--kibee-dock-right", cssVars.right);
1675
+ this.root.style.setProperty("--kibee-dock-bottom", cssVars.bottom);
1676
+ this.root.style.setProperty("--kibee-dock-left", cssVars.left);
1677
+ this.root.style.setProperty("--kibee-dock-top", cssVars.top);
1678
+ if (this.hoverZone) {
1679
+ this.hoverZone.style.pointerEvents = "";
1680
+ }
1681
+ if (this.dockButton) {
1682
+ const passThrough = this.docked && this.dockClickBehavior === "assist-panel";
1683
+ this.dockButton.style.pointerEvents = passThrough ? "none" : "";
1684
+ }
1685
+ }
1686
+ isDockEnabled() {
1687
+ return this.options.dockEnabled ?? true;
1688
+ }
1689
+ setCustomDockPosition(x, y, opts = {}) {
1690
+ const persist = opts.persist !== false && this.restorePersistedDockPosition;
1691
+ this.customDockPos = { x, y };
1692
+ if (persist) {
1693
+ try {
1694
+ localStorage.setItem("kibee:dock_pos", JSON.stringify({ x, y }));
1695
+ } catch {
1696
+ }
1697
+ }
1698
+ this.syncDockState();
1699
+ }
1700
+ /**
1701
+ * Snap the docked bee and dock chrome to the current `getDockCenter()` without a full resize.
1702
+ * Use after `setCustomDockPosition` when the dock target moves (e.g. scroll-linked layout).
1703
+ */
1704
+ refreshDockedBeeAfterDockMove() {
1705
+ if (!this.isDockEnabled() || !this.docked || this.isDragging) return;
1706
+ const dock = this.getDockCenter();
1707
+ this.currentX = dock.x;
1708
+ this.currentY = dock.y;
1709
+ this.updateBeeScreenPosition();
1710
+ this.positionTooltipNearBee();
1711
+ this.syncDockState();
1712
+ }
1713
+ getDockCenter() {
1714
+ if (this.customDockPos) {
1715
+ return { ...this.customDockPos };
1716
+ }
1717
+ const size = this.getDockSize();
1718
+ const pos = this.options.dockPosition ?? "bottom-right";
1719
+ if (typeof pos === "string") {
1720
+ return this.presetToCenter(pos, size);
1721
+ }
1722
+ if ("preset" in pos) {
1723
+ const base = this.presetToCenter(pos.preset, size);
1724
+ return { x: base.x + (pos.offsetX ?? 0), y: base.y + (pos.offsetY ?? 0) };
1725
+ }
1726
+ if ("custom" in pos) {
1727
+ return { x: pos.custom.x, y: pos.custom.y };
1728
+ }
1729
+ if ("inline" in pos) {
1730
+ return this.resolveInlineCenter(pos.inline, size);
1731
+ }
1732
+ return this.presetToCenter("bottom-right", size);
1733
+ }
1734
+ presetToCenter(preset, size) {
1735
+ const margin = 22;
1736
+ const padding = 24;
1737
+ switch (preset) {
1738
+ case "bottom-right":
1739
+ return {
1740
+ x: window.innerWidth - margin - size / 2,
1741
+ y: window.innerHeight - padding - size / 2
1742
+ };
1743
+ case "bottom-left":
1744
+ return {
1745
+ x: margin + size / 2,
1746
+ y: window.innerHeight - padding - size / 2
1747
+ };
1748
+ case "top-right":
1749
+ return {
1750
+ x: window.innerWidth - margin - size / 2,
1751
+ y: padding + size / 2
1752
+ };
1753
+ case "top-left":
1754
+ return {
1755
+ x: margin + size / 2,
1756
+ y: padding + size / 2
1757
+ };
1758
+ }
1759
+ }
1760
+ resolveInlineCenter(anchor, size) {
1761
+ const el = typeof anchor.container === "string" ? document.querySelector(anchor.container) : anchor.container;
1762
+ if (!el) return this.presetToCenter("bottom-right", size);
1763
+ const rect = el.getBoundingClientRect();
1764
+ const align = anchor.align ?? "center";
1765
+ const x = align === "left" ? rect.left + size / 2 : align === "right" ? rect.right - size / 2 : rect.left + rect.width / 2;
1766
+ const y = rect.top + rect.height / 2;
1767
+ return { x, y };
1768
+ }
1769
+ /** Returns true when the dock sits on the right half of the viewport */
1770
+ isDockOnRight() {
1771
+ const pos = this.options.dockPosition ?? "bottom-right";
1772
+ if (typeof pos === "string") {
1773
+ return pos === "bottom-right" || pos === "top-right";
1774
+ }
1775
+ if ("preset" in pos) {
1776
+ return pos.preset === "bottom-right" || pos.preset === "top-right";
1777
+ }
1778
+ return this.getDockCenter().x > window.innerWidth / 2;
1779
+ }
1780
+ /** Returns true when the dock sits on the bottom half of the viewport */
1781
+ isDockOnBottom() {
1782
+ const pos = this.options.dockPosition ?? "bottom-right";
1783
+ if (typeof pos === "string") {
1784
+ return pos === "bottom-right" || pos === "bottom-left";
1785
+ }
1786
+ if ("preset" in pos) {
1787
+ return pos.preset === "bottom-right" || pos.preset === "bottom-left";
1788
+ }
1789
+ return this.getDockCenter().y > window.innerHeight / 2;
1790
+ }
1791
+ resolveDockCSSVars() {
1792
+ const defaults = { right: "auto", bottom: "auto", left: "auto", top: "auto" };
1793
+ if (this.customDockPos) {
1794
+ const size = this.getDockSize();
1795
+ return {
1796
+ ...defaults,
1797
+ left: `${this.customDockPos.x - size / 2}px`,
1798
+ top: `${this.customDockPos.y - size / 2}px`
1799
+ };
1800
+ }
1801
+ const pos = this.options.dockPosition ?? "bottom-right";
1802
+ if (typeof pos === "string" || "preset" in pos) {
1803
+ const preset = typeof pos === "string" ? pos : pos.preset;
1804
+ switch (preset) {
1805
+ case "bottom-right":
1806
+ return { ...defaults, right: "22px", bottom: "24px" };
1807
+ case "bottom-left":
1808
+ return { ...defaults, left: "22px", bottom: "24px" };
1809
+ case "top-right":
1810
+ return { ...defaults, right: "22px", top: "24px" };
1811
+ case "top-left":
1812
+ return { ...defaults, left: "22px", top: "24px" };
1813
+ }
1814
+ }
1815
+ if ("custom" in pos) {
1816
+ return { ...defaults, left: `${pos.custom.x}px`, top: `${pos.custom.y}px` };
1817
+ }
1818
+ return defaults;
1819
+ }
1820
+ getWakeHoverPosition() {
1821
+ const dock = this.getDockCenter();
1822
+ const xDir = this.isDockOnRight() ? -1 : 1;
1823
+ const yDir = this.isDockOnBottom() ? -1 : 1;
1824
+ return {
1825
+ x: dock.x + xDir * 34,
1826
+ y: dock.y + yDir * 66
1827
+ };
1828
+ }
1829
+ getDockScreenPosition() {
1830
+ return this.getDockCenter();
1831
+ }
1832
+ getDockSize() {
1833
+ return this.options.dockSize ?? 60;
1834
+ }
1835
+ getActiveScale() {
1836
+ return 0.86;
1837
+ }
1838
+ getDockedScale() {
1839
+ return 0.48;
1840
+ }
1841
+ getHomeHoverScale() {
1842
+ return 0.68;
1843
+ }
1844
+ disposeSceneObjects() {
1845
+ this.scene?.traverse((node) => {
1846
+ if (!(node instanceof THREE2.Mesh)) {
1847
+ return;
1848
+ }
1849
+ node.geometry.dispose();
1850
+ const materials = Array.isArray(node.material) ? node.material : [node.material];
1851
+ for (const material of materials) {
1852
+ const textureKeys = [
1853
+ "map",
1854
+ "normalMap",
1855
+ "aoMap",
1856
+ "roughnessMap",
1857
+ "metalnessMap",
1858
+ "specularMap",
1859
+ "emissiveMap"
1860
+ ];
1861
+ for (const key of textureKeys) {
1862
+ const texture = material[key];
1863
+ if (texture instanceof THREE2.Texture) {
1864
+ texture.dispose();
1865
+ }
1866
+ }
1867
+ material.dispose();
1868
+ }
1869
+ });
1870
+ }
1871
+ };
1872
+ export {
1873
+ ThreeBeeRenderer,
1874
+ applyLegacyBeeMaterialCompat
1875
+ };