2025-12-07 19:32:53 +02:00
|
|
|
import React from 'react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
interface FadeInOnRevealProps {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
className?: string;
|
2026-01-20 03:23:39 +02:00
|
|
|
skipAnimation?: boolean;
|
2026-03-12 23:45:45 +02:00
|
|
|
forceAnimation?: boolean;
|
|
|
|
|
ignoreContextDisabled?: boolean;
|
|
|
|
|
respectReducedMotion?: boolean;
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
const FADE_ANIMATION_ENABLED = false;
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-01-25 19:16:26 +02:00
|
|
|
// Context to allow parent components (like VirtualMessageList) to disable animations
|
|
|
|
|
// for items entering the viewport due to scrolling rather than new content
|
|
|
|
|
const FadeInDisabledContext = React.createContext(false);
|
|
|
|
|
|
|
|
|
|
export const FadeInDisabledProvider: React.FC<{ disabled: boolean; children: React.ReactNode }> = ({ disabled, children }) => (
|
|
|
|
|
<FadeInDisabledContext.Provider value={disabled}>
|
|
|
|
|
{children}
|
|
|
|
|
</FadeInDisabledContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
export const FadeInOnReveal: React.FC<FadeInOnRevealProps> = ({
|
|
|
|
|
children,
|
|
|
|
|
className,
|
|
|
|
|
skipAnimation,
|
|
|
|
|
forceAnimation = false,
|
|
|
|
|
ignoreContextDisabled = false,
|
|
|
|
|
respectReducedMotion = false,
|
|
|
|
|
}) => {
|
2026-01-25 19:16:26 +02:00
|
|
|
const contextDisabled = React.useContext(FadeInDisabledContext);
|
2026-03-12 23:45:45 +02:00
|
|
|
const reducedMotion =
|
|
|
|
|
respectReducedMotion &&
|
|
|
|
|
typeof window !== 'undefined' &&
|
|
|
|
|
typeof window.matchMedia === 'function' &&
|
|
|
|
|
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
|
const shouldSkip = Boolean(skipAnimation) || (!ignoreContextDisabled && contextDisabled) || reducedMotion;
|
|
|
|
|
const animationEnabled = FADE_ANIMATION_ENABLED || forceAnimation;
|
2026-01-25 19:16:26 +02:00
|
|
|
const [visible, setVisible] = React.useState(shouldSkip);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2026-03-12 23:45:45 +02:00
|
|
|
if (!animationEnabled || shouldSkip) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let frame: number | null = null;
|
|
|
|
|
|
|
|
|
|
const enable = () => setVisible(true);
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
|
|
|
frame = window.requestAnimationFrame(enable);
|
|
|
|
|
} else {
|
|
|
|
|
enable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (
|
|
|
|
|
frame !== null &&
|
|
|
|
|
typeof window !== 'undefined' &&
|
|
|
|
|
typeof window.cancelAnimationFrame === 'function'
|
|
|
|
|
) {
|
|
|
|
|
window.cancelAnimationFrame(frame);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-12 23:45:45 +02:00
|
|
|
}, [animationEnabled, shouldSkip]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-03-12 23:45:45 +02:00
|
|
|
if (!animationEnabled || shouldSkip) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return <>{children}</>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'w-full transition-all duration-300 ease-out',
|
|
|
|
|
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2',
|
|
|
|
|
className
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|