All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 27s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 QA (push) Successful in 1m46s
Build & Deploy / 🏗️ Build (push) Successful in 3m38s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m19s
Build & Deploy / 🔔 Notify (push) Successful in 5s
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
'use client';
|
|
|
|
import { useInView, m } from 'framer-motion';
|
|
import { useRef } from 'react';
|
|
|
|
/**
|
|
* StratumLines
|
|
*
|
|
* Abstract horizontal "earth stratum" lines that slide in from opposite sides
|
|
* and form a downward-tapered open shape — evoking excavated earth layers.
|
|
*
|
|
* - On scroll entry: lines slide in with staggered delay, longest at top
|
|
* - After entry: each line gently breathes (oscillates horizontally) forever
|
|
*
|
|
* Place inside any `relative overflow-hidden` section. Zero layout impact.
|
|
*/
|
|
|
|
interface StratumLinesProps {
|
|
/** Which corner to anchor to */
|
|
placement?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
/** Invert colors for light backgrounds */
|
|
invert?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
type StratumLine = {
|
|
width: number; // % of container width
|
|
fromRight: boolean; // which side it slides in from
|
|
delay: number;
|
|
breatheAmp: number; // px oscillation
|
|
breatheDur: number;
|
|
};
|
|
|
|
// The stratum lines — longest at top, tapering like an open excavation
|
|
const LINES: StratumLine[] = [
|
|
{ width: 100, fromRight: false, delay: 0.0, breatheAmp: 4, breatheDur: 5.2 },
|
|
{ width: 88, fromRight: true, delay: 0.1, breatheAmp: 5, breatheDur: 4.7 },
|
|
{ width: 74, fromRight: false, delay: 0.2, breatheAmp: 6, breatheDur: 5.8 },
|
|
{ width: 60, fromRight: true, delay: 0.3, breatheAmp: 5, breatheDur: 4.3 },
|
|
{ width: 46, fromRight: false, delay: 0.4, breatheAmp: 4, breatheDur: 6.1 },
|
|
{ width: 32, fromRight: true, delay: 0.5, breatheAmp: 3, breatheDur: 5.5 },
|
|
{ width: 18, fromRight: false, delay: 0.6, breatheAmp: 2, breatheDur: 4.9 },
|
|
];
|
|
|
|
const PLACEMENT_CLASSES: Record<NonNullable<StratumLinesProps['placement']>, string> = {
|
|
'bottom-left': 'bottom-0 left-0',
|
|
'bottom-right': 'bottom-0 right-0',
|
|
'top-left': 'top-0 left-0',
|
|
'top-right': 'top-0 right-0',
|
|
};
|
|
|
|
export function StratumLines({
|
|
placement = 'bottom-left',
|
|
invert = false,
|
|
className = '',
|
|
}: StratumLinesProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const isInView = useInView(ref, { once: true, margin: '0px 0px -8% 0px' });
|
|
|
|
const color = invert ? '#084c39' : '#0e7a5c';
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={`absolute pointer-events-none select-none ${PLACEMENT_CLASSES[placement]} ${className}`}
|
|
style={{
|
|
width: 'clamp(180px, 24vw, 380px)',
|
|
height: 'clamp(160px, 20vw, 320px)',
|
|
}}
|
|
aria-hidden="true"
|
|
>
|
|
<div className="relative w-full h-full flex flex-col justify-end gap-[10%] pb-[8%]">
|
|
{LINES.map((line, i) => {
|
|
const isFromRight = placement.includes('right') ? !line.fromRight : line.fromRight;
|
|
return (
|
|
<m.div
|
|
key={i}
|
|
style={{
|
|
width: `${line.width}%`,
|
|
height: '1.5px',
|
|
background: color,
|
|
opacity: 0.12 + (i / LINES.length) * 0.28,
|
|
alignSelf: isFromRight ? 'flex-end' : 'flex-start',
|
|
borderRadius: '2px',
|
|
}}
|
|
// Slide in from edge on first view
|
|
initial={{
|
|
x: isFromRight ? 60 : -60,
|
|
opacity: 0,
|
|
}}
|
|
animate={
|
|
isInView
|
|
? {
|
|
// Settle at natural position, then breathe
|
|
x: [
|
|
0,
|
|
isFromRight ? -line.breatheAmp : line.breatheAmp,
|
|
0,
|
|
isFromRight ? -line.breatheAmp * 0.6 : line.breatheAmp * 0.6,
|
|
0,
|
|
],
|
|
opacity: 0.12 + (i / LINES.length) * 0.28,
|
|
}
|
|
: { x: isFromRight ? 60 : -60, opacity: 0 }
|
|
}
|
|
transition={
|
|
isInView
|
|
? {
|
|
x: {
|
|
times: [0, 0.25, 0.5, 0.75, 1],
|
|
duration: line.breatheDur,
|
|
delay: line.delay,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
},
|
|
opacity: {
|
|
duration: 0.7,
|
|
delay: line.delay,
|
|
ease: 'easeOut',
|
|
},
|
|
}
|
|
: { duration: 0.3 }
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|