Files
www_mycelium_net/src/components/ProxyDetection.tsx

195 lines
4.6 KiB
TypeScript

'use client';
import * as React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
type Props = {
className?: string; // e.g. "w-full h-64"
bg?: string; // defaults to white
};
// Palette (only these)
const ACCENT = '#00b8db';
const STROKE = '#111827';
const GRAY = '#9CA3AF';
const GRAY_LT = '#E5E7EB';
function Magnifier({
x = 0,
y = 0,
flip = false,
delay = 0,
duration = 3,
}: {
x?: number;
y?: number;
flip?: boolean; // rotate handle direction
delay?: number;
duration?: number;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.g
initial={{ x: 0 }}
animate={{ x: [0, 520] }}
transition={{
delay,
duration: prefersReduced ? 0.01 : duration,
ease: [0.22, 1, 0.36, 1],
repeat: prefersReduced ? 0 : Infinity,
repeatType: 'reverse',
repeatDelay: 0.4,
}}
transform={`translate(${x}, ${y})`}
>
{/* glass */}
<circle cx={0} cy={0} r={38} fill="#fff" stroke={STROKE} strokeWidth={6} />
{/* subtle scanning pulse inside the glass */}
<motion.circle
cx={0}
cy={0}
r={26}
fill="none"
stroke={ACCENT}
strokeWidth={4}
initial={{ opacity: 0.15, scale: 0.8 }}
animate={{ opacity: [0.15, 0.35, 0.15], scale: [0.8, 1.05, 0.8] }}
transition={{ duration: 1.6, repeat: Infinity }}
/>
{/* handle */}
<g transform={`rotate(${flip ? 40 : -40}) translate(35, 10)`}>
<rect x={0} y={-6} width={80} height={12} rx={6} fill={STROKE} />
<rect x={0} y={-12} width={14} height={24} rx={6} fill={GRAY} />
</g>
</motion.g>
);
}
function ServerBox({
x,
y,
w = 88,
h = 50,
delay = 0,
accentPulse = false,
}: {
x: number;
y: number;
w?: number;
h?: number;
delay?: number;
accentPulse?: boolean;
}) {
const prefersReduced = useReducedMotion();
return (
<motion.g
transform={`translate(${x}, ${y})`}
initial={{ opacity: 0.6 }}
animate={{
opacity: 1,
}}
transition={{ delay, duration: 0.4 }}
>
{/* outer box */}
<rect
x={-w / 2}
y={-h / 2}
width={w}
height={h}
rx={10}
fill="#fff"
stroke={STROKE}
strokeWidth={3}
/>
{/* top bar */}
<rect
x={-w / 2 + 6}
y={-h / 2 + 8}
width={w - 12}
height={12}
rx={6}
fill={GRAY_LT}
/>
{/* activity line */}
<motion.rect
x={-w / 2 + 10}
y={-h / 2 + 26}
width={w - 20}
height={10}
rx={5}
fill={GRAY_LT}
initial={{ width: w * 0.2 }}
animate={{ width: [w * 0.2, w - 20, w * 0.2] }}
transition={{
delay,
duration: prefersReduced ? 0.01 : 1.8,
repeat: prefersReduced ? 0 : Infinity,
ease: [0.22, 1, 0.36, 1],
}}
/>
{/* “detected” indicator */}
<motion.circle
cx={w / 2 - 14}
cy={h / 2 - 14}
r={6}
fill={accentPulse ? ACCENT : GRAY}
initial={{ scale: 0.9, opacity: 0.8 }}
animate={
accentPulse && !prefersReduced
? { scale: [0.9, 1.15, 0.9], opacity: [0.8, 1, 0.8] }
: { scale: 1, opacity: 0.9 }
}
transition={{ duration: 1.4, repeat: accentPulse && !prefersReduced ? Infinity : 0 }}
/>
</motion.g>
);
}
export default function ProxyDetection({ className, bg = '#ffffff' }: Props) {
// Canvas
const W = 900;
const H = 180;
// Layout: a row of proxy servers
const rowY = H / 2;
const xs = [180, 320, 460, 600, 740];
// Sequence timings so boxes light up as magnifier passes
const delays = [0.8, 0.6, 0.4, 0.2, 0.0];
return (
<div
className={className}
aria-hidden="true"
role="img"
style={{ background: bg }}
>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%">
{/* subtle grid */}
<defs>
<pattern id="grid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke={GRAY_LT} strokeWidth="1" />
</pattern>
</defs>
<rect width={W} height={H} fill="url(#grid)" />
{/* Server row (right -> left sweep) */}
{xs.map((x, i) => (
<ServerBox
key={`b-${i}`}
x={x}
y={rowY}
delay={delays[i]}
accentPulse
/>
))}
{/* Magnifier scanning across the row (opposite direction) */}
<Magnifier x={120} y={rowY} flip={true} delay={0.25} duration={3.2} />
</svg>
</div>
);
}