@shohojdhara/atomix 0.4.5 → 0.4.7

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 (54) hide show
  1. package/dist/atomix.css +70 -33
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +2 -2
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.d.ts +93 -109
  6. package/dist/charts.js +273 -371
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.js +183 -184
  9. package/dist/core.js.map +1 -1
  10. package/dist/forms.js +183 -184
  11. package/dist/forms.js.map +1 -1
  12. package/dist/heavy.js +183 -184
  13. package/dist/heavy.js.map +1 -1
  14. package/dist/index.d.ts +7 -51
  15. package/dist/index.esm.js +281 -470
  16. package/dist/index.esm.js.map +1 -1
  17. package/dist/index.js +287 -476
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.min.js +1 -1
  20. package/dist/index.min.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/components/AtomixGlass/AtomixGlass.tsx +60 -38
  23. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +6 -35
  24. package/src/components/AtomixGlass/glass-utils.ts +27 -14
  25. package/src/components/AtomixGlass/stories/Overview.stories.tsx +19 -21
  26. package/src/components/AtomixGlass/stories/Playground.stories.tsx +1162 -515
  27. package/src/components/AtomixGlass/stories/shared-components.tsx +11 -3
  28. package/src/components/Chart/BubbleChart.tsx +6 -2
  29. package/src/components/Chart/Chart.stories.tsx +108 -96
  30. package/src/components/Chart/ChartToolbar.tsx +6 -4
  31. package/src/components/Chart/ChartTooltip.tsx +5 -4
  32. package/src/components/Chart/GaugeChart.tsx +20 -12
  33. package/src/components/Chart/HeatmapChart.tsx +53 -23
  34. package/src/components/Chart/TreemapChart.tsx +44 -15
  35. package/src/components/Chart/index.ts +0 -2
  36. package/src/components/Chart/types.ts +4 -4
  37. package/src/components/Navigation/Navbar/Navbar.tsx +13 -5
  38. package/src/components/index.ts +0 -1
  39. package/src/lib/composables/index.ts +1 -2
  40. package/src/lib/composables/useAtomixGlass.ts +246 -222
  41. package/src/lib/composables/useAtomixGlassStyles.ts +46 -23
  42. package/src/lib/constants/components.ts +3 -1
  43. package/src/styles/01-settings/_settings.chart.scss +13 -13
  44. package/src/styles/06-components/_components.atomix-glass.scss +45 -20
  45. package/src/styles/06-components/_components.chart.scss +23 -5
  46. package/src/components/Chart/AnimatedChart.tsx +0 -230
  47. package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +0 -329
  48. package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +0 -82
  49. package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +0 -153
  50. package/src/lib/composables/atomix-glass/useGlassOverLight.ts +0 -198
  51. package/src/lib/composables/atomix-glass/useGlassState.ts +0 -112
  52. package/src/lib/composables/atomix-glass/useGlassTransforms.ts +0 -160
  53. package/src/lib/composables/glass-styles.ts +0 -302
  54. package/src/lib/composables/useGlassContainer.ts +0 -177
@@ -15,12 +15,12 @@ import type {
15
15
  import { ATOMIX_GLASS } from '../constants/components';
16
16
  import { globalMouseTracker } from './shared-mouse-tracker';
17
17
  import {
18
- calculateDistance,
19
18
  calculateElementCenter,
20
19
  calculateMouseInfluence,
21
20
  extractBorderRadiusFromChildren,
22
21
  extractBorderRadiusFromDOMElement,
23
22
  validateGlassSize,
23
+ lerp,
24
24
  } from '../../components/AtomixGlass/glass-utils';
25
25
  import { updateAtomixGlassStyles } from './useAtomixGlassStyles';
26
26
 
@@ -179,8 +179,6 @@ interface UseAtomixGlassReturn {
179
179
  };
180
180
 
181
181
  // Transform calculations
182
- elasticTranslation: { x: number; y: number };
183
- directionalScale: string;
184
182
  transformStyle: string;
185
183
 
186
184
  // Event handlers
@@ -188,7 +186,6 @@ interface UseAtomixGlassReturn {
188
186
  handleMouseLeave: () => void;
189
187
  handleMouseDown: () => void;
190
188
  handleMouseUp: () => void;
191
- handleMouseMove: (e: MouseEvent) => void;
192
189
  handleKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
193
190
  }
194
191
 
@@ -230,6 +227,14 @@ export function useAtomixGlass({
230
227
  const internalGlobalMousePositionRef = useRef<MousePosition>({ x: 0, y: 0 });
231
228
  const internalMouseOffsetRef = useRef<MousePosition>({ x: 0, y: 0 });
232
229
 
230
+ // ── Lerp smoothing refs ───────────────────────────────────────────────
231
+ // Target positions that raw mouse events write to;
232
+ // the lerp loop continuously interpolates the "current" refs toward these.
233
+ const targetMouseOffsetRef = useRef<MousePosition>({ x: 0, y: 0 });
234
+ const targetGlobalMousePositionRef = useRef<MousePosition>({ x: 0, y: 0 });
235
+ const lerpRafRef = useRef<number | null>(null);
236
+ const lerpActiveRef = useRef(false);
237
+
233
238
  const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
234
239
  CONSTANTS.DEFAULT_CORNER_RADIUS
235
240
  );
@@ -310,7 +315,36 @@ export function useAtomixGlass({
310
315
  return () => clearTimeout(timeoutId);
311
316
  }, [children, debugBorderRadius, contentRef]);
312
317
 
313
- // Media query handlers and background detection
318
+ // Media query detection for reduced motion and high contrast
319
+ useEffect(() => {
320
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
321
+ return undefined;
322
+ }
323
+
324
+ const mediaQueryReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
325
+ const mediaQueryHighContrast = window.matchMedia('(prefers-contrast: high)');
326
+
327
+ setUserPrefersReducedMotion(mediaQueryReducedMotion.matches);
328
+ setUserPrefersHighContrast(mediaQueryHighContrast.matches);
329
+
330
+ const handleReducedMotionChange = (e: MediaQueryListEvent) => {
331
+ setUserPrefersReducedMotion(e.matches);
332
+ };
333
+
334
+ const handleHighContrastChange = (e: MediaQueryListEvent) => {
335
+ setUserPrefersHighContrast(e.matches);
336
+ };
337
+
338
+ mediaQueryReducedMotion.addEventListener('change', handleReducedMotionChange);
339
+ mediaQueryHighContrast.addEventListener('change', handleHighContrastChange);
340
+
341
+ return () => {
342
+ mediaQueryReducedMotion.removeEventListener('change', handleReducedMotionChange);
343
+ mediaQueryHighContrast.removeEventListener('change', handleHighContrastChange);
344
+ };
345
+ }, []);
346
+
347
+ // Background detection for overLight auto-detect
314
348
  useEffect(() => {
315
349
  // Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
316
350
  const shouldDetect = (overLight === 'auto' || (typeof overLight === 'object' && overLight !== null));
@@ -442,44 +476,8 @@ export function useAtomixGlass({
442
476
  setDetectedOverLight(false);
443
477
  }
444
478
 
445
- if (typeof window.matchMedia !== 'function') {
446
- return undefined;
447
- }
448
-
449
- try {
450
- const mediaQueryReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
451
- const mediaQueryHighContrast = window.matchMedia('(prefers-contrast: high)');
452
-
453
- setUserPrefersReducedMotion(mediaQueryReducedMotion.matches);
454
- setUserPrefersHighContrast(mediaQueryHighContrast.matches);
455
-
456
- const handleReducedMotionChange = (e: MediaQueryListEvent) => {
457
- setUserPrefersReducedMotion(e.matches);
458
- };
459
-
460
- const handleHighContrastChange = (e: MediaQueryListEvent) => {
461
- setUserPrefersHighContrast(e.matches);
462
- };
463
-
464
- if (mediaQueryReducedMotion.addEventListener) {
465
- mediaQueryReducedMotion.addEventListener('change', handleReducedMotionChange);
466
- mediaQueryHighContrast.addEventListener('change', handleHighContrastChange);
467
- } else if (mediaQueryReducedMotion.addListener) {
468
- mediaQueryReducedMotion.addListener(handleReducedMotionChange);
469
- mediaQueryHighContrast.addListener(handleHighContrastChange);
470
- }
471
-
472
- return () => {
473
- try {
474
- // cleanup
475
- } catch (cleanupError) {
476
- // ignore
477
- }
478
- };
479
- } catch (error) {
480
- return undefined;
481
- }
482
- }, [overLight, glassRef, debugOverLight]);
479
+ return undefined;
480
+ }, [overLight, glassRef]);
483
481
 
484
482
  /**
485
483
  * Get effective overLight value based on configuration
@@ -510,21 +508,23 @@ export function useAtomixGlass({
510
508
  []
511
509
  );
512
510
 
513
- // Calculate Base OverLight Config (without mouse influence)
514
- const baseOverLightConfig = useMemo(() => {
511
+ const overLightConfig = useMemo(() => {
515
512
  const isOverLight = getEffectiveOverLight();
516
- // Use static mouse influence for base config
517
- const mouseInfluence = 0;
513
+ const hoverIntensity = isHovered ? 1.4 : 1;
514
+ const activeIntensity = isActive ? 1.6 : 1;
518
515
 
519
- const baseOpacity = isOverLight ? Math.min(0.6, Math.max(0.2, 0.5)) : 0;
516
+ // More robust overlight configuration with better defaults and clamping
517
+ const baseOpacity = isOverLight
518
+ ? Math.min(0.6, Math.max(0.2, 0.5 * hoverIntensity * activeIntensity))
519
+ : 0;
520
520
 
521
521
  const baseConfig = {
522
522
  isOverLight,
523
523
  threshold: 0.7,
524
524
  opacity: baseOpacity,
525
- contrast: 1, // Base contrast
526
- brightness: 1, // Base brightness
527
- saturationBoost: 1.3,
525
+ contrast: 1.4,
526
+ brightness: 0.9,
527
+ saturationBoost: 1.3, // Fixed value — dynamic saturation amplifies perceived displacement
528
528
  shadowIntensity: 0.9,
529
529
  borderOpacity: 0.7,
530
530
  };
@@ -532,42 +532,105 @@ export function useAtomixGlass({
532
532
  if (typeof overLight === 'object' && overLight !== null) {
533
533
  const objConfig = overLight as OverLightObjectConfig;
534
534
 
535
- const validatedThreshold = validateConfigValue(objConfig.threshold, 0.1, 1.0, baseConfig.threshold);
535
+ const validatedThreshold = validateConfigValue(
536
+ objConfig.threshold,
537
+ 0.1,
538
+ 1.0,
539
+ baseConfig.threshold
540
+ );
536
541
  const validatedOpacity = validateConfigValue(objConfig.opacity, 0.1, 1.0, baseConfig.opacity);
537
- const validatedContrast = validateConfigValue(objConfig.contrast, 0.5, 2.5, baseConfig.contrast);
538
- const validatedBrightness = validateConfigValue(objConfig.brightness, 0.5, 2.0, baseConfig.brightness);
539
- const validatedSaturationBoost = validateConfigValue(objConfig.saturationBoost, 0.5, 3.0, baseConfig.saturationBoost);
542
+ const validatedContrast = validateConfigValue(
543
+ objConfig.contrast,
544
+ 0.5,
545
+ 2.5,
546
+ baseConfig.contrast
547
+ );
548
+ const validatedBrightness = validateConfigValue(
549
+ objConfig.brightness,
550
+ 0.5,
551
+ 2.0,
552
+ baseConfig.brightness
553
+ );
554
+ const validatedSaturationBoost = validateConfigValue(
555
+ objConfig.saturationBoost,
556
+ 0.5,
557
+ 3.0,
558
+ baseConfig.saturationBoost
559
+ );
540
560
 
541
- return {
561
+ const finalConfig = {
542
562
  ...baseConfig,
543
563
  threshold: validatedThreshold,
544
- opacity: validatedOpacity,
564
+ opacity: validatedOpacity * hoverIntensity * activeIntensity,
545
565
  contrast: validatedContrast,
546
566
  brightness: validatedBrightness,
547
567
  saturationBoost: validatedSaturationBoost,
548
568
  };
569
+
570
+ if (
571
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
572
+ debugOverLight
573
+ ) {
574
+ console.log('[AtomixGlass] OverLight Config:', {
575
+ isOverLight,
576
+ config: {
577
+ threshold: finalConfig.threshold.toFixed(3),
578
+ opacity: finalConfig.opacity.toFixed(3),
579
+ contrast: finalConfig.contrast.toFixed(3),
580
+ brightness: finalConfig.brightness.toFixed(3),
581
+ saturationBoost: finalConfig.saturationBoost.toFixed(3),
582
+ shadowIntensity: finalConfig.shadowIntensity.toFixed(3),
583
+ borderOpacity: finalConfig.borderOpacity.toFixed(3),
584
+ },
585
+ input: {
586
+ threshold: objConfig.threshold,
587
+ opacity: objConfig.opacity,
588
+ contrast: objConfig.contrast,
589
+ brightness: objConfig.brightness,
590
+ saturationBoost: objConfig.saturationBoost,
591
+ },
592
+ timestamp: new Date().toISOString(),
593
+ });
594
+ }
595
+
596
+ return finalConfig;
549
597
  }
550
598
 
551
- return baseConfig;
552
- }, [overLight, getEffectiveOverLight, validateConfigValue]);
599
+ if (
600
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
601
+ debugOverLight
602
+ ) {
603
+ console.log('[AtomixGlass] OverLight Config:', {
604
+ isOverLight,
605
+ configType: typeof overLight === 'boolean' ? (overLight ? 'true' : 'false') : overLight,
606
+ config: {
607
+ threshold: baseConfig.threshold.toFixed(3),
608
+ opacity: baseConfig.opacity.toFixed(3),
609
+ contrast: baseConfig.contrast.toFixed(3),
610
+ brightness: baseConfig.brightness.toFixed(3),
611
+ saturationBoost: baseConfig.saturationBoost.toFixed(3),
612
+ shadowIntensity: baseConfig.shadowIntensity.toFixed(3),
613
+ borderOpacity: baseConfig.borderOpacity.toFixed(3),
614
+ },
615
+ timestamp: new Date().toISOString(),
616
+ });
617
+ }
553
618
 
554
- // Calculate Effective OverLight Config (for component return value, static mouse influence for initial render)
555
- const overLightConfig = useMemo(() => {
556
- const mouseInfluence = calculateMouseInfluence(mouseOffset);
557
- const hoverIntensity = isHovered ? 1.4 : 1;
558
- const activeIntensity = isActive ? 1.6 : 1;
619
+ return baseConfig;
620
+ }, [
621
+ overLight,
622
+ getEffectiveOverLight,
623
+ isHovered,
624
+ isActive,
625
+ validateConfigValue,
626
+ debugOverLight,
627
+ ]);
559
628
 
560
- return {
561
- isOverLight: baseOverLightConfig.isOverLight,
562
- threshold: baseOverLightConfig.threshold,
563
- opacity: baseOverLightConfig.opacity * hoverIntensity * activeIntensity,
564
- contrast: Math.min(1.6, baseOverLightConfig.contrast + mouseInfluence * 0.1),
565
- brightness: Math.min(1.1, baseOverLightConfig.brightness + mouseInfluence * 0.05),
566
- saturationBoost: baseOverLightConfig.saturationBoost,
567
- shadowIntensity: Math.min(1.2, Math.max(0.5, baseOverLightConfig.shadowIntensity + mouseInfluence * 0.2)),
568
- borderOpacity: Math.min(1.0, Math.max(0.3, baseOverLightConfig.borderOpacity + mouseInfluence * 0.1)),
569
- };
570
- }, [baseOverLightConfig, mouseOffset, isHovered, isActive]);
629
+ // Transform calculation (static base for React render)
630
+ // Mouse interactions are purely handled by imperative updates in the RAF lerp loop to prevent re-renders
631
+ const transformStyle = useMemo(() => {
632
+ return effectiveWithoutEffects || (isActive && Boolean(onClick)) ? 'scale(0.98)' : 'scale(1)';
633
+ }, [effectiveWithoutEffects, isActive, onClick]);
571
634
 
572
635
  // Mouse tracking
573
636
  const updateRectRef = useRef<number | null>(null);
@@ -575,100 +638,11 @@ export function useAtomixGlass({
575
638
  // Derived values for imperative updates (we can use memoized ones or re-calculate)
576
639
  // Since updateAtomixGlassStyles is called imperatively, we pass current refs and state
577
640
 
578
- // Transform calculations for initial render
579
- const calculateDirectionalScale = useCallback(() => {
580
- const isOverLightActive = baseOverLightConfig.isOverLight;
581
-
582
- if (isOverLightActive) {
583
- return 'scale(1)';
584
- }
585
-
586
- if (!globalMousePosition.x || !globalMousePosition.y || !glassRef.current || !validateGlassSize(glassSize)) {
587
- return 'scale(1)';
588
- }
589
-
590
- const rect = glassRef.current.getBoundingClientRect();
591
- const center = calculateElementCenter(rect);
592
- const deltaX = globalMousePosition.x - center.x;
593
- const deltaY = globalMousePosition.y - center.y;
594
-
595
- const edgeDistanceX = Math.max(0, Math.abs(deltaX) - glassSize.width / 2);
596
- const edgeDistanceY = Math.max(0, Math.abs(deltaY) - glassSize.height / 2);
597
- const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
598
-
599
- if (edgeDistance > CONSTANTS.ACTIVATION_ZONE) {
600
- return 'scale(1)';
601
- }
602
-
603
- const fadeInFactor = 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
604
- const centerDistance = calculateDistance(globalMousePosition, center);
605
-
606
- if (centerDistance === 0) {
607
- return 'scale(1)';
608
- }
609
-
610
- const normalizedX = deltaX / centerDistance;
611
- const normalizedY = deltaY / centerDistance;
612
- const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor;
613
-
614
- const scaleX = 1 + Math.abs(normalizedX) * stretchIntensity * 0.3 - Math.abs(normalizedY) * stretchIntensity * 0.15;
615
- const scaleY = 1 + Math.abs(normalizedY) * stretchIntensity * 0.3 - Math.abs(normalizedX) * stretchIntensity * 0.15;
616
-
617
- return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})`;
618
- }, [globalMousePosition, elasticity, glassSize, glassRef, baseOverLightConfig]);
619
-
620
- const calculateFadeInFactor = useCallback(() => {
621
- if (!globalMousePosition.x || !globalMousePosition.y || !glassRef.current || !validateGlassSize(glassSize)) {
622
- return 0;
623
- }
624
-
625
- const rect = glassRef.current.getBoundingClientRect();
626
- const center = calculateElementCenter(rect);
627
-
628
- const edgeDistanceX = Math.max(0, Math.abs(globalMousePosition.x - center.x) - glassSize.width / 2);
629
- const edgeDistanceY = Math.max(0, Math.abs(globalMousePosition.y - center.y) - glassSize.height / 2);
630
- const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
631
-
632
- return edgeDistance > CONSTANTS.ACTIVATION_ZONE ? 0 : 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
633
- }, [globalMousePosition, glassSize, glassRef]);
634
-
635
- const calculateElasticTranslation = useCallback(() => {
636
- if (!glassRef.current) {
637
- return { x: 0, y: 0 };
638
- }
639
-
640
- const fadeInFactor = calculateFadeInFactor();
641
- const rect = glassRef.current.getBoundingClientRect();
642
- const center = calculateElementCenter(rect);
643
-
644
- return {
645
- x: (globalMousePosition.x - center.x) * elasticity * 0.1 * fadeInFactor,
646
- y: (globalMousePosition.y - center.y) * elasticity * 0.1 * fadeInFactor,
647
- };
648
- }, [globalMousePosition, elasticity, calculateFadeInFactor, glassRef]);
649
-
650
- const elasticTranslation = useMemo(() => {
651
- if (effectiveWithoutEffects) {
652
- return { x: 0, y: 0 };
653
- }
654
- return calculateElasticTranslation();
655
- }, [calculateElasticTranslation, effectiveWithoutEffects]);
656
-
657
- const directionalScale = useMemo(() => {
658
- if (effectiveWithoutEffects) {
659
- return 'scale(1)';
660
- }
661
- return calculateDirectionalScale();
662
- }, [calculateDirectionalScale, effectiveWithoutEffects]);
663
-
664
- const transformStyle = useMemo(() => {
665
- if (effectiveWithoutEffects) {
666
- return isActive && Boolean(onClick) ? 'scale(0.98)' : 'scale(1)';
667
- }
668
- return `translate(${elasticTranslation.x}px, ${elasticTranslation.y}px) ${isActive && Boolean(onClick) ? 'scale(0.96)' : directionalScale}`;
669
- }, [elasticTranslation, isActive, onClick, directionalScale, effectiveWithoutEffects]);
670
641
 
671
642
  // Handle mouse position updates
643
+ // ── Raw mouse handler — writes to TARGET refs only ──────────────────
644
+ // The lerp loop (below) reads the targets and incrementally
645
+ // moves the "current" refs toward them for liquid smoothing.
672
646
  const handleGlobalMousePosition = useCallback(
673
647
  (globalPos: MousePosition) => {
674
648
  if (externalGlobalMousePosition && externalMouseOffset) {
@@ -697,63 +671,113 @@ export function useAtomixGlass({
697
671
 
698
672
  const center = calculateElementCenter(rect);
699
673
 
700
- // Calculate offset relative to this container
701
- const newOffset = {
674
+ // Write raw target the lerp loop will smoothly pursue it
675
+ targetMouseOffsetRef.current = {
702
676
  x: ((globalPos.x - center.x) / rect.width) * 100,
703
677
  y: ((globalPos.y - center.y) / rect.height) * 100,
704
678
  };
705
-
706
- // Store in refs instead of state
707
- internalMouseOffsetRef.current = newOffset;
708
- internalGlobalMousePositionRef.current = globalPos;
709
-
710
- // Imperative style update
711
- updateAtomixGlassStyles(
712
- wrapperRef?.current || null,
713
- glassRef.current,
714
- {
715
- mouseOffset: newOffset,
716
- globalMousePosition: globalPos,
717
- glassSize,
718
- isHovered,
719
- isActive,
720
- isOverLight: baseOverLightConfig.isOverLight,
721
- baseOverLightConfig,
722
- effectiveBorderRadius,
723
- effectiveWithoutEffects,
724
- effectiveReducedMotion,
725
- elasticity,
726
- directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)', // Simplified directional scale for fast path
727
- onClick,
728
- withLiquidBlur,
729
- blurAmount,
730
- saturation,
731
- padding,
732
- }
733
- );
679
+ targetGlobalMousePositionRef.current = globalPos;
734
680
  },
735
681
  [
736
682
  mouseContainer,
737
683
  glassRef,
738
- wrapperRef,
739
684
  externalGlobalMousePosition,
740
685
  externalMouseOffset,
741
686
  effectiveWithoutEffects,
742
- glassSize,
743
- isHovered,
744
- isActive,
745
- baseOverLightConfig,
746
- effectiveBorderRadius,
747
- effectiveReducedMotion,
748
- elasticity,
749
- onClick,
750
- withLiquidBlur,
751
- blurAmount,
752
- saturation,
753
- padding
754
687
  ]
755
688
  );
756
689
 
690
+ // ── Lerp animation loop ─────────────────────────────────────────────
691
+ // Continuously interpolates the current offset toward the target.
692
+ // Produces the signature liquid / viscous feel.
693
+ const startLerpLoop = useCallback(() => {
694
+ if (lerpActiveRef.current) return;
695
+ lerpActiveRef.current = true;
696
+
697
+ const LERP_T = CONSTANTS.LERP_FACTOR; // 0.08 – lower = more viscous
698
+ const EPSILON = 0.05; // Stop iterating when close enough
699
+
700
+ const tick = () => {
701
+ if (!lerpActiveRef.current) return;
702
+
703
+ const cur = internalMouseOffsetRef.current;
704
+ const tgt = targetMouseOffsetRef.current;
705
+
706
+ const dx = tgt.x - cur.x;
707
+ const dy = tgt.y - cur.y;
708
+
709
+ // If we're close enough, snap and park
710
+ if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
711
+ internalMouseOffsetRef.current = { ...tgt };
712
+ internalGlobalMousePositionRef.current = { ...targetGlobalMousePositionRef.current };
713
+ } else {
714
+ internalMouseOffsetRef.current = {
715
+ x: lerp(cur.x, tgt.x, LERP_T),
716
+ y: lerp(cur.y, tgt.y, LERP_T),
717
+ };
718
+ const curG = internalGlobalMousePositionRef.current;
719
+ const tgtG = targetGlobalMousePositionRef.current;
720
+ internalGlobalMousePositionRef.current = {
721
+ x: lerp(curG.x, tgtG.x, LERP_T),
722
+ y: lerp(curG.y, tgtG.y, LERP_T),
723
+ };
724
+ }
725
+
726
+ // Imperative style update with the smoothed values
727
+ updateAtomixGlassStyles(
728
+ wrapperRef?.current || null,
729
+ glassRef.current,
730
+ {
731
+ mouseOffset: internalMouseOffsetRef.current,
732
+ globalMousePosition: internalGlobalMousePositionRef.current,
733
+ glassSize,
734
+ isHovered,
735
+ isActive,
736
+ isOverLight: overLightConfig.isOverLight,
737
+ baseOverLightConfig: overLightConfig,
738
+ effectiveBorderRadius,
739
+ effectiveWithoutEffects,
740
+ effectiveReducedMotion,
741
+ elasticity,
742
+ directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
743
+ onClick,
744
+ withLiquidBlur,
745
+ blurAmount,
746
+ saturation,
747
+ padding,
748
+ }
749
+ );
750
+
751
+ lerpRafRef.current = requestAnimationFrame(tick);
752
+ };
753
+
754
+ lerpRafRef.current = requestAnimationFrame(tick);
755
+ }, [
756
+ glassRef,
757
+ wrapperRef,
758
+ glassSize,
759
+ isHovered,
760
+ isActive,
761
+ overLightConfig,
762
+ effectiveBorderRadius,
763
+ effectiveWithoutEffects,
764
+ effectiveReducedMotion,
765
+ elasticity,
766
+ onClick,
767
+ withLiquidBlur,
768
+ blurAmount,
769
+ saturation,
770
+ padding,
771
+ ]);
772
+
773
+ const stopLerpLoop = useCallback(() => {
774
+ lerpActiveRef.current = false;
775
+ if (lerpRafRef.current !== null) {
776
+ cancelAnimationFrame(lerpRafRef.current);
777
+ lerpRafRef.current = null;
778
+ }
779
+ }, []);
780
+
757
781
  // Subscribe to shared mouse tracker
758
782
  useEffect(() => {
759
783
  if (externalGlobalMousePosition && externalMouseOffset) {
@@ -766,6 +790,9 @@ export function useAtomixGlass({
766
790
 
767
791
  const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
768
792
 
793
+ // Start the lerp loop — it will smoothly chase the target
794
+ startLerpLoop();
795
+
769
796
  const updateRect = () => {
770
797
  if (updateRectRef.current !== null) {
771
798
  cancelAnimationFrame(updateRectRef.current);
@@ -789,6 +816,7 @@ export function useAtomixGlass({
789
816
 
790
817
  return () => {
791
818
  unsubscribe();
819
+ stopLerpLoop();
792
820
  if (updateRectRef.current !== null) {
793
821
  cancelAnimationFrame(updateRectRef.current);
794
822
  updateRectRef.current = null;
@@ -799,6 +827,8 @@ export function useAtomixGlass({
799
827
  };
800
828
  }, [
801
829
  handleGlobalMousePosition,
830
+ startLerpLoop,
831
+ stopLerpLoop,
802
832
  mouseContainer,
803
833
  glassRef,
804
834
  externalGlobalMousePosition,
@@ -817,13 +847,13 @@ export function useAtomixGlass({
817
847
  glassSize,
818
848
  isHovered,
819
849
  isActive,
820
- isOverLight: baseOverLightConfig.isOverLight,
821
- baseOverLightConfig,
850
+ isOverLight: overLightConfig.isOverLight,
851
+ baseOverLightConfig: overLightConfig,
822
852
  effectiveBorderRadius,
823
853
  effectiveWithoutEffects,
824
854
  effectiveReducedMotion,
825
855
  elasticity,
826
- directionalScale,
856
+ directionalScale: isActive && Boolean(onClick) ? 'scale(0.96)' : 'scale(1)',
827
857
  onClick,
828
858
  withLiquidBlur,
829
859
  blurAmount,
@@ -835,12 +865,11 @@ export function useAtomixGlass({
835
865
  isHovered,
836
866
  isActive,
837
867
  glassSize,
838
- baseOverLightConfig,
868
+ overLightConfig,
839
869
  effectiveBorderRadius,
840
870
  effectiveWithoutEffects,
841
871
  effectiveReducedMotion,
842
872
  elasticity,
843
- directionalScale,
844
873
  wrapperRef,
845
874
  glassRef,
846
875
  externalMouseOffset,
@@ -858,9 +887,7 @@ export function useAtomixGlass({
858
887
  const handleMouseDown = useCallback(() => setIsActive(true), []);
859
888
  const handleMouseUp = useCallback(() => setIsActive(false), []);
860
889
 
861
- const handleMouseMove = useCallback((_e: MouseEvent) => {
862
- // Mouse tracking handled by shared global tracker
863
- }, []);
890
+
864
891
 
865
892
  const handleKeyDown = useCallback(
866
893
  (e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -885,14 +912,11 @@ export function useAtomixGlass({
885
912
  globalMousePosition, // This is now static (refs or props) unless prop changes
886
913
  mouseOffset, // This is now static (refs or props) unless prop changes
887
914
  overLightConfig,
888
- elasticTranslation,
889
- directionalScale,
890
915
  transformStyle,
891
916
  handleMouseEnter,
892
917
  handleMouseLeave,
893
918
  handleMouseDown,
894
919
  handleMouseUp,
895
- handleMouseMove,
896
920
  handleKeyDown,
897
921
  };
898
922
  }