@shohojdhara/atomix 0.3.0 → 0.3.1

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 (95) hide show
  1. package/dist/atomix.css +309 -105
  2. package/dist/atomix.min.css +3 -5
  3. package/dist/index.d.ts +804 -53
  4. package/dist/index.esm.js +16367 -16413
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +16275 -16336
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.min.js +1 -1
  9. package/dist/index.min.js.map +1 -1
  10. package/dist/themes/applemix.css +309 -105
  11. package/dist/themes/applemix.min.css +5 -7
  12. package/dist/themes/boomdevs.css +202 -10
  13. package/dist/themes/boomdevs.min.css +3 -5
  14. package/dist/themes/esrar.css +309 -105
  15. package/dist/themes/esrar.min.css +4 -6
  16. package/dist/themes/flashtrade.css +310 -105
  17. package/dist/themes/flashtrade.min.css +5 -7
  18. package/dist/themes/mashroom.css +300 -96
  19. package/dist/themes/mashroom.min.css +4 -6
  20. package/dist/themes/shaj-default.css +300 -96
  21. package/dist/themes/shaj-default.min.css +4 -6
  22. package/package.json +1 -1
  23. package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
  24. package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
  25. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
  26. package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
  27. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
  28. package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
  29. package/src/components/AtomixGlass/shader-utils.ts +8 -0
  30. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
  31. package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
  32. package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
  33. package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
  34. package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
  35. package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
  36. package/src/components/Button/Button.tsx +62 -17
  37. package/src/components/Callout/Callout.test.tsx +8 -14
  38. package/src/components/Card/Card.tsx +103 -1
  39. package/src/components/Card/index.ts +3 -2
  40. package/src/components/Icon/index.ts +1 -1
  41. package/src/components/Modal/Modal.stories.tsx +29 -38
  42. package/src/components/Modal/Modal.tsx +4 -4
  43. package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
  44. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
  45. package/src/components/Popover/Popover.tsx +1 -1
  46. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
  47. package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
  48. package/src/lib/composables/shared-mouse-tracker.ts +133 -0
  49. package/src/lib/composables/useAtomixGlass.ts +303 -115
  50. package/src/lib/theme/ThemeManager.integration.test.ts +124 -0
  51. package/src/lib/theme/ThemeManager.stories.tsx +13 -13
  52. package/src/lib/theme/ThemeManager.test.ts +4 -0
  53. package/src/lib/theme/ThemeManager.ts +203 -59
  54. package/src/lib/theme/ThemeProvider.tsx +183 -33
  55. package/src/lib/theme/composeTheme.ts +375 -0
  56. package/src/lib/theme/createTheme.test.ts +475 -0
  57. package/src/lib/theme/createTheme.ts +510 -0
  58. package/src/lib/theme/generateCSSVariables.ts +713 -0
  59. package/src/lib/theme/index.ts +67 -0
  60. package/src/lib/theme/themeUtils.ts +333 -0
  61. package/src/lib/theme/types.ts +337 -8
  62. package/src/lib/theme/useTheme.test.tsx +2 -1
  63. package/src/lib/theme/useTheme.ts +6 -22
  64. package/src/lib/types/components.ts +148 -59
  65. package/src/styles/01-settings/_index.scss +2 -2
  66. package/src/styles/01-settings/_settings.badge.scss +2 -2
  67. package/src/styles/01-settings/_settings.border-radius.scss +1 -1
  68. package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
  69. package/src/styles/01-settings/_settings.modal.scss +1 -1
  70. package/src/styles/01-settings/_settings.spacing.scss +14 -13
  71. package/src/styles/03-generic/_generic.root.scss +131 -50
  72. package/src/styles/05-objects/_objects.block.scss +1 -1
  73. package/src/styles/06-components/_components.atomix-glass.scss +20 -22
  74. package/src/styles/06-components/_components.badge.scss +2 -2
  75. package/src/styles/06-components/_components.button.scss +1 -1
  76. package/src/styles/06-components/_components.callout.scss +1 -1
  77. package/src/styles/06-components/_components.card.scss +74 -2
  78. package/src/styles/06-components/_components.chart.scss +1 -1
  79. package/src/styles/06-components/_components.dropdown.scss +6 -0
  80. package/src/styles/06-components/_components.footer.scss +1 -1
  81. package/src/styles/06-components/_components.list-group.scss +1 -1
  82. package/src/styles/06-components/_components.list.scss +1 -1
  83. package/src/styles/06-components/_components.menu.scss +1 -1
  84. package/src/styles/06-components/_components.messages.scss +1 -1
  85. package/src/styles/06-components/_components.modal.scss +7 -2
  86. package/src/styles/06-components/_components.navbar.scss +1 -1
  87. package/src/styles/06-components/_components.popover.scss +10 -0
  88. package/src/styles/06-components/_components.product-review.scss +1 -1
  89. package/src/styles/06-components/_components.progress.scss +1 -1
  90. package/src/styles/06-components/_components.rating.scss +1 -1
  91. package/src/styles/06-components/_components.spinner.scss +1 -1
  92. package/src/styles/99-utilities/_utilities.background.scss +1 -1
  93. package/src/styles/99-utilities/_utilities.border.scss +1 -1
  94. package/src/styles/99-utilities/_utilities.link.scss +1 -1
  95. package/src/styles/99-utilities/_utilities.text.scss +1 -1
@@ -14,6 +14,7 @@ import type {
14
14
  OverLightObjectConfig,
15
15
  } from '../types/components';
16
16
  import { ATOMIX_GLASS } from '../constants/components';
17
+ import { globalMouseTracker } from './shared-mouse-tracker';
17
18
  import {
18
19
  calculateDistance,
19
20
  calculateElementCenter,
@@ -25,6 +26,125 @@ import {
25
26
 
26
27
  const { CONSTANTS } = ATOMIX_GLASS;
27
28
 
29
+ // Module-level shared background detection cache using WeakMap
30
+ // Automatically cleaned up when elements are removed from DOM
31
+ interface BackgroundDetectionCacheEntry {
32
+ result: boolean;
33
+ timestamp: number;
34
+ config: OverLightConfig;
35
+ threshold: number;
36
+ }
37
+
38
+ const backgroundDetectionCache = new WeakMap<HTMLElement, BackgroundDetectionCacheEntry>();
39
+
40
+ /**
41
+ * Compare two OverLightConfig values for equality
42
+ * Handles primitives (boolean, 'auto') and objects with deep comparison
43
+ */
44
+ const compareOverLightConfig = (
45
+ config1: OverLightConfig,
46
+ config2: OverLightConfig
47
+ ): boolean => {
48
+ // Primitive comparison for boolean and 'auto'
49
+ if (typeof config1 !== 'object' || config1 === null) {
50
+ return config1 === config2;
51
+ }
52
+
53
+ // Both must be objects at this point
54
+ if (typeof config2 !== 'object' || config2 === null) {
55
+ return false;
56
+ }
57
+
58
+ const obj1 = config1 as OverLightObjectConfig;
59
+ const obj2 = config2 as OverLightObjectConfig;
60
+
61
+ // Deep comparison of object properties
62
+ // Compare all defined properties (threshold, opacity, contrast, brightness, saturationBoost)
63
+ const props: (keyof OverLightObjectConfig)[] = [
64
+ 'threshold',
65
+ 'opacity',
66
+ 'contrast',
67
+ 'brightness',
68
+ 'saturationBoost',
69
+ ];
70
+
71
+ for (const prop of props) {
72
+ const val1 = obj1[prop];
73
+ const val2 = obj2[prop];
74
+
75
+ // If both are undefined, they're equal for this property
76
+ if (val1 === undefined && val2 === undefined) {
77
+ continue;
78
+ }
79
+
80
+ // If one is undefined and the other isn't, they're different
81
+ if (val1 === undefined || val2 === undefined) {
82
+ return false;
83
+ }
84
+
85
+ // Compare numeric values (handle NaN and floating point precision)
86
+ if (typeof val1 === 'number' && typeof val2 === 'number') {
87
+ // Use Number.isNaN for proper NaN comparison
88
+ if (Number.isNaN(val1) && Number.isNaN(val2)) {
89
+ continue;
90
+ }
91
+ if (Number.isNaN(val1) || Number.isNaN(val2)) {
92
+ return false;
93
+ }
94
+ // Compare with small epsilon for floating point numbers
95
+ if (Math.abs(val1 - val2) > Number.EPSILON) {
96
+ return false;
97
+ }
98
+ } else if (val1 !== val2) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ return true;
104
+ };
105
+
106
+ /**
107
+ * Get cached background detection result
108
+ */
109
+ const getCachedBackgroundDetection = (
110
+ parentElement: HTMLElement | null,
111
+ overLightConfig: OverLightConfig
112
+ ): boolean | null => {
113
+ if (!parentElement) {
114
+ return null;
115
+ }
116
+
117
+ const cached = backgroundDetectionCache.get(parentElement);
118
+ if (cached && compareOverLightConfig(cached.config, overLightConfig)) {
119
+ // Update timestamp for LRU-like behavior (though WeakMap doesn't support LRU)
120
+ cached.timestamp = Date.now();
121
+ return cached.result;
122
+ }
123
+
124
+ return null;
125
+ };
126
+
127
+ /**
128
+ * Set cached background detection result
129
+ */
130
+ const setCachedBackgroundDetection = (
131
+ parentElement: HTMLElement | null,
132
+ overLightConfig: OverLightConfig,
133
+ result: boolean,
134
+ threshold: number
135
+ ): void => {
136
+ if (!parentElement) {
137
+ return;
138
+ }
139
+
140
+ backgroundDetectionCache.set(parentElement, {
141
+ result,
142
+ timestamp: Date.now(),
143
+ config: overLightConfig,
144
+ threshold,
145
+ });
146
+ };
147
+
28
148
  interface UseAtomixGlassOptions extends Omit<AtomixGlassProps, 'children'> {
29
149
  glassRef: React.RefObject<HTMLDivElement>;
30
150
  contentRef: React.RefObject<HTMLDivElement>;
@@ -44,7 +164,7 @@ interface UseAtomixGlassReturn {
44
164
  detectedOverLight: boolean;
45
165
  globalMousePosition: MousePosition;
46
166
  mouseOffset: MousePosition;
47
-
167
+
48
168
  // OverLight config
49
169
  overLightConfig: {
50
170
  isOverLight: boolean;
@@ -56,12 +176,12 @@ interface UseAtomixGlassReturn {
56
176
  shadowIntensity: number;
57
177
  borderOpacity: number;
58
178
  };
59
-
179
+
60
180
  // Transform calculations
61
181
  elasticTranslation: { x: number; y: number };
62
182
  directionalScale: string;
63
183
  transformStyle: string;
64
-
184
+
65
185
  // Event handlers
66
186
  handleMouseEnter: () => void;
67
187
  handleMouseLeave: () => void;
@@ -108,19 +228,21 @@ export function useAtomixGlass({
108
228
  const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(false);
109
229
  const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
110
230
  const [detectedOverLight, setDetectedOverLight] = useState(false);
231
+
232
+ // Use shared module-level cache (no per-instance cache needed)
111
233
 
112
234
  // Memoized derived values
113
235
  const effectiveCornerRadius = useMemo(() => {
114
236
  if (cornerRadius !== undefined) {
115
237
  const result = Math.max(0, cornerRadius);
116
- if (debugCornerRadius) {
238
+ if (process.env.NODE_ENV !== 'production' && debugCornerRadius) {
117
239
  console.log('[AtomixGlass] Using manual cornerRadius prop:', result);
118
240
  }
119
241
  return result;
120
242
  }
121
243
 
122
244
  const result = Math.max(0, dynamicCornerRadius);
123
- if (debugCornerRadius) {
245
+ if (process.env.NODE_ENV !== 'production' && debugCornerRadius) {
124
246
  console.log('[AtomixGlass] Using dynamic cornerRadius:', result);
125
247
  }
126
248
  return result;
@@ -180,21 +302,21 @@ export function useAtomixGlass({
180
302
  if (extractedRadius !== null && extractedRadius > 0) {
181
303
  setDynamicCornerRadius(extractedRadius);
182
304
 
183
- if (debugCornerRadius) {
305
+ if (process.env.NODE_ENV !== 'production' && debugCornerRadius) {
184
306
  console.log('[AtomixGlass] Corner radius extracted:', {
185
307
  value: extractedRadius,
186
308
  source: extractionSource,
187
309
  timestamp: new Date().toISOString(),
188
310
  });
189
311
  }
190
- } else if (debugCornerRadius) {
312
+ } else if (process.env.NODE_ENV !== 'production' && debugCornerRadius) {
191
313
  console.log(
192
314
  '[AtomixGlass] No corner radius found, using default:',
193
315
  CONSTANTS.DEFAULT_CORNER_RADIUS
194
316
  );
195
317
  }
196
318
  } catch (error) {
197
- if (debugCornerRadius) {
319
+ if (process.env.NODE_ENV !== 'production' && debugCornerRadius) {
198
320
  console.error('[AtomixGlass] Error extracting corner radius:', error);
199
321
  }
200
322
  }
@@ -209,22 +331,31 @@ export function useAtomixGlass({
209
331
  useEffect(() => {
210
332
  // Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
211
333
  const shouldDetect = (overLight === 'auto' || (typeof overLight === 'object' && overLight !== null));
212
-
334
+
213
335
  if (shouldDetect && glassRef.current) {
336
+ const element = glassRef.current;
337
+ const parentElement = element.parentElement;
338
+
339
+ // Check shared cache: skip detection if parent unchanged and config unchanged
340
+ const cachedResult = getCachedBackgroundDetection(parentElement, overLight);
341
+ if (cachedResult !== null) {
342
+ setDetectedOverLight(cachedResult);
343
+ return;
344
+ }
345
+
214
346
  const timeoutId = setTimeout(() => {
215
347
  try {
216
- const element = glassRef.current;
217
348
  if (!element) {
218
349
  setDetectedOverLight(false);
219
350
  return;
220
351
  }
221
-
352
+
222
353
  // Validate window context
223
354
  if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
224
355
  setDetectedOverLight(false);
225
356
  return;
226
357
  }
227
-
358
+
228
359
  let totalLuminance = 0;
229
360
  let validSamples = 0;
230
361
  let hasValidBackground = false;
@@ -233,7 +364,7 @@ export function useAtomixGlass({
233
364
  let depth = 0;
234
365
  const maxDepth = 20;
235
366
  const maxSamples = 10;
236
-
367
+
237
368
  // Limit traversal depth to prevent infinite loops and performance issues
238
369
  while (currentElement && validSamples < maxSamples && depth < maxDepth) {
239
370
  try {
@@ -243,7 +374,7 @@ export function useAtomixGlass({
243
374
  depth++;
244
375
  continue;
245
376
  }
246
-
377
+
247
378
  const bgColor = computedStyle.backgroundColor;
248
379
  const bgImage = computedStyle.backgroundImage;
249
380
 
@@ -254,11 +385,11 @@ export function useAtomixGlass({
254
385
  const r = Number(rgb[0]);
255
386
  const g = Number(rgb[1]);
256
387
  const b = Number(rgb[2]);
257
-
388
+
258
389
  // Validate RGB values are valid numbers
259
- if (!isNaN(r) && !isNaN(g) && !isNaN(b) &&
260
- isFinite(r) && isFinite(g) && isFinite(b) &&
261
- r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
390
+ if (!isNaN(r) && !isNaN(g) && !isNaN(b) &&
391
+ isFinite(r) && isFinite(g) && isFinite(b) &&
392
+ r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
262
393
  // Only consider if it's not pure black or very dark
263
394
  if (r > 10 || g > 10 || b > 10) {
264
395
  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
@@ -271,7 +402,7 @@ export function useAtomixGlass({
271
402
  }
272
403
  }
273
404
  }
274
-
405
+
275
406
  // Check for image backgrounds
276
407
  if (bgImage && bgImage !== 'none' && bgImage !== 'initial') {
277
408
  // For image backgrounds, assume medium luminance
@@ -285,7 +416,7 @@ export function useAtomixGlass({
285
416
  console.debug('AtomixGlass: Error getting computed style for element:', styleError);
286
417
  }
287
418
  }
288
-
419
+
289
420
  // Move to parent element for next iteration
290
421
  if (currentElement) {
291
422
  currentElement = currentElement.parentElement;
@@ -300,25 +431,29 @@ export function useAtomixGlass({
300
431
  const avgLuminance = totalLuminance / validSamples;
301
432
  if (!isNaN(avgLuminance) && isFinite(avgLuminance)) {
302
433
  let threshold = 0.7; // Conservative threshold for overlight
303
-
434
+
304
435
  // If overLight is an object, use its threshold property with validation
305
436
  if (typeof overLight === 'object' && overLight !== null) {
306
437
  const objConfig = overLight as OverLightObjectConfig;
307
438
  if (objConfig.threshold !== undefined) {
308
- const configThreshold = typeof objConfig.threshold === 'number' &&
309
- !isNaN(objConfig.threshold) &&
310
- isFinite(objConfig.threshold)
311
- ? objConfig.threshold
312
- : 0.7;
439
+ const configThreshold = typeof objConfig.threshold === 'number' &&
440
+ !isNaN(objConfig.threshold) &&
441
+ isFinite(objConfig.threshold)
442
+ ? objConfig.threshold
443
+ : 0.7;
313
444
  threshold = Math.min(0.9, Math.max(0.1, configThreshold));
314
445
  }
315
446
  }
316
-
447
+
317
448
  const isOverLightDetected = avgLuminance > threshold;
318
- setDetectedOverLight(isOverLightDetected);
319
449
 
450
+ // Cache the result in shared cache
451
+ setCachedBackgroundDetection(element.parentElement, overLight, isOverLightDetected, threshold);
452
+
453
+ setDetectedOverLight(isOverLightDetected);
454
+
320
455
  // Debug logging
321
- if (debugOverLight) {
456
+ if (process.env.NODE_ENV !== 'production' && debugOverLight) {
322
457
  console.log('[AtomixGlass] OverLight Detection:', {
323
458
  avgLuminance: avgLuminance.toFixed(3),
324
459
  threshold: threshold.toFixed(3),
@@ -331,28 +466,46 @@ export function useAtomixGlass({
331
466
  }
332
467
  } else {
333
468
  // Invalid luminance calculation, default to false
334
- setDetectedOverLight(false);
469
+ const result = false;
470
+ const threshold = typeof overLight === 'object' && overLight !== null
471
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
472
+ : 0.7;
473
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
474
+ setDetectedOverLight(result);
335
475
  }
336
476
  } else {
337
477
  // Default to false if no valid background found
338
- setDetectedOverLight(false);
478
+ const result = false;
479
+ const threshold = typeof overLight === 'object' && overLight !== null
480
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
481
+ : 0.7;
482
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
483
+ setDetectedOverLight(result);
339
484
  }
340
485
  } catch (error) {
341
486
  // Enhanced error logging with context
342
487
  if (process.env.NODE_ENV === 'development') {
343
488
  console.warn('AtomixGlass: Error detecting background brightness:', error);
344
489
  }
345
- setDetectedOverLight(false);
490
+ const result = false;
491
+ if (element && element.parentElement) {
492
+ const threshold = typeof overLight === 'object' && overLight !== null
493
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
494
+ : 0.7;
495
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
496
+ }
497
+ setDetectedOverLight(result);
346
498
  }
347
499
  }, 150);
348
-
500
+
349
501
  return () => clearTimeout(timeoutId);
350
502
  } else if (typeof overLight === 'boolean') {
351
503
  // For boolean values, disable auto-detection
504
+ // Cache is automatically managed by WeakMap (no manual clearing needed)
352
505
  setDetectedOverLight(false);
353
-
506
+
354
507
  // Debug logging for boolean mode
355
- if (debugOverLight) {
508
+ if (process.env.NODE_ENV !== 'production' && debugOverLight) {
356
509
  console.log('[AtomixGlass] OverLight Mode: boolean', {
357
510
  value: overLight,
358
511
  autoDetection: false,
@@ -407,88 +560,121 @@ export function useAtomixGlass({
407
560
  }
408
561
  }, [overLight, glassRef, debugOverLight]);
409
562
 
410
- // Mouse tracking
411
- const mouseMoveThrottleRef = useRef<number | null>(null);
412
- const lastMouseEventRef = useRef<MouseEvent | null>(null);
413
-
414
- const handleMouseMove = useCallback(
415
- (e: MouseEvent) => {
416
- lastMouseEventRef.current = e;
563
+ // Mouse tracking using shared global tracker
564
+ // Cache bounding rect to avoid repeated getBoundingClientRect calls
565
+ const cachedRectRef = useRef<DOMRect | null>(null);
566
+ const updateRectRef = useRef<number | null>(null);
567
+
568
+ // Handle mouse position updates from shared tracker
569
+ const handleGlobalMousePosition = useCallback(
570
+ (globalPos: MousePosition) => {
571
+ if (externalGlobalMousePosition && externalMouseOffset) {
572
+ // External mouse position provided, skip internal tracking
573
+ return;
574
+ }
417
575
 
418
- if (mouseMoveThrottleRef.current === null) {
419
- mouseMoveThrottleRef.current = requestAnimationFrame(() => {
420
- const event = lastMouseEventRef.current;
421
- if (!event) {
422
- mouseMoveThrottleRef.current = null;
423
- return;
424
- }
576
+ if (effectiveDisableEffects) {
577
+ return;
578
+ }
425
579
 
426
- const container = mouseContainer?.current || glassRef.current;
427
- if (!container) {
428
- mouseMoveThrottleRef.current = null;
429
- return;
430
- }
580
+ const container = mouseContainer?.current || glassRef.current;
581
+ if (!container) {
582
+ return;
583
+ }
431
584
 
432
- const startTime = enablePerformanceMonitoring ? performance.now() : 0;
585
+ const startTime = enablePerformanceMonitoring ? performance.now() : 0;
433
586
 
434
- const rect = container.getBoundingClientRect();
435
- if (rect.width === 0 || rect.height === 0) {
436
- mouseMoveThrottleRef.current = null;
437
- return;
438
- }
587
+ // Use cached rect if available, otherwise get new one
588
+ let rect = cachedRectRef.current;
589
+ if (!rect || rect.width === 0 || rect.height === 0) {
590
+ rect = container.getBoundingClientRect();
591
+ cachedRectRef.current = rect;
592
+ }
439
593
 
440
- const center = calculateElementCenter(rect);
594
+ if (rect.width === 0 || rect.height === 0) {
595
+ return;
596
+ }
441
597
 
442
- setInternalMouseOffset({
443
- x: ((event.clientX - center.x) / rect.width) * 100,
444
- y: ((event.clientY - center.y) / rect.height) * 100,
445
- });
598
+ const center = calculateElementCenter(rect);
446
599
 
447
- setInternalGlobalMousePosition({
448
- x: event.clientX,
449
- y: event.clientY,
450
- });
600
+ // Calculate offset relative to this container
601
+ const newOffset = {
602
+ x: ((globalPos.x - center.x) / rect.width) * 100,
603
+ y: ((globalPos.y - center.y) / rect.height) * 100,
604
+ };
451
605
 
452
- if (enablePerformanceMonitoring) {
453
- const endTime = performance.now();
454
- const duration = endTime - startTime;
455
- if (duration > 5) {
456
- console.warn(`AtomixGlass: Mouse tracking took ${duration.toFixed(2)}ms`);
457
- }
458
- }
606
+ // React 18 automatically batches these updates
607
+ setInternalMouseOffset(newOffset);
608
+ setInternalGlobalMousePosition(globalPos);
459
609
 
460
- mouseMoveThrottleRef.current = null;
461
- });
610
+ if (process.env.NODE_ENV !== 'production' && enablePerformanceMonitoring) {
611
+ const endTime = performance.now();
612
+ const duration = endTime - startTime;
613
+ if (duration > 5) {
614
+ console.warn(`AtomixGlass: Mouse tracking took ${duration.toFixed(2)}ms`);
615
+ }
462
616
  }
463
617
  },
464
- [mouseContainer, glassRef, enablePerformanceMonitoring]
618
+ [
619
+ mouseContainer,
620
+ glassRef,
621
+ externalGlobalMousePosition,
622
+ externalMouseOffset,
623
+ effectiveDisableEffects,
624
+ enablePerformanceMonitoring,
625
+ ]
465
626
  );
466
627
 
628
+ // Subscribe to shared mouse tracker
467
629
  useEffect(() => {
468
630
  if (externalGlobalMousePosition && externalMouseOffset) {
631
+ // External mouse position provided, don't subscribe
469
632
  return undefined;
470
633
  }
471
634
 
472
635
  if (effectiveDisableEffects) {
636
+ // Effects disabled, don't subscribe
473
637
  return undefined;
474
638
  }
475
639
 
640
+ // Subscribe to shared tracker
641
+ const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
642
+
643
+ // Update cached rect when container size changes
644
+ const updateRect = () => {
645
+ if (updateRectRef.current !== null) {
646
+ cancelAnimationFrame(updateRectRef.current);
647
+ }
648
+ updateRectRef.current = requestAnimationFrame(() => {
649
+ const container = mouseContainer?.current || glassRef.current;
650
+ if (container) {
651
+ cachedRectRef.current = container.getBoundingClientRect();
652
+ }
653
+ updateRectRef.current = null;
654
+ });
655
+ };
656
+
657
+ // Use ResizeObserver to update cached rect when container size changes
476
658
  const container = mouseContainer?.current || glassRef.current;
477
- if (!container) {
478
- return undefined;
659
+ let resizeObserver: ResizeObserver | null = null;
660
+
661
+ if (container && typeof ResizeObserver !== 'undefined') {
662
+ resizeObserver = new ResizeObserver(updateRect);
663
+ resizeObserver.observe(container);
479
664
  }
480
665
 
481
- container.addEventListener('mousemove', handleMouseMove, { passive: true });
482
-
483
666
  return () => {
484
- container.removeEventListener('mousemove', handleMouseMove);
485
- if (mouseMoveThrottleRef.current) {
486
- cancelAnimationFrame(mouseMoveThrottleRef.current);
487
- mouseMoveThrottleRef.current = null;
667
+ unsubscribe();
668
+ if (updateRectRef.current !== null) {
669
+ cancelAnimationFrame(updateRectRef.current);
670
+ updateRectRef.current = null;
671
+ }
672
+ if (resizeObserver) {
673
+ resizeObserver.disconnect();
488
674
  }
489
675
  };
490
676
  }, [
491
- handleMouseMove,
677
+ handleGlobalMousePosition,
492
678
  mouseContainer,
493
679
  glassRef,
494
680
  externalGlobalMousePosition,
@@ -632,10 +818,10 @@ export function useAtomixGlass({
632
818
  return;
633
819
  }
634
820
 
635
- const cornerRadiusOffset = Math.max(0, Math.min(effectiveCornerRadius * 0.1, 10));
821
+ // Measure actual rendered size without artificial offsets to avoid feedback loops
636
822
  const newSize: GlassSize = {
637
- width: Math.round(rect.width + cornerRadiusOffset),
638
- height: Math.round(rect.height + cornerRadiusOffset),
823
+ width: Math.round(rect.width),
824
+ height: Math.round(rect.height),
639
825
  };
640
826
 
641
827
  const cornerRadiusChanged = lastCornerRadius !== effectiveCornerRadius;
@@ -662,32 +848,29 @@ export function useAtomixGlass({
662
848
  const initialTimeoutId = setTimeout(() => updateGlassSize(true), 0);
663
849
 
664
850
  let resizeObserver: ResizeObserver | null = null;
665
- let fallbackInterval: NodeJS.Timeout | null = null;
666
-
667
- const hasResizeObserver = typeof ResizeObserver !== 'undefined';
851
+ let resizeDebounceTimeout: NodeJS.Timeout | null = null;
668
852
 
669
- if (hasResizeObserver && isValidElement(glassRef.current)) {
853
+ // ResizeObserver has 98%+ browser support, no need for fallback
854
+ if (typeof ResizeObserver !== 'undefined' && isValidElement(glassRef.current)) {
670
855
  try {
671
856
  resizeObserver = new ResizeObserver(entries => {
672
857
  for (const entry of entries) {
673
858
  if (entry.target === glassRef.current) {
674
- updateGlassSize(false);
859
+ // Update cached rect when size changes
860
+ if (glassRef.current) {
861
+ cachedRectRef.current = glassRef.current.getBoundingClientRect();
862
+ }
863
+ // Debounce resize updates to match RAF timing (16ms)
864
+ if (resizeDebounceTimeout) clearTimeout(resizeDebounceTimeout);
865
+ resizeDebounceTimeout = setTimeout(() => updateGlassSize(false), 16);
675
866
  break;
676
867
  }
677
868
  }
678
869
  });
679
870
  resizeObserver.observe(glassRef.current);
680
- } catch {
681
- fallbackInterval = setInterval(
682
- () => isValidElement(glassRef.current) && updateGlassSize(false),
683
- 100
684
- );
871
+ } catch (error) {
872
+ console.warn('AtomixGlass: ResizeObserver not available, using window resize only', error);
685
873
  }
686
- } else {
687
- fallbackInterval = setInterval(
688
- () => isValidElement(glassRef.current) && updateGlassSize(false),
689
- 100
690
- );
691
874
  }
692
875
 
693
876
  window.addEventListener('resize', debouncedResizeHandler, { passive: true });
@@ -696,7 +879,7 @@ export function useAtomixGlass({
696
879
  clearTimeout(initialTimeoutId);
697
880
  if (rafId !== null) cancelAnimationFrame(rafId);
698
881
  if (resizeTimeoutId) clearTimeout(resizeTimeoutId);
699
- if (fallbackInterval) clearInterval(fallbackInterval);
882
+ if (resizeDebounceTimeout) clearTimeout(resizeDebounceTimeout);
700
883
  window.removeEventListener('resize', debouncedResizeHandler);
701
884
  resizeObserver?.disconnect();
702
885
  };
@@ -749,7 +932,7 @@ export function useAtomixGlass({
749
932
 
750
933
  // More robust overlight configuration with better defaults and clamping
751
934
  const baseOpacity = isOverLight ? Math.min(0.6, Math.max(0.2, 0.5 * hoverIntensity * activeIntensity)) : 0;
752
-
935
+
753
936
  const baseConfig = {
754
937
  isOverLight,
755
938
  threshold: 0.7,
@@ -763,14 +946,14 @@ export function useAtomixGlass({
763
946
 
764
947
  if (typeof overLight === 'object' && overLight !== null) {
765
948
  const objConfig = overLight as OverLightObjectConfig;
766
-
949
+
767
950
  // Validate and apply object config values with proper clamping
768
951
  const validatedThreshold = validateConfigValue(objConfig.threshold, 0.1, 1.0, baseConfig.threshold);
769
952
  const validatedOpacity = validateConfigValue(objConfig.opacity, 0.1, 1.0, baseConfig.opacity);
770
953
  const validatedContrast = validateConfigValue(objConfig.contrast, 0.5, 2.5, baseConfig.contrast);
771
954
  const validatedBrightness = validateConfigValue(objConfig.brightness, 0.5, 2.0, baseConfig.brightness);
772
955
  const validatedSaturationBoost = validateConfigValue(objConfig.saturationBoost, 0.5, 3.0, baseConfig.saturationBoost);
773
-
956
+
774
957
  const finalConfig = {
775
958
  ...baseConfig,
776
959
  threshold: validatedThreshold,
@@ -779,9 +962,9 @@ export function useAtomixGlass({
779
962
  brightness: validatedBrightness + mouseInfluence * 0.15,
780
963
  saturationBoost: validatedSaturationBoost + mouseInfluence * 0.4,
781
964
  };
782
-
965
+
783
966
  // Debug logging
784
- if (debugOverLight) {
967
+ if (process.env.NODE_ENV !== 'production' && debugOverLight) {
785
968
  console.log('[AtomixGlass] OverLight Config:', {
786
969
  isOverLight,
787
970
  config: {
@@ -808,12 +991,12 @@ export function useAtomixGlass({
808
991
  timestamp: new Date().toISOString(),
809
992
  });
810
993
  }
811
-
994
+
812
995
  return finalConfig;
813
996
  }
814
997
 
815
998
  // Debug logging for non-object configs
816
- if (debugOverLight) {
999
+ if (process.env.NODE_ENV !== 'production' && debugOverLight) {
817
1000
  console.log('[AtomixGlass] OverLight Config:', {
818
1001
  isOverLight,
819
1002
  configType: typeof overLight === 'boolean' ? (overLight ? 'true' : 'false') : overLight,
@@ -854,6 +1037,12 @@ export function useAtomixGlass({
854
1037
  [onClick]
855
1038
  );
856
1039
 
1040
+ // No-op handler for backward compatibility (mouse tracking now handled by shared tracker)
1041
+ const handleMouseMove = useCallback((_e: MouseEvent) => {
1042
+ // Mouse tracking is now handled by shared global tracker
1043
+ // This handler is kept for backward compatibility with existing code
1044
+ }, []);
1045
+
857
1046
  return {
858
1047
  // State
859
1048
  isHovered,
@@ -885,4 +1074,3 @@ export function useAtomixGlass({
885
1074
  handleKeyDown,
886
1075
  };
887
1076
  }
888
-