@shohojdhara/atomix 0.6.2 → 0.6.4

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 (63) hide show
  1. package/README.md +510 -106
  2. package/dist/atomix.css +26 -22
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +5 -5
  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 +2 -2
  10. package/dist/charts.js +251 -131
  11. package/dist/charts.js.map +1 -1
  12. package/dist/core.d.ts +5 -39
  13. package/dist/core.js +254 -137
  14. package/dist/core.js.map +1 -1
  15. package/dist/forms.d.ts +2 -1
  16. package/dist/forms.js +342 -177
  17. package/dist/forms.js.map +1 -1
  18. package/dist/heavy.js +254 -135
  19. package/dist/heavy.js.map +1 -1
  20. package/dist/index.d.ts +141 -159
  21. package/dist/index.esm.js +348 -195
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.js +348 -195
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.min.js +1 -1
  26. package/dist/index.min.js.map +1 -1
  27. package/dist/theme.d.ts +14 -6
  28. package/dist/theme.js +2 -9
  29. package/dist/theme.js.map +1 -1
  30. package/package.json +26 -26
  31. package/src/components/AtomixGlass/AtomixGlass.tsx +1 -1
  32. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +8 -1
  33. package/src/components/AtomixGlass/glass-utils.ts +29 -0
  34. package/src/components/AtomixGlass/stories/Playground.stories.tsx +32 -1
  35. package/src/components/Button/Button.stories.tsx +1 -1
  36. package/src/components/Button/Button.tsx +6 -5
  37. package/src/components/Card/Card.tsx +2 -2
  38. package/src/components/Dropdown/Dropdown.tsx +1 -0
  39. package/src/components/EdgePanel/EdgePanel.tsx +1 -3
  40. package/src/components/Form/Select.test.tsx +75 -0
  41. package/src/components/Form/Select.tsx +348 -252
  42. package/src/components/Form/SelectOption.tsx +16 -10
  43. package/src/components/index.ts +1 -1
  44. package/src/layouts/CssGrid/index.ts +1 -0
  45. package/src/lib/composables/useAtomixGlass.ts +238 -138
  46. package/src/lib/composables/useAtomixGlassStyles.ts +201 -149
  47. package/src/lib/constants/components.ts +50 -35
  48. package/src/lib/theme/config/configLoader.ts +1 -1
  49. package/src/lib/theme/test/testTheme.ts +2 -2
  50. package/src/lib/theme/utils/themeUtils.ts +98 -110
  51. package/src/lib/types/components.ts +21 -63
  52. package/src/styles/01-settings/_settings.atomix-glass.scss +5 -5
  53. package/src/styles/01-settings/_settings.spacing.scss +6 -1
  54. package/src/styles/03-generic/_generic.reset.scss +1 -1
  55. package/src/styles/06-components/_components.atomix-glass.scss +20 -29
  56. package/src/styles/06-components/_components.data-table.scss +5 -4
  57. package/src/styles/06-components/_components.dynamic-background.scss +9 -8
  58. package/src/styles/06-components/_components.footer.scss +8 -7
  59. package/src/styles/06-components/_components.hero.scss +2 -2
  60. package/src/styles/06-components/_components.messages.scss +16 -16
  61. package/src/styles/06-components/_components.select.scss +15 -2
  62. package/src/styles/06-components/_components.upload.scss +3 -3
  63. package/CHANGELOG.md +0 -165
@@ -8,12 +8,15 @@ export interface SelectContextType {
8
8
  unregisterOption: (value: string) => void;
9
9
  selectedValue?: string | string[];
10
10
  onSelect: (value: string, label: string) => void;
11
+ focusedValue?: string;
12
+ id?: string;
11
13
  }
12
14
 
13
15
  export const SelectContext = createContext<SelectContextType | null>(null);
14
16
 
15
17
  export interface SelectOptionProps {
16
18
  value: string;
19
+ label?: string;
17
20
  children?: ReactNode;
18
21
  disabled?: boolean;
19
22
  className?: string;
@@ -21,45 +24,48 @@ export interface SelectOptionProps {
21
24
  }
22
25
 
23
26
  export const SelectOption: React.FC<SelectOptionProps> = memo(
24
- ({ value, children, disabled = false, className = '', style }) => {
27
+ ({ value, label, children, disabled = false, className = '', style }) => {
25
28
  const context = useContext(SelectContext);
26
29
 
27
- // We assume children is the label if it's a string, or we need a way to get label.
28
- // For simplicity, we use children as label for registration if it's a string.
29
- const label = typeof children === 'string' ? children : value;
30
+ const displayLabel = label || (typeof children === 'string' ? children : value);
30
31
 
31
32
  useEffect(() => {
32
33
  if (context) {
33
- context.registerOption({ value, label, disabled });
34
+ context.registerOption({ value, label: displayLabel, disabled });
34
35
  return () => {
35
36
  context.unregisterOption(value);
36
37
  };
37
38
  }
38
39
  return undefined;
39
- }, [context, value, label, disabled]);
40
+ }, [context, value, displayLabel, disabled]);
40
41
 
41
42
  if (!context) {
42
43
  console.warn('SelectOption must be used within a Select component');
43
44
  return null;
44
45
  }
45
46
 
46
- const { selectedValue, onSelect } = context;
47
+ const { selectedValue, onSelect, focusedValue, id } = context;
47
48
 
48
49
  const isSelected = Array.isArray(selectedValue)
49
50
  ? selectedValue.includes(value)
50
51
  : selectedValue === value;
51
52
 
53
+ const isFocused = focusedValue === value;
54
+
52
55
  const handleClick = (e: React.MouseEvent) => {
53
56
  e.preventDefault();
54
57
  e.stopPropagation();
55
58
  if (!disabled) {
56
- onSelect(value, label);
59
+ onSelect(value, displayLabel);
57
60
  }
58
61
  };
59
62
 
60
63
  return (
61
64
  <li
62
- className={`${SELECT.CLASSES.SELECT_ITEM} ${className}`.trim()}
65
+ id={id ? `${id}-opt-${value}` : undefined}
66
+ className={`${SELECT.CLASSES.SELECT_ITEM} ${isFocused ? 'is-focused' : ''} ${
67
+ isSelected ? 'is-selected' : ''
68
+ } ${className}`.trim()}
63
69
  data-value={value}
64
70
  onClick={handleClick}
65
71
  style={style}
@@ -76,7 +82,7 @@ export const SelectOption: React.FC<SelectOptionProps> = memo(
76
82
  disabled={disabled}
77
83
  tabIndex={-1}
78
84
  />
79
- <div className="c-select__item-label">{children}</div>
85
+ <div className="c-select__item-label">{children || displayLabel}</div>
80
86
  </label>
81
87
  </li>
82
88
  );
@@ -89,7 +89,7 @@ export { default as Radio, type RadioProps } from './Form/Radio';
89
89
  export { default as Select, type SelectProps } from './Form/Select';
90
90
  export { default as Textarea, type TextareaProps } from './Form/Textarea';
91
91
  export { default as Hero, type HeroProps } from './Hero/Hero';
92
- export { default as Icon, type IconProps } from './Icon/Icon';
92
+ export { default as Icon } from './Icon/Icon';
93
93
  export { default as List, type ListProps } from './List/List';
94
94
  // List sub-components
95
95
  export { ListGroup } from './List/ListGroup';
@@ -3,6 +3,7 @@
3
3
  * @description Export for both React and vanilla JavaScript CSS Grid implementations
4
4
  */
5
5
 
6
+ import { CssGrid } from './CssGrid';
6
7
  export { CssGrid, type CssGridProps, type ResponsiveColumns } from './CssGrid';
7
8
 
8
9
  export default CssGrid;
@@ -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;
@@ -267,6 +261,15 @@ export function useAtomixGlass({
267
261
  const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
268
262
  CONSTANTS.DEFAULT_CORNER_RADIUS
269
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
+
270
273
  const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(false);
271
274
  const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
272
275
  const [detectedOverLight, setDetectedOverLight] = useState(false);
@@ -287,7 +290,7 @@ export function useAtomixGlass({
287
290
  const fbmConfig = useMemo(() => {
288
291
  // If quality preset is provided, use it as base
289
292
  const preset = getFBMConfigForQuality(distortionQuality);
290
-
293
+
291
294
  // Override with custom values if provided
292
295
  return {
293
296
  octaves: distortionOctaves ?? preset.octaves,
@@ -373,7 +376,7 @@ export function useAtomixGlass({
373
376
  }
374
377
 
375
378
  const time = shaderTimeRef.current;
376
-
379
+
377
380
  // Apply liquid glass distortion with time
378
381
  return liquidGlassWithTime(uv, time, fbmConfig);
379
382
  },
@@ -390,13 +393,12 @@ export function useAtomixGlass({
390
393
  return result;
391
394
  }, [borderRadius, dynamicBorderRadius]);
392
395
 
393
- const { glassSize } = useGlassSize({
394
- glassRef,
395
- effectiveBorderRadius,
396
- cachedRectRef
396
+ const { glassSize } = useGlassSize({
397
+ glassRef,
398
+ effectiveBorderRadius,
399
+ cachedRectRef,
397
400
  });
398
401
 
399
-
400
402
  const effectiveHighContrast = useMemo(
401
403
  () => highContrast || userPrefersHighContrast,
402
404
  [highContrast, userPrefersHighContrast]
@@ -438,7 +440,10 @@ export function useAtomixGlass({
438
440
  setDynamicCornerRadius(extractedRadius);
439
441
  }
440
442
  } catch (error) {
441
- if ((typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') && debugBorderRadius) {
443
+ if (
444
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
445
+ debugBorderRadius
446
+ ) {
442
447
  console.error('[AtomixGlass] Error extracting corner radius:', error);
443
448
  }
444
449
  }
@@ -481,7 +486,8 @@ export function useAtomixGlass({
481
486
  // Background detection for overLight auto-detect
482
487
  useEffect(() => {
483
488
  // Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
484
- const shouldDetect = (overLight === 'auto' || (typeof overLight === 'object' && overLight !== null));
489
+ const shouldDetect =
490
+ overLight === 'auto' || (typeof overLight === 'object' && overLight !== null);
485
491
 
486
492
  if (shouldDetect && glassRef.current) {
487
493
  const element = glassRef.current;
@@ -530,7 +536,13 @@ export function useAtomixGlass({
530
536
  const bgImage = computedStyle.backgroundImage;
531
537
 
532
538
  // Check for solid color backgrounds
533
- 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
+ ) {
534
546
  const rgb = bgColor.match(/\d+/g);
535
547
  if (rgb && rgb.length >= 3) {
536
548
  const r = Number(rgb[0]);
@@ -556,7 +568,7 @@ export function useAtomixGlass({
556
568
  hasValidBackground = true;
557
569
  }
558
570
  } catch (styleError) {
559
- // Silently continue
571
+ // Silently continue
560
572
  }
561
573
 
562
574
  if (currentElement) {
@@ -575,27 +587,37 @@ export function useAtomixGlass({
575
587
  if (typeof overLight === 'object' && overLight !== null) {
576
588
  const objConfig = overLight as OverLightObjectConfig;
577
589
  if (objConfig.threshold !== undefined) {
578
- 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;
579
594
  threshold = Math.min(0.9, Math.max(0.1, configThreshold));
580
595
  }
581
596
  }
582
597
 
583
598
  const isOverLightDetected = avgLuminance > threshold;
584
- setCachedBackgroundDetection(element.parentElement, overLight, isOverLightDetected, threshold);
599
+ setCachedBackgroundDetection(
600
+ element.parentElement,
601
+ overLight,
602
+ isOverLightDetected,
603
+ threshold
604
+ );
585
605
  setDetectedOverLight(isOverLightDetected);
586
606
  } else {
587
607
  const result = false;
588
- const threshold = typeof overLight === 'object' && overLight !== null
589
- ? (overLight as OverLightObjectConfig).threshold || 0.7
590
- : 0.7;
608
+ const threshold =
609
+ typeof overLight === 'object' && overLight !== null
610
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
611
+ : 0.7;
591
612
  setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
592
613
  setDetectedOverLight(result);
593
614
  }
594
615
  } else {
595
616
  const result = false;
596
- const threshold = typeof overLight === 'object' && overLight !== null
597
- ? (overLight as OverLightObjectConfig).threshold || 0.7
598
- : 0.7;
617
+ const threshold =
618
+ typeof overLight === 'object' && overLight !== null
619
+ ? (overLight as OverLightObjectConfig).threshold || 0.7
620
+ : 0.7;
599
621
  setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
600
622
  setDetectedOverLight(result);
601
623
  }
@@ -751,19 +773,12 @@ export function useAtomixGlass({
751
773
  }
752
774
 
753
775
  return baseConfig;
754
- }, [
755
- overLight,
756
- getEffectiveOverLight,
757
- isHovered,
758
- isActive,
759
- validateConfigValue,
760
- debugOverLight,
761
- ]);
776
+ }, [overLight, getEffectiveOverLight, isHovered, isActive, validateConfigValue, debugOverLight]);
762
777
 
763
778
  // Transform calculation (static base for React render)
764
779
  // Mouse interactions are purely handled by imperative updates in the RAF lerp loop to prevent re-renders
765
780
  const transformStyle = useMemo(() => {
766
- return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.98)' : 'scale(1)';
781
+ return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.99)' : 'scale(1)';
767
782
  }, [effectiveWithoutEffects, isActive, onClick]);
768
783
 
769
784
  // Mouse tracking
@@ -772,7 +787,6 @@ export function useAtomixGlass({
772
787
  // Derived values for imperative updates (we can use memoized ones or re-calculate)
773
788
  // Since updateAtomixGlassStyles is called imperatively, we pass current refs and state
774
789
 
775
-
776
790
  // Handle mouse position updates
777
791
  // ── Raw mouse handler — writes to TARGET refs only ──────────────────
778
792
  // The lerp loop (below) reads the targets and incrementally
@@ -806,82 +820,166 @@ export function useAtomixGlass({
806
820
 
807
821
  const cur = internalMouseOffsetRef.current;
808
822
  const tgt = targetMouseOffsetRef.current;
809
- const dx = tgt.x - cur.x;
810
- const dy = tgt.y - cur.y;
811
-
812
- // If we're close enough, snap and park
813
- if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
814
- internalMouseOffsetRef.current = { ...tgt };
815
- internalGlobalMousePositionRef.current = { ...targetGlobalMousePositionRef.current };
816
-
817
- // Final update and stop
818
- updateAtomixGlassStyles(
819
- wrapperRef?.current || null,
820
- glassRef.current,
821
- {
822
- mouseOffset: internalMouseOffsetRef.current,
823
- globalMousePosition: internalGlobalMousePositionRef.current,
824
- glassSize,
825
- isHovered,
826
- isActive,
827
- isOverLight: overLightConfig.isOverLight,
828
- baseOverLightConfig: overLightConfig,
829
- effectiveBorderRadius,
830
- effectiveWithoutEffects,
831
- effectiveReducedMotion,
832
- elasticity,
833
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
834
- onClick,
835
- withLiquidBlur,
836
- blurAmount,
837
- saturation,
838
- padding,
839
- isFixedOrSticky,
840
- }
841
- );
842
-
843
- stopLerpLoop();
844
- return;
845
- }
846
823
 
847
- // Smooth step
848
- internalMouseOffsetRef.current = {
849
- x: lerp(cur.x, tgt.x, LERP_T),
850
- y: lerp(cur.y, tgt.y, LERP_T),
851
- };
852
-
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
+
853
843
  const curG = internalGlobalMousePositionRef.current;
854
844
  const tgtG = targetGlobalMousePositionRef.current;
855
845
  internalGlobalMousePositionRef.current = {
856
- x: lerp(curG.x, tgtG.x, LERP_T),
857
- 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),
858
848
  };
859
849
 
860
- // Imperative style update
861
- updateAtomixGlassStyles(
862
- wrapperRef?.current || null,
863
- glassRef.current,
864
- {
865
- mouseOffset: internalMouseOffsetRef.current,
866
- globalMousePosition: internalGlobalMousePositionRef.current,
867
- glassSize,
868
- isHovered,
869
- isActive,
870
- isOverLight: overLightConfig.isOverLight,
871
- baseOverLightConfig: overLightConfig,
872
- effectiveBorderRadius,
873
- effectiveWithoutEffects,
874
- effectiveReducedMotion,
875
- elasticity,
876
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
877
- onClick,
878
- withLiquidBlur,
879
- blurAmount,
880
- saturation,
881
- padding,
882
- 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
+ }
883
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
884
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
+ }
885
983
 
886
984
  lerpRafRef.current = requestAnimationFrame(tick);
887
985
  };
@@ -967,8 +1065,12 @@ export function useAtomixGlass({
967
1065
  return undefined;
968
1066
  }
969
1067
 
970
- const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition, glassRef.current || undefined, 300); // 300px max distance for full effect
971
-
1068
+ const unsubscribe = globalMouseTracker.subscribe(
1069
+ handleGlobalMousePosition,
1070
+ glassRef.current || undefined,
1071
+ 300
1072
+ ); // 300px max distance for full effect
1073
+
972
1074
  // Initial start
973
1075
  startLerpLoop();
974
1076
 
@@ -1017,29 +1119,29 @@ export function useAtomixGlass({
1017
1119
 
1018
1120
  // Also call updateStyles on other state changes (hover, active, etc)
1019
1121
  useEffect(() => {
1020
- updateAtomixGlassStyles(
1021
- wrapperRef?.current || null,
1022
- glassRef.current,
1023
- {
1024
- mouseOffset: externalMouseOffset || internalMouseOffsetRef.current,
1025
- globalMousePosition: externalGlobalMousePosition || internalGlobalMousePositionRef.current,
1026
- glassSize,
1027
- isHovered,
1028
- isActive,
1029
- isOverLight: overLightConfig.isOverLight,
1030
- baseOverLightConfig: overLightConfig,
1031
- effectiveBorderRadius,
1032
- effectiveWithoutEffects,
1033
- effectiveReducedMotion,
1034
- elasticity,
1035
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
1036
- onClick,
1037
- withLiquidBlur,
1038
- blurAmount,
1039
- saturation,
1040
- padding,
1041
- }
1042
- );
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
+ });
1043
1145
  }, [
1044
1146
  isHovered,
1045
1147
  isActive,
@@ -1057,7 +1159,7 @@ export function useAtomixGlass({
1057
1159
  blurAmount,
1058
1160
  saturation,
1059
1161
  padding,
1060
- onClick
1162
+ onClick,
1061
1163
  ]);
1062
1164
 
1063
1165
  // Event handlers
@@ -1066,8 +1168,6 @@ export function useAtomixGlass({
1066
1168
  const handleMouseDown = useCallback(() => setIsActive(true), []);
1067
1169
  const handleMouseUp = useCallback(() => setIsActive(false), []);
1068
1170
 
1069
-
1070
-
1071
1171
  const handleKeyDown = useCallback(
1072
1172
  (e: React.KeyboardEvent<HTMLDivElement>) => {
1073
1173
  if (onClick && (e.key === 'Enter' || e.key === ' ')) {
@@ -1089,7 +1189,7 @@ export function useAtomixGlass({
1089
1189
  effectiveWithoutEffects,
1090
1190
  detectedOverLight,
1091
1191
  globalMousePosition, // This is now static (refs or props) unless prop changes
1092
- mouseOffset, // This is now static (refs or props) unless prop changes
1192
+ mouseOffset, // This is now static (refs or props) unless prop changes
1093
1193
  overLightConfig,
1094
1194
  transformStyle,
1095
1195
  getShaderTime,
@@ -1100,4 +1200,4 @@ export function useAtomixGlass({
1100
1200
  handleMouseUp,
1101
1201
  handleKeyDown,
1102
1202
  };
1103
- }
1203
+ }