@shohojdhara/atomix 0.6.1 → 0.6.3

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 (67) hide show
  1. package/README.md +510 -106
  2. package/dist/atomix.css +30 -24
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +6 -6
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/atomix.umd.js +1 -1
  7. package/dist/atomix.umd.js.map +1 -1
  8. package/dist/atomix.umd.min.js +1 -1
  9. package/dist/charts.d.ts +11 -2
  10. package/dist/charts.js +294 -139
  11. package/dist/charts.js.map +1 -1
  12. package/dist/core.d.ts +14 -39
  13. package/dist/core.js +297 -145
  14. package/dist/core.js.map +1 -1
  15. package/dist/forms.d.ts +11 -1
  16. package/dist/forms.js +385 -185
  17. package/dist/forms.js.map +1 -1
  18. package/dist/heavy.d.ts +9 -0
  19. package/dist/heavy.js +297 -143
  20. package/dist/heavy.js.map +1 -1
  21. package/dist/index.d.ts +156 -164
  22. package/dist/index.esm.js +391 -203
  23. package/dist/index.esm.js.map +1 -1
  24. package/dist/index.js +391 -203
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.min.js +1 -1
  27. package/dist/index.min.js.map +1 -1
  28. package/dist/theme.d.ts +14 -6
  29. package/dist/theme.js +2 -9
  30. package/dist/theme.js.map +1 -1
  31. package/package.json +26 -26
  32. package/src/components/AtomixGlass/AtomixGlass.tsx +1 -1
  33. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +8 -1
  34. package/src/components/AtomixGlass/deprecated/AtomixGlass.deprecated.tsx +390 -0
  35. package/src/components/AtomixGlass/glass-utils.ts +29 -0
  36. package/src/components/AtomixGlass/stories/Playground.stories.tsx +32 -1
  37. package/src/components/Button/Button.stories.tsx +1 -1
  38. package/src/components/Button/Button.tsx +6 -5
  39. package/src/components/Card/Card.tsx +2 -2
  40. package/src/components/Dropdown/Dropdown.tsx +1 -0
  41. package/src/components/EdgePanel/EdgePanel.tsx +1 -3
  42. package/src/components/Form/Select.test.tsx +75 -0
  43. package/src/components/Form/Select.tsx +348 -252
  44. package/src/components/Form/SelectOption.tsx +16 -10
  45. package/src/components/index.ts +1 -1
  46. package/src/layouts/CssGrid/index.ts +1 -0
  47. package/src/lib/composables/shared-mouse-tracker.ts +62 -6
  48. package/src/lib/composables/useAtomixGlass.ts +241 -139
  49. package/src/lib/composables/useAtomixGlassStyles.ts +201 -149
  50. package/src/lib/constants/components.ts +54 -35
  51. package/src/lib/theme/config/configLoader.ts +1 -1
  52. package/src/lib/theme/test/testTheme.ts +2 -2
  53. package/src/lib/theme/utils/themeUtils.ts +98 -110
  54. package/src/lib/types/components.ts +29 -65
  55. package/src/styles/01-settings/_settings.spacing.scss +6 -1
  56. package/src/styles/03-generic/_generic.reset.scss +1 -1
  57. package/src/styles/06-components/_components.atomix-glass.scss +20 -29
  58. package/src/styles/06-components/_components.data-table.scss +5 -4
  59. package/src/styles/06-components/_components.dynamic-background.scss +9 -8
  60. package/src/styles/06-components/_components.footer.scss +8 -7
  61. package/src/styles/06-components/_components.hero.scss +2 -2
  62. package/src/styles/06-components/_components.messages.scss +16 -16
  63. package/src/styles/06-components/_components.navbar.scss +2 -0
  64. package/src/styles/06-components/_components.select.scss +15 -2
  65. package/src/styles/06-components/_components.upload.scss +3 -3
  66. package/CHANGELOG.md +0 -165
  67. package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +0 -215
@@ -1,10 +1,4 @@
1
- import React, {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
2
  import type {
9
3
  AtomixGlassProps,
10
4
  GlassSize,
@@ -21,6 +15,9 @@ import {
21
15
  extractBorderRadiusFromDOMElement,
22
16
  validateGlassSize,
23
17
  lerp,
18
+ calculateSpring,
19
+ calculateVelocity,
20
+ smoothstep,
24
21
  } from '../../components/AtomixGlass/glass-utils';
25
22
  import { updateAtomixGlassStyles } from './useAtomixGlassStyles';
26
23
  // Phase 1: Time-Based Animation System
@@ -48,10 +45,7 @@ const backgroundDetectionCache = new WeakMap<HTMLElement, BackgroundDetectionCac
48
45
  * Compare two OverLightConfig values for equality
49
46
  * Handles primitives (boolean, 'auto') and objects with deep comparison
50
47
  */
51
- const compareOverLightConfig = (
52
- config1: OverLightConfig,
53
- config2: OverLightConfig
54
- ): boolean => {
48
+ const compareOverLightConfig = (config1: OverLightConfig, config2: OverLightConfig): boolean => {
55
49
  // Primitive comparison for boolean and 'auto'
56
50
  if (typeof config1 !== 'object' || config1 === null) {
57
51
  return config1 === config2;
@@ -158,6 +152,7 @@ interface UseAtomixGlassOptions extends Omit<AtomixGlassProps, 'children'> {
158
152
  wrapperRef?: React.RefObject<HTMLDivElement | null>;
159
153
  children?: React.ReactNode;
160
154
  isFixedOrSticky?: boolean;
155
+ priority?: number; // Priority for z-index ordering
161
156
  // Phase 1: Time-Based Animation System
162
157
  withLiquidBlur?: boolean;
163
158
  animationQuality?: 'low' | 'medium' | 'high';
@@ -226,7 +221,7 @@ export function useAtomixGlass({
226
221
  reducedMotion = false,
227
222
  highContrast = false,
228
223
  withoutEffects = false,
229
- elasticity = 0.05,
224
+ elasticity = ATOMIX_GLASS.DEFAULTS.ELASTICITY,
230
225
  onClick,
231
226
  debugBorderRadius = false,
232
227
  debugOverLight = false,
@@ -236,6 +231,7 @@ export function useAtomixGlass({
236
231
  padding,
237
232
  withLiquidBlur,
238
233
  isFixedOrSticky = false,
234
+ priority = 1, // Default priority
239
235
  // Phase 1: Animation System Props
240
236
  withTimeAnimation = ATOMIX_GLASS.DEFAULTS.WITH_TIME_ANIMATION,
241
237
  animationSpeed = ATOMIX_GLASS.DEFAULTS.ANIMATION_SPEED,
@@ -265,6 +261,15 @@ export function useAtomixGlass({
265
261
  const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
266
262
  CONSTANTS.DEFAULT_CORNER_RADIUS
267
263
  );
264
+
265
+ // ── Physics state refs ────────────────────────────────────────────────
266
+ const elasticTranslationRef = useRef<MousePosition>({ x: 0, y: 0 });
267
+ const elasticVelocityRef = useRef<MousePosition>({ x: 0, y: 0 });
268
+ const directionalScaleRef = useRef<{ x: number; y: number }>({ x: 1, y: 1 });
269
+ const scaleVelocityRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
270
+ const lastMouseTimeRef = useRef<number>(0);
271
+ const mouseVelocityRef = useRef<MousePosition>({ x: 0, y: 0 });
272
+
268
273
  const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(false);
269
274
  const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
270
275
  const [detectedOverLight, setDetectedOverLight] = useState(false);
@@ -285,7 +290,7 @@ export function useAtomixGlass({
285
290
  const fbmConfig = useMemo(() => {
286
291
  // If quality preset is provided, use it as base
287
292
  const preset = getFBMConfigForQuality(distortionQuality);
288
-
293
+
289
294
  // Override with custom values if provided
290
295
  return {
291
296
  octaves: distortionOctaves ?? preset.octaves,
@@ -371,7 +376,7 @@ export function useAtomixGlass({
371
376
  }
372
377
 
373
378
  const time = shaderTimeRef.current;
374
-
379
+
375
380
  // Apply liquid glass distortion with time
376
381
  return liquidGlassWithTime(uv, time, fbmConfig);
377
382
  },
@@ -388,13 +393,12 @@ export function useAtomixGlass({
388
393
  return result;
389
394
  }, [borderRadius, dynamicBorderRadius]);
390
395
 
391
- const { glassSize } = useGlassSize({
392
- glassRef,
393
- effectiveBorderRadius,
394
- cachedRectRef
396
+ const { glassSize } = useGlassSize({
397
+ glassRef,
398
+ effectiveBorderRadius,
399
+ cachedRectRef,
395
400
  });
396
401
 
397
-
398
402
  const effectiveHighContrast = useMemo(
399
403
  () => highContrast || userPrefersHighContrast,
400
404
  [highContrast, userPrefersHighContrast]
@@ -436,7 +440,10 @@ export function useAtomixGlass({
436
440
  setDynamicCornerRadius(extractedRadius);
437
441
  }
438
442
  } catch (error) {
439
- if ((typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') && debugBorderRadius) {
443
+ if (
444
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
445
+ debugBorderRadius
446
+ ) {
440
447
  console.error('[AtomixGlass] Error extracting corner radius:', error);
441
448
  }
442
449
  }
@@ -479,7 +486,8 @@ export function useAtomixGlass({
479
486
  // Background detection for overLight auto-detect
480
487
  useEffect(() => {
481
488
  // Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
482
- const shouldDetect = (overLight === 'auto' || (typeof overLight === 'object' && overLight !== null));
489
+ const shouldDetect =
490
+ overLight === 'auto' || (typeof overLight === 'object' && overLight !== null);
483
491
 
484
492
  if (shouldDetect && glassRef.current) {
485
493
  const element = glassRef.current;
@@ -528,7 +536,13 @@ export function useAtomixGlass({
528
536
  const bgImage = computedStyle.backgroundImage;
529
537
 
530
538
  // Check for solid color backgrounds
531
- if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' && bgColor !== 'initial' && bgColor !== 'none') {
539
+ if (
540
+ bgColor &&
541
+ bgColor !== 'rgba(0, 0, 0, 0)' &&
542
+ bgColor !== 'transparent' &&
543
+ bgColor !== 'initial' &&
544
+ bgColor !== 'none'
545
+ ) {
532
546
  const rgb = bgColor.match(/\d+/g);
533
547
  if (rgb && rgb.length >= 3) {
534
548
  const r = Number(rgb[0]);
@@ -554,7 +568,7 @@ export function useAtomixGlass({
554
568
  hasValidBackground = true;
555
569
  }
556
570
  } catch (styleError) {
557
- // Silently continue
571
+ // Silently continue
558
572
  }
559
573
 
560
574
  if (currentElement) {
@@ -573,27 +587,37 @@ export function useAtomixGlass({
573
587
  if (typeof overLight === 'object' && overLight !== null) {
574
588
  const objConfig = overLight as OverLightObjectConfig;
575
589
  if (objConfig.threshold !== undefined) {
576
- const configThreshold = typeof objConfig.threshold === 'number' && !isNaN(objConfig.threshold) ? objConfig.threshold : 0.7;
590
+ const configThreshold =
591
+ typeof objConfig.threshold === 'number' && !isNaN(objConfig.threshold)
592
+ ? objConfig.threshold
593
+ : 0.7;
577
594
  threshold = Math.min(0.9, Math.max(0.1, configThreshold));
578
595
  }
579
596
  }
580
597
 
581
598
  const isOverLightDetected = avgLuminance > threshold;
582
- setCachedBackgroundDetection(element.parentElement, overLight, isOverLightDetected, threshold);
599
+ setCachedBackgroundDetection(
600
+ element.parentElement,
601
+ overLight,
602
+ isOverLightDetected,
603
+ threshold
604
+ );
583
605
  setDetectedOverLight(isOverLightDetected);
584
606
  } else {
585
607
  const result = false;
586
- const threshold = typeof overLight === 'object' && overLight !== null
587
- ? (overLight as OverLightObjectConfig).threshold || 0.7
588
- : 0.7;
608
+ const threshold =
609
+ typeof overLight === 'object' && overLight !== null
610
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
611
+ : 0.7;
589
612
  setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
590
613
  setDetectedOverLight(result);
591
614
  }
592
615
  } else {
593
616
  const result = false;
594
- const threshold = typeof overLight === 'object' && overLight !== null
595
- ? (overLight as OverLightObjectConfig).threshold || 0.7
596
- : 0.7;
617
+ const threshold =
618
+ typeof overLight === 'object' && overLight !== null
619
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
620
+ : 0.7;
597
621
  setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
598
622
  setDetectedOverLight(result);
599
623
  }
@@ -749,19 +773,12 @@ export function useAtomixGlass({
749
773
  }
750
774
 
751
775
  return baseConfig;
752
- }, [
753
- overLight,
754
- getEffectiveOverLight,
755
- isHovered,
756
- isActive,
757
- validateConfigValue,
758
- debugOverLight,
759
- ]);
776
+ }, [overLight, getEffectiveOverLight, isHovered, isActive, validateConfigValue, debugOverLight]);
760
777
 
761
778
  // Transform calculation (static base for React render)
762
779
  // Mouse interactions are purely handled by imperative updates in the RAF lerp loop to prevent re-renders
763
780
  const transformStyle = useMemo(() => {
764
- return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.98)' : 'scale(1)';
781
+ return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.99)' : 'scale(1)';
765
782
  }, [effectiveWithoutEffects, isActive, onClick]);
766
783
 
767
784
  // Mouse tracking
@@ -770,7 +787,6 @@ export function useAtomixGlass({
770
787
  // Derived values for imperative updates (we can use memoized ones or re-calculate)
771
788
  // Since updateAtomixGlassStyles is called imperatively, we pass current refs and state
772
789
 
773
-
774
790
  // Handle mouse position updates
775
791
  // ── Raw mouse handler — writes to TARGET refs only ──────────────────
776
792
  // The lerp loop (below) reads the targets and incrementally
@@ -804,82 +820,166 @@ export function useAtomixGlass({
804
820
 
805
821
  const cur = internalMouseOffsetRef.current;
806
822
  const tgt = targetMouseOffsetRef.current;
807
- const dx = tgt.x - cur.x;
808
- const dy = tgt.y - cur.y;
809
-
810
- // If we're close enough, snap and park
811
- if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
812
- internalMouseOffsetRef.current = { ...tgt };
813
- internalGlobalMousePositionRef.current = { ...targetGlobalMousePositionRef.current };
814
-
815
- // Final update and stop
816
- updateAtomixGlassStyles(
817
- wrapperRef?.current || null,
818
- glassRef.current,
819
- {
820
- mouseOffset: internalMouseOffsetRef.current,
821
- globalMousePosition: internalGlobalMousePositionRef.current,
822
- glassSize,
823
- isHovered,
824
- isActive,
825
- isOverLight: overLightConfig.isOverLight,
826
- baseOverLightConfig: overLightConfig,
827
- effectiveBorderRadius,
828
- effectiveWithoutEffects,
829
- effectiveReducedMotion,
830
- elasticity,
831
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
832
- onClick,
833
- withLiquidBlur,
834
- blurAmount,
835
- saturation,
836
- padding,
837
- isFixedOrSticky,
838
- }
839
- );
840
-
841
- stopLerpLoop();
842
- return;
843
- }
844
823
 
845
- // Smooth step
846
- internalMouseOffsetRef.current = {
847
- x: lerp(cur.x, tgt.x, LERP_T),
848
- y: lerp(cur.y, tgt.y, LERP_T),
849
- };
850
-
824
+ // Calculate spring-based mouse offset (keeps the liquid tracking feel)
825
+ const springX = calculateSpring(
826
+ cur.x,
827
+ tgt.x,
828
+ mouseVelocityRef.current.x,
829
+ CONSTANTS.LERP_FACTOR,
830
+ CONSTANTS.ELASTICITY_DAMPING
831
+ );
832
+ const springY = calculateSpring(
833
+ cur.y,
834
+ tgt.y,
835
+ mouseVelocityRef.current.y,
836
+ CONSTANTS.LERP_FACTOR,
837
+ CONSTANTS.ELASTICITY_DAMPING
838
+ );
839
+
840
+ internalMouseOffsetRef.current = { x: springX.value, y: springY.value };
841
+ mouseVelocityRef.current = { x: springX.velocity, y: springY.velocity };
842
+
851
843
  const curG = internalGlobalMousePositionRef.current;
852
844
  const tgtG = targetGlobalMousePositionRef.current;
853
845
  internalGlobalMousePositionRef.current = {
854
- x: lerp(curG.x, tgtG.x, LERP_T),
855
- y: lerp(curG.y, tgtG.y, LERP_T),
846
+ x: lerp(curG.x, tgtG.x, CONSTANTS.LERP_FACTOR),
847
+ y: lerp(curG.y, tgtG.y, CONSTANTS.LERP_FACTOR),
856
848
  };
857
849
 
858
- // Imperative style update
859
- updateAtomixGlassStyles(
860
- wrapperRef?.current || null,
861
- glassRef.current,
862
- {
863
- mouseOffset: internalMouseOffsetRef.current,
864
- globalMousePosition: internalGlobalMousePositionRef.current,
865
- glassSize,
866
- isHovered,
867
- isActive,
868
- isOverLight: overLightConfig.isOverLight,
869
- baseOverLightConfig: overLightConfig,
870
- effectiveBorderRadius,
871
- effectiveWithoutEffects,
872
- effectiveReducedMotion,
873
- elasticity,
874
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
875
- onClick,
876
- withLiquidBlur,
877
- blurAmount,
878
- saturation,
879
- padding,
880
- isFixedOrSticky,
850
+ // ── Calculate Elastic Physics ─────────────────────────────────────
851
+ let targetElasticTranslation = { x: 0, y: 0 };
852
+ let targetScale = { x: 1, y: 1 };
853
+
854
+ if (!effectiveWithoutEffects && glassRef.current) {
855
+ const rect = cachedRectRef.current || glassRef.current.getBoundingClientRect();
856
+ const center = calculateElementCenter(rect);
857
+ const globalPos = internalGlobalMousePositionRef.current;
858
+
859
+ if (globalPos.x && globalPos.y) {
860
+ const deltaX = globalPos.x - center.x;
861
+ const deltaY = globalPos.y - center.y;
862
+ const edgeDistanceX = Math.max(0, Math.abs(deltaX) - rect.width / 2);
863
+ const edgeDistanceY = Math.max(0, Math.abs(deltaY) - rect.height / 2);
864
+ const edgeDistance = Math.sqrt(
865
+ edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY
866
+ );
867
+
868
+ const activationZone = CONSTANTS.ACTIVATION_ZONE;
869
+ const rawT = edgeDistance > activationZone ? 0 : 1 - edgeDistance / activationZone;
870
+ const fadeInFactor = smoothstep(rawT);
871
+
872
+ targetElasticTranslation = {
873
+ x: deltaX * elasticity * CONSTANTS.ELASTICITY_TRANSLATION_FACTOR * fadeInFactor,
874
+ y: deltaY * elasticity * CONSTANTS.ELASTICITY_TRANSLATION_FACTOR * fadeInFactor,
875
+ };
876
+
877
+ // Scale stretch logic (liquid surface tension)
878
+ if (edgeDistance <= activationZone) {
879
+ const centerDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
880
+ if (centerDistance > 0) {
881
+ const nx = deltaX / centerDistance;
882
+ const ny = deltaY / centerDistance;
883
+ const stretchIntensity = Math.min(centerDistance / 350, 1) * elasticity * rawT;
884
+
885
+ // Liquid magnification (lens effect)
886
+ const mag = 1 + stretchIntensity * 0.06;
887
+
888
+ targetScale = {
889
+ x: mag + Math.abs(nx) * stretchIntensity * CONSTANTS.ELASTICITY_STRETCH_RATIO,
890
+ y: mag + Math.abs(ny) * stretchIntensity * CONSTANTS.ELASTICITY_STRETCH_RATIO,
891
+ };
892
+
893
+ // Maintain liquid volume by compressing the perpendicular axis
894
+ targetScale.x -= Math.abs(ny) * stretchIntensity * 0.15;
895
+ targetScale.y -= Math.abs(nx) * stretchIntensity * 0.15;
896
+ }
897
+ }
881
898
  }
899
+ }
900
+
901
+ // Integrate Elastic Translation Spring
902
+ const springTX = calculateSpring(
903
+ elasticTranslationRef.current.x,
904
+ targetElasticTranslation.x,
905
+ elasticVelocityRef.current.x,
906
+ CONSTANTS.ELASTICITY_STIFFNESS,
907
+ CONSTANTS.ELASTICITY_DAMPING
882
908
  );
909
+ const springTY = calculateSpring(
910
+ elasticTranslationRef.current.y,
911
+ targetElasticTranslation.y,
912
+ elasticVelocityRef.current.y,
913
+ CONSTANTS.ELASTICITY_STIFFNESS,
914
+ CONSTANTS.ELASTICITY_DAMPING
915
+ );
916
+
917
+ elasticTranslationRef.current = { x: springTX.value, y: springTY.value };
918
+ elasticVelocityRef.current = { x: springTX.velocity, y: springTY.velocity };
919
+
920
+ // Integrate Scale Spring
921
+ const springSX = calculateSpring(
922
+ directionalScaleRef.current.x,
923
+ targetScale.x,
924
+ scaleVelocityRef.current.x,
925
+ CONSTANTS.ELASTICITY_STIFFNESS,
926
+ CONSTANTS.ELASTICITY_DAMPING
927
+ );
928
+ const springSY = calculateSpring(
929
+ directionalScaleRef.current.y,
930
+ targetScale.y,
931
+ scaleVelocityRef.current.y,
932
+ CONSTANTS.ELASTICITY_STIFFNESS,
933
+ CONSTANTS.ELASTICITY_DAMPING
934
+ );
935
+
936
+ directionalScaleRef.current = { x: springSX.value, y: springSY.value };
937
+ scaleVelocityRef.current = { x: springSX.velocity, y: springSY.velocity };
938
+
939
+ // Imperative style update
940
+ updateAtomixGlassStyles(wrapperRef?.current || null, glassRef.current, {
941
+ mouseOffset: internalMouseOffsetRef.current,
942
+ globalMousePosition: internalGlobalMousePositionRef.current,
943
+ elasticTranslation: elasticTranslationRef.current,
944
+ elasticVelocity: elasticVelocityRef.current,
945
+ mouseVelocity: mouseVelocityRef.current,
946
+ directionalScale: directionalScaleRef.current,
947
+ glassSize,
948
+ isHovered,
949
+ isActive,
950
+ isOverLight: overLightConfig.isOverLight,
951
+ baseOverLightConfig: overLightConfig,
952
+ effectiveBorderRadius,
953
+ effectiveWithoutEffects,
954
+ effectiveReducedMotion,
955
+ elasticity,
956
+ scaleBase: isActive && Boolean(onClick) ? 0.99 : 1,
957
+ onClick,
958
+ withLiquidBlur,
959
+ blurAmount,
960
+ saturation,
961
+ padding,
962
+ isFixedOrSticky,
963
+ });
964
+
965
+ // ── Stop check ──────────────────────────────────────────────────
966
+ const VEL_EPS = 0.001;
967
+ const POS_EPS = 0.001;
968
+
969
+ const isAtRest =
970
+ Math.abs(mouseVelocityRef.current.x) < VEL_EPS &&
971
+ Math.abs(mouseVelocityRef.current.y) < VEL_EPS &&
972
+ Math.abs(elasticVelocityRef.current.x) < VEL_EPS &&
973
+ Math.abs(elasticVelocityRef.current.y) < VEL_EPS &&
974
+ Math.abs(scaleVelocityRef.current.x) < VEL_EPS &&
975
+ Math.abs(scaleVelocityRef.current.y) < VEL_EPS &&
976
+ Math.abs(internalMouseOffsetRef.current.x - targetMouseOffsetRef.current.x) < POS_EPS &&
977
+ Math.abs(internalMouseOffsetRef.current.y - targetMouseOffsetRef.current.y) < POS_EPS;
978
+
979
+ if (isAtRest) {
980
+ stopLerpLoop();
981
+ return;
982
+ }
883
983
 
884
984
  lerpRafRef.current = requestAnimationFrame(tick);
885
985
  };
@@ -965,8 +1065,12 @@ export function useAtomixGlass({
965
1065
  return undefined;
966
1066
  }
967
1067
 
968
- const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
969
-
1068
+ const unsubscribe = globalMouseTracker.subscribe(
1069
+ handleGlobalMousePosition,
1070
+ glassRef.current || undefined,
1071
+ 300
1072
+ ); // 300px max distance for full effect
1073
+
970
1074
  // Initial start
971
1075
  startLerpLoop();
972
1076
 
@@ -1015,29 +1119,29 @@ export function useAtomixGlass({
1015
1119
 
1016
1120
  // Also call updateStyles on other state changes (hover, active, etc)
1017
1121
  useEffect(() => {
1018
- updateAtomixGlassStyles(
1019
- wrapperRef?.current || null,
1020
- glassRef.current,
1021
- {
1022
- mouseOffset: externalMouseOffset || internalMouseOffsetRef.current,
1023
- globalMousePosition: externalGlobalMousePosition || internalGlobalMousePositionRef.current,
1024
- glassSize,
1025
- isHovered,
1026
- isActive,
1027
- isOverLight: overLightConfig.isOverLight,
1028
- baseOverLightConfig: overLightConfig,
1029
- effectiveBorderRadius,
1030
- effectiveWithoutEffects,
1031
- effectiveReducedMotion,
1032
- elasticity,
1033
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
1034
- onClick,
1035
- withLiquidBlur,
1036
- blurAmount,
1037
- saturation,
1038
- padding,
1039
- }
1040
- );
1122
+ updateAtomixGlassStyles(wrapperRef?.current || null, glassRef.current, {
1123
+ mouseOffset: externalMouseOffset || internalMouseOffsetRef.current,
1124
+ globalMousePosition: externalGlobalMousePosition || internalGlobalMousePositionRef.current,
1125
+ elasticTranslation: elasticTranslationRef.current,
1126
+ elasticVelocity: elasticVelocityRef.current,
1127
+ mouseVelocity: mouseVelocityRef.current,
1128
+ directionalScale: directionalScaleRef.current,
1129
+ scaleBase: isActive && Boolean(onClick) ? 0.96 : 1,
1130
+ glassSize,
1131
+ isHovered,
1132
+ isActive,
1133
+ isOverLight: overLightConfig.isOverLight,
1134
+ baseOverLightConfig: overLightConfig,
1135
+ effectiveBorderRadius,
1136
+ effectiveWithoutEffects,
1137
+ effectiveReducedMotion,
1138
+ elasticity,
1139
+ onClick,
1140
+ withLiquidBlur,
1141
+ blurAmount,
1142
+ saturation,
1143
+ padding,
1144
+ });
1041
1145
  }, [
1042
1146
  isHovered,
1043
1147
  isActive,
@@ -1055,7 +1159,7 @@ export function useAtomixGlass({
1055
1159
  blurAmount,
1056
1160
  saturation,
1057
1161
  padding,
1058
- onClick
1162
+ onClick,
1059
1163
  ]);
1060
1164
 
1061
1165
  // Event handlers
@@ -1064,8 +1168,6 @@ export function useAtomixGlass({
1064
1168
  const handleMouseDown = useCallback(() => setIsActive(true), []);
1065
1169
  const handleMouseUp = useCallback(() => setIsActive(false), []);
1066
1170
 
1067
-
1068
-
1069
1171
  const handleKeyDown = useCallback(
1070
1172
  (e: React.KeyboardEvent<HTMLDivElement>) => {
1071
1173
  if (onClick && (e.key === 'Enter' || e.key === ' ')) {
@@ -1087,7 +1189,7 @@ export function useAtomixGlass({
1087
1189
  effectiveWithoutEffects,
1088
1190
  detectedOverLight,
1089
1191
  globalMousePosition, // This is now static (refs or props) unless prop changes
1090
- mouseOffset, // This is now static (refs or props) unless prop changes
1192
+ mouseOffset, // This is now static (refs or props) unless prop changes
1091
1193
  overLightConfig,
1092
1194
  transformStyle,
1093
1195
  getShaderTime,
@@ -1098,4 +1200,4 @@ export function useAtomixGlass({
1098
1200
  handleMouseUp,
1099
1201
  handleKeyDown,
1100
1202
  };
1101
- }
1203
+ }