@shohojdhara/atomix 0.4.1 → 0.4.2

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 (126) hide show
  1. package/dist/atomix.css +9341 -9249
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +4 -4
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.d.ts +12 -19
  6. package/dist/charts.js +555 -358
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.d.ts +16 -23
  9. package/dist/core.js +418 -262
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.d.ts +11 -18
  12. package/dist/forms.js +411 -257
  13. package/dist/forms.js.map +1 -1
  14. package/dist/heavy.d.ts +14 -21
  15. package/dist/heavy.js +408 -254
  16. package/dist/heavy.js.map +1 -1
  17. package/dist/index.d.ts +33 -40
  18. package/dist/index.esm.js +663 -453
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/index.js +667 -460
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.min.js +1 -1
  23. package/dist/index.min.js.map +1 -1
  24. package/package.json +1 -1
  25. package/scripts/atomix-cli.js +34 -1
  26. package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
  27. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
  28. package/src/components/AtomixGlass/README.md +5 -5
  29. package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
  30. package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
  31. package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
  32. package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
  33. package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
  34. package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
  35. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
  36. package/src/components/Badge/Badge.stories.tsx +1 -1
  37. package/src/components/Badge/Badge.tsx +1 -1
  38. package/src/components/Breadcrumb/Breadcrumb.tsx +90 -76
  39. package/src/components/Breadcrumb/index.ts +2 -2
  40. package/src/components/Button/Button.stories.tsx +1 -1
  41. package/src/components/Button/README.md +2 -2
  42. package/src/components/Callout/Callout.test.tsx +3 -3
  43. package/src/components/Callout/Callout.tsx +2 -2
  44. package/src/components/Callout/README.md +2 -2
  45. package/src/components/Chart/Chart.stories.tsx +1 -1
  46. package/src/components/Chart/Chart.tsx +5 -5
  47. package/src/components/Chart/TreemapChart.tsx +37 -29
  48. package/src/components/DatePicker/readme.md +3 -3
  49. package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
  50. package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
  51. package/src/components/Form/Checkbox.stories.tsx +1 -1
  52. package/src/components/Form/Checkbox.tsx +1 -1
  53. package/src/components/Form/Input.stories.tsx +1 -1
  54. package/src/components/Form/Input.tsx +1 -1
  55. package/src/components/Form/Radio.stories.tsx +1 -1
  56. package/src/components/Form/Radio.tsx +1 -1
  57. package/src/components/Form/Select.stories.tsx +1 -1
  58. package/src/components/Form/Select.tsx +1 -1
  59. package/src/components/Form/Textarea.stories.tsx +1 -1
  60. package/src/components/Form/Textarea.tsx +1 -1
  61. package/src/components/Hero/Hero.stories.tsx +2 -2
  62. package/src/components/Hero/Hero.tsx +2 -2
  63. package/src/components/Messages/Messages.stories.tsx +1 -1
  64. package/src/components/Messages/Messages.tsx +2 -2
  65. package/src/components/Modal/Modal.stories.tsx +1 -1
  66. package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
  67. package/src/components/Navigation/Nav/Nav.tsx +1 -1
  68. package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
  69. package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
  70. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
  71. package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
  72. package/src/components/Pagination/Pagination.stories.tsx +1 -1
  73. package/src/components/Pagination/Pagination.tsx +1 -1
  74. package/src/components/Popover/Popover.stories.tsx +1 -1
  75. package/src/components/Popover/Popover.tsx +1 -1
  76. package/src/components/Progress/Progress.tsx +1 -1
  77. package/src/components/Rating/Rating.stories.tsx +1 -1
  78. package/src/components/Rating/Rating.test.tsx +73 -0
  79. package/src/components/Rating/Rating.tsx +25 -37
  80. package/src/components/Spinner/Spinner.tsx +1 -1
  81. package/src/components/Steps/Steps.stories.tsx +1 -1
  82. package/src/components/Steps/Steps.tsx +2 -2
  83. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  84. package/src/components/Tabs/Tabs.tsx +1 -1
  85. package/src/components/Todo/Todo.tsx +0 -1
  86. package/src/components/Toggle/Toggle.stories.tsx +1 -1
  87. package/src/components/Toggle/Toggle.tsx +1 -1
  88. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  89. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
  90. package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
  91. package/src/lib/composables/__tests__/useChart.test.ts +50 -0
  92. package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
  93. package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
  94. package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
  95. package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
  96. package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
  97. package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
  98. package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
  99. package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
  100. package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
  101. package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
  102. package/src/lib/composables/glass-styles.ts +302 -0
  103. package/src/lib/composables/index.ts +0 -4
  104. package/src/lib/composables/useAtomixGlass.ts +331 -522
  105. package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
  106. package/src/lib/composables/useBarChart.ts +1 -1
  107. package/src/lib/composables/useBreadcrumb.ts +6 -6
  108. package/src/lib/composables/useChart.ts +104 -21
  109. package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
  110. package/src/lib/composables/useSlider.ts +66 -34
  111. package/src/lib/theme/devtools/CLI.ts +1 -1
  112. package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
  113. package/src/lib/types/components.ts +13 -21
  114. package/src/lib/utils/__tests__/dom.test.ts +100 -0
  115. package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
  116. package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
  117. package/src/styles/02-tools/_tools.utility-api.scss +6 -6
  118. package/src/styles/06-components/_components.accordion.scss +0 -2
  119. package/src/styles/06-components/_components.chart.scss +0 -1
  120. package/src/styles/06-components/_components.dropdown.scss +0 -1
  121. package/src/styles/06-components/_components.edge-panel.scss +0 -2
  122. package/src/styles/06-components/_components.photoviewer.scss +0 -1
  123. package/src/styles/06-components/_components.river.scss +0 -1
  124. package/src/styles/06-components/_components.slider.scss +0 -3
  125. package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
  126. package/src/styles/99-utilities/_utilities.text.scss +1 -0
@@ -0,0 +1,329 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { OverLightConfig, OverLightObjectConfig } from '../../types/components';
3
+
4
+ // Module-level shared background detection cache using WeakMap
5
+ // Automatically cleaned up when elements are removed from DOM
6
+ interface BackgroundDetectionCacheEntry {
7
+ result: boolean;
8
+ timestamp: number;
9
+ config: OverLightConfig;
10
+ threshold: number;
11
+ }
12
+
13
+ const backgroundDetectionCache = new WeakMap<HTMLElement, BackgroundDetectionCacheEntry>();
14
+
15
+ /**
16
+ * Compare two OverLightConfig values for equality
17
+ * Handles primitives (boolean, 'auto') and objects with deep comparison
18
+ */
19
+ export const compareOverLightConfig = (
20
+ config1: OverLightConfig,
21
+ config2: OverLightConfig
22
+ ): boolean => {
23
+ // Primitive comparison for boolean and 'auto'
24
+ if (typeof config1 !== 'object' || config1 === null) {
25
+ return config1 === config2;
26
+ }
27
+
28
+ // Both must be objects at this point
29
+ if (typeof config2 !== 'object' || config2 === null) {
30
+ return false;
31
+ }
32
+
33
+ const obj1 = config1 as OverLightObjectConfig;
34
+ const obj2 = config2 as OverLightObjectConfig;
35
+
36
+ // Deep comparison of object properties
37
+ // Compare all defined properties (threshold, opacity, contrast, brightness, saturationBoost)
38
+ const props: (keyof OverLightObjectConfig)[] = [
39
+ 'threshold',
40
+ 'opacity',
41
+ 'contrast',
42
+ 'brightness',
43
+ 'saturationBoost',
44
+ ];
45
+
46
+ for (const prop of props) {
47
+ const val1 = obj1[prop];
48
+ const val2 = obj2[prop];
49
+
50
+ // If both are undefined, they're equal for this property
51
+ if (val1 === undefined && val2 === undefined) {
52
+ continue;
53
+ }
54
+
55
+ // If one is undefined and the other isn't, they're different
56
+ if (val1 === undefined || val2 === undefined) {
57
+ return false;
58
+ }
59
+
60
+ // Compare numeric values (handle NaN and floating point precision)
61
+ if (typeof val1 === 'number' && typeof val2 === 'number') {
62
+ // Use Number.isNaN for proper NaN comparison
63
+ if (Number.isNaN(val1) && Number.isNaN(val2)) {
64
+ continue;
65
+ }
66
+ if (Number.isNaN(val1) || Number.isNaN(val2)) {
67
+ return false;
68
+ }
69
+ // Compare with small epsilon for floating point numbers
70
+ if (Math.abs(val1 - val2) > Number.EPSILON) {
71
+ return false;
72
+ }
73
+ } else if (val1 !== val2) {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ return true;
79
+ };
80
+
81
+ /**
82
+ * Get cached background detection result
83
+ */
84
+ const getCachedBackgroundDetection = (
85
+ parentElement: HTMLElement | null,
86
+ overLightConfig: OverLightConfig
87
+ ): boolean | null => {
88
+ if (!parentElement) {
89
+ return null;
90
+ }
91
+
92
+ const cached = backgroundDetectionCache.get(parentElement);
93
+ if (cached && compareOverLightConfig(cached.config, overLightConfig)) {
94
+ // Update timestamp for LRU-like behavior (though WeakMap doesn't support LRU)
95
+ cached.timestamp = Date.now();
96
+ return cached.result;
97
+ }
98
+
99
+ return null;
100
+ };
101
+
102
+ /**
103
+ * Set cached background detection result
104
+ */
105
+ const setCachedBackgroundDetection = (
106
+ parentElement: HTMLElement | null,
107
+ overLightConfig: OverLightConfig,
108
+ result: boolean,
109
+ threshold: number
110
+ ): void => {
111
+ if (!parentElement) {
112
+ return;
113
+ }
114
+
115
+ backgroundDetectionCache.set(parentElement, {
116
+ result,
117
+ timestamp: Date.now(),
118
+ config: overLightConfig,
119
+ threshold,
120
+ });
121
+ };
122
+
123
+ interface UseGlassBackgroundDetectionProps {
124
+ glassRef: React.RefObject<HTMLDivElement>;
125
+ overLight: OverLightConfig;
126
+ debugOverLight?: boolean;
127
+ }
128
+
129
+ export function useGlassBackgroundDetection({
130
+ glassRef,
131
+ overLight,
132
+ debugOverLight = false,
133
+ }: UseGlassBackgroundDetectionProps) {
134
+ const [detectedOverLight, setDetectedOverLight] = useState(false);
135
+
136
+ useEffect(() => {
137
+ // Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
138
+ const shouldDetect =
139
+ overLight === 'auto' || (typeof overLight === 'object' && overLight !== null);
140
+
141
+ if (shouldDetect && glassRef.current) {
142
+ const element = glassRef.current;
143
+ const parentElement = element.parentElement;
144
+
145
+ // Check shared cache: skip detection if parent unchanged and config unchanged
146
+ const cachedResult = getCachedBackgroundDetection(parentElement, overLight);
147
+ if (cachedResult !== null) {
148
+ setDetectedOverLight(cachedResult);
149
+ return;
150
+ }
151
+
152
+ const timeoutId = setTimeout(() => {
153
+ try {
154
+ if (!element) {
155
+ setDetectedOverLight(false);
156
+ return;
157
+ }
158
+
159
+ // Validate window context
160
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
161
+ setDetectedOverLight(false);
162
+ return;
163
+ }
164
+
165
+ let totalLuminance = 0;
166
+ let validSamples = 0;
167
+ let hasValidBackground = false;
168
+
169
+ let currentElement = element.parentElement;
170
+ let depth = 0;
171
+ const maxDepth = 20;
172
+ const maxSamples = 10;
173
+
174
+ // Limit traversal depth to prevent infinite loops and performance issues
175
+ while (currentElement && validSamples < maxSamples && depth < maxDepth) {
176
+ try {
177
+ const computedStyle = window.getComputedStyle(currentElement);
178
+ if (!computedStyle) {
179
+ currentElement = currentElement.parentElement;
180
+ depth++;
181
+ continue;
182
+ }
183
+
184
+ const bgColor = computedStyle.backgroundColor;
185
+ const bgImage = computedStyle.backgroundImage;
186
+
187
+ // Check for solid color backgrounds
188
+ if (
189
+ bgColor &&
190
+ bgColor !== 'rgba(0, 0, 0, 0)' &&
191
+ bgColor !== 'transparent' &&
192
+ bgColor !== 'initial' &&
193
+ bgColor !== 'none'
194
+ ) {
195
+ const rgb = bgColor.match(/\d+/g);
196
+ if (rgb && rgb.length >= 3) {
197
+ const r = Number(rgb[0]);
198
+ const g = Number(rgb[1]);
199
+ const b = Number(rgb[2]);
200
+
201
+ // Validate RGB values are valid numbers
202
+ if (
203
+ !isNaN(r) &&
204
+ !isNaN(g) &&
205
+ !isNaN(b) &&
206
+ isFinite(r) &&
207
+ isFinite(g) &&
208
+ isFinite(b) &&
209
+ r >= 0 &&
210
+ r <= 255 &&
211
+ g >= 0 &&
212
+ g <= 255 &&
213
+ b >= 0 &&
214
+ b <= 255
215
+ ) {
216
+ // Only consider if it's not pure black or very dark
217
+ if (r > 10 || g > 10 || b > 10) {
218
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
219
+ if (!isNaN(luminance) && isFinite(luminance)) {
220
+ totalLuminance += luminance;
221
+ validSamples++;
222
+ hasValidBackground = true;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ // Check for image backgrounds
230
+ if (bgImage && bgImage !== 'none' && bgImage !== 'initial') {
231
+ // For image backgrounds, assume medium luminance
232
+ totalLuminance += 0.5;
233
+ validSamples++;
234
+ hasValidBackground = true;
235
+ }
236
+ } catch (styleError) {
237
+ // Silently continue if getting computed style fails for this element
238
+ if (typeof process === 'undefined' || process.env?.NODE_ENV === 'development') {
239
+ console.debug('AtomixGlass: Error getting computed style for element:', styleError);
240
+ }
241
+ }
242
+
243
+ // Move to parent element for next iteration
244
+ if (currentElement) {
245
+ currentElement = currentElement.parentElement;
246
+ depth++;
247
+ } else {
248
+ break; // Exit loop if currentElement becomes null
249
+ }
250
+ }
251
+
252
+ // More conservative detection with better error handling
253
+ if (hasValidBackground && validSamples > 0) {
254
+ const avgLuminance = totalLuminance / validSamples;
255
+ if (!isNaN(avgLuminance) && isFinite(avgLuminance)) {
256
+ let threshold = 0.7; // Conservative threshold for overlight
257
+
258
+ // If overLight is an object, use its threshold property with validation
259
+ if (typeof overLight === 'object' && overLight !== null) {
260
+ const objConfig = overLight as OverLightObjectConfig;
261
+ if (objConfig.threshold !== undefined) {
262
+ const configThreshold =
263
+ typeof objConfig.threshold === 'number' &&
264
+ !isNaN(objConfig.threshold) &&
265
+ isFinite(objConfig.threshold)
266
+ ? objConfig.threshold
267
+ : 0.7;
268
+ threshold = Math.min(0.9, Math.max(0.1, configThreshold));
269
+ }
270
+ }
271
+
272
+ const isOverLightDetected = avgLuminance > threshold;
273
+
274
+ // Cache the result in shared cache
275
+ setCachedBackgroundDetection(
276
+ element.parentElement,
277
+ overLight,
278
+ isOverLightDetected,
279
+ threshold
280
+ );
281
+
282
+ setDetectedOverLight(isOverLightDetected);
283
+ } else {
284
+ // Invalid luminance calculation, default to false
285
+ const result = false;
286
+ const threshold =
287
+ typeof overLight === 'object' && overLight !== null
288
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
289
+ : 0.7;
290
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
291
+ setDetectedOverLight(result);
292
+ }
293
+ } else {
294
+ // Default to false if no valid background found
295
+ const result = false;
296
+ const threshold =
297
+ typeof overLight === 'object' && overLight !== null
298
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
299
+ : 0.7;
300
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
301
+ setDetectedOverLight(result);
302
+ }
303
+ } catch (error) {
304
+ // Enhanced error logging with context
305
+ if (typeof process === 'undefined' || process.env?.NODE_ENV === 'development') {
306
+ console.warn('AtomixGlass: Error detecting background brightness:', error);
307
+ }
308
+ const result = false;
309
+ if (element && element.parentElement) {
310
+ const threshold =
311
+ typeof overLight === 'object' && overLight !== null
312
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
313
+ : 0.7;
314
+ setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
315
+ }
316
+ setDetectedOverLight(result);
317
+ }
318
+ }, 150);
319
+
320
+ return () => clearTimeout(timeoutId);
321
+ } else if (typeof overLight === 'boolean') {
322
+ // For boolean values, disable auto-detection
323
+ setDetectedOverLight(false);
324
+ }
325
+ return undefined;
326
+ }, [overLight, glassRef, debugOverLight]);
327
+
328
+ return { detectedOverLight };
329
+ }
@@ -0,0 +1,82 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { ATOMIX_GLASS } from '../../constants/components';
3
+ import {
4
+ extractBorderRadiusFromChildren,
5
+ extractBorderRadiusFromDOMElement,
6
+ } from '../../../components/AtomixGlass/glass-utils';
7
+
8
+ const { CONSTANTS } = ATOMIX_GLASS;
9
+
10
+ interface UseGlassCornerRadiusProps {
11
+ contentRef: React.RefObject<HTMLDivElement>;
12
+ borderRadius?: number;
13
+ children?: React.ReactNode;
14
+ debugBorderRadius?: boolean;
15
+ }
16
+
17
+ export function useGlassCornerRadius({
18
+ contentRef,
19
+ borderRadius,
20
+ children,
21
+ debugBorderRadius = false,
22
+ }: UseGlassCornerRadiusProps) {
23
+ const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
24
+ CONSTANTS.DEFAULT_CORNER_RADIUS
25
+ );
26
+
27
+ const effectiveBorderRadius = useMemo(() => {
28
+ if (borderRadius !== undefined) {
29
+ const result = Math.max(0, borderRadius);
30
+ return result;
31
+ }
32
+
33
+ const result = Math.max(0, dynamicBorderRadius);
34
+ return result;
35
+ }, [borderRadius, dynamicBorderRadius]);
36
+
37
+ // Extract border-radius from children
38
+ useEffect(() => {
39
+ const extractRadius = () => {
40
+ try {
41
+ let extractedRadius: number | null = null;
42
+
43
+ if (contentRef.current) {
44
+ const firstChild = contentRef.current.firstElementChild as HTMLElement;
45
+ if (firstChild) {
46
+ const domRadius = extractBorderRadiusFromDOMElement(firstChild);
47
+ if (domRadius !== null && domRadius > 0) {
48
+ extractedRadius = domRadius;
49
+ }
50
+ }
51
+ }
52
+
53
+ if (extractedRadius === null) {
54
+ const childRadius = extractBorderRadiusFromChildren(children);
55
+ if (childRadius > 0 && childRadius !== CONSTANTS.DEFAULT_CORNER_RADIUS) {
56
+ extractedRadius = childRadius;
57
+ }
58
+ }
59
+
60
+ if (extractedRadius !== null && extractedRadius > 0) {
61
+ setDynamicCornerRadius(extractedRadius);
62
+ }
63
+ } catch (error) {
64
+ if (
65
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
66
+ debugBorderRadius
67
+ ) {
68
+ console.error('[AtomixGlass] Error extracting corner radius:', error);
69
+ }
70
+ }
71
+ };
72
+
73
+ extractRadius();
74
+ const timeoutId = setTimeout(extractRadius, 100);
75
+ return () => clearTimeout(timeoutId);
76
+ }, [children, debugBorderRadius, contentRef]);
77
+
78
+ return {
79
+ dynamicBorderRadius,
80
+ effectiveBorderRadius,
81
+ };
82
+ }
@@ -0,0 +1,153 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { globalMouseTracker } from '../shared-mouse-tracker';
3
+ import { calculateElementCenter } from '../../../components/AtomixGlass/glass-utils';
4
+ import type { MousePosition } from '../../types/components';
5
+
6
+ interface UseGlassMouseTrackingProps {
7
+ glassRef: React.RefObject<HTMLDivElement>;
8
+ mouseContainer?: React.RefObject<HTMLElement | null> | null;
9
+ externalGlobalMousePosition?: MousePosition;
10
+ externalMouseOffset?: MousePosition;
11
+ effectiveWithoutEffects?: boolean;
12
+ }
13
+
14
+ export function useGlassMouseTracking({
15
+ glassRef,
16
+ mouseContainer,
17
+ externalGlobalMousePosition,
18
+ externalMouseOffset,
19
+ effectiveWithoutEffects = false,
20
+ }: UseGlassMouseTrackingProps) {
21
+ const [internalGlobalMousePosition, setInternalGlobalMousePosition] = useState<MousePosition>({
22
+ x: 0,
23
+ y: 0,
24
+ });
25
+ const [internalMouseOffset, setInternalMouseOffset] = useState<MousePosition>({ x: 0, y: 0 });
26
+
27
+ // Mouse tracking using shared global tracker
28
+ // Cache bounding rect to avoid repeated getBoundingClientRect calls
29
+ const cachedRectRef = useRef<DOMRect | null>(null);
30
+ const updateRectRef = useRef<number | null>(null);
31
+
32
+ const globalMousePosition = useMemo(
33
+ () => externalGlobalMousePosition || internalGlobalMousePosition,
34
+ [externalGlobalMousePosition, internalGlobalMousePosition]
35
+ );
36
+
37
+ const mouseOffset = useMemo(
38
+ () => externalMouseOffset || internalMouseOffset,
39
+ [externalMouseOffset, internalMouseOffset]
40
+ );
41
+
42
+ // Handle mouse position updates from shared tracker
43
+ const handleGlobalMousePosition = useCallback(
44
+ (globalPos: MousePosition) => {
45
+ if (externalGlobalMousePosition && externalMouseOffset) {
46
+ // External mouse position provided, skip internal tracking
47
+ return;
48
+ }
49
+
50
+ if (effectiveWithoutEffects) {
51
+ return;
52
+ }
53
+
54
+ const container = mouseContainer?.current || glassRef.current;
55
+ if (!container) {
56
+ return;
57
+ }
58
+
59
+ // Use cached rect if available, otherwise get new one
60
+ let rect = cachedRectRef.current;
61
+ if (!rect || rect.width === 0 || rect.height === 0) {
62
+ rect = container.getBoundingClientRect();
63
+ cachedRectRef.current = rect;
64
+ }
65
+
66
+ if (rect.width === 0 || rect.height === 0) {
67
+ return;
68
+ }
69
+
70
+ const center = calculateElementCenter(rect);
71
+
72
+ // Calculate offset relative to this container
73
+ const newOffset = {
74
+ x: ((globalPos.x - center.x) / rect.width) * 100,
75
+ y: ((globalPos.y - center.y) / rect.height) * 100,
76
+ };
77
+
78
+ // React 18 automatically batches these updates
79
+ setInternalMouseOffset(newOffset);
80
+ setInternalGlobalMousePosition(globalPos);
81
+ },
82
+ [
83
+ mouseContainer,
84
+ glassRef,
85
+ externalGlobalMousePosition,
86
+ externalMouseOffset,
87
+ effectiveWithoutEffects,
88
+ ]
89
+ );
90
+
91
+ // Subscribe to shared mouse tracker
92
+ useEffect(() => {
93
+ if (externalGlobalMousePosition && externalMouseOffset) {
94
+ // External mouse position provided, don't subscribe
95
+ return undefined;
96
+ }
97
+
98
+ if (effectiveWithoutEffects) {
99
+ // Effects disabled, don't subscribe
100
+ return undefined;
101
+ }
102
+
103
+ // Subscribe to shared tracker
104
+ const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
105
+
106
+ // Update cached rect when container size changes
107
+ const updateRect = () => {
108
+ if (updateRectRef.current !== null) {
109
+ cancelAnimationFrame(updateRectRef.current);
110
+ }
111
+ updateRectRef.current = requestAnimationFrame(() => {
112
+ const container = mouseContainer?.current || glassRef.current;
113
+ if (container) {
114
+ cachedRectRef.current = container.getBoundingClientRect();
115
+ }
116
+ updateRectRef.current = null;
117
+ });
118
+ };
119
+
120
+ // Use ResizeObserver to update cached rect when container size changes
121
+ const container = mouseContainer?.current || glassRef.current;
122
+ let resizeObserver: ResizeObserver | null = null;
123
+
124
+ if (container && typeof ResizeObserver !== 'undefined') {
125
+ resizeObserver = new ResizeObserver(updateRect);
126
+ resizeObserver.observe(container);
127
+ }
128
+
129
+ return () => {
130
+ unsubscribe();
131
+ if (updateRectRef.current !== null) {
132
+ cancelAnimationFrame(updateRectRef.current);
133
+ updateRectRef.current = null;
134
+ }
135
+ if (resizeObserver) {
136
+ resizeObserver.disconnect();
137
+ }
138
+ };
139
+ }, [
140
+ handleGlobalMousePosition,
141
+ mouseContainer,
142
+ glassRef,
143
+ externalGlobalMousePosition,
144
+ externalMouseOffset,
145
+ effectiveWithoutEffects,
146
+ ]);
147
+
148
+ return {
149
+ globalMousePosition,
150
+ mouseOffset,
151
+ cachedRectRef,
152
+ };
153
+ }