Files
ogrechkoandClaude Sonnet 5 130c5382cd Initial commit: TopSysOps site + admin panel
Multi-page public site (Next.js), Node/Express backend with Postgres,
and Vite admin/account app, restructured from a single-page layout
into dedicated service, promotions, cases, about, FAQ, and blog pages
for SEO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
2026-08-25 23:12:16 +03:00

57 lines
1.3 KiB
React

'use client';
import { useEffect, useRef, useState } from 'react';
function cx(...args) {
return args.filter(Boolean).join(' ');
}
export default function Reveal({ children, className, delay }) {
const ref = useRef(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node) return undefined;
let reduced = false;
try {
reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
} catch (e) {
// matchMedia unavailable - fall back to animating
}
if (reduced) {
setVisible(true);
return undefined;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
});
},
{ threshold: 0.15 }
);
observer.observe(node);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
style={delay ? { transitionDelay: `${delay}ms` } : undefined}
className={cx(
className,
'transition-all duration-700 ease-out',
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-6'
)}
>
{children}
</div>
);
}