163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
import React, { useRef, useLayoutEffect, useState } from 'react';
|
|
import { Copy, Check, Sprout, Leaf, Wind } from 'lucide-react';
|
|
import gsap from 'gsap';
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
interface TerminalBlockProps {
|
|
code: string;
|
|
language?: string;
|
|
}
|
|
|
|
export const TerminalBlock: React.FC<TerminalBlockProps> = ({ code, language = 'bash' }) => {
|
|
const [copied, setCopied] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const copyBtnRef = useRef<HTMLButtonElement>(null);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(code);
|
|
setCopied(true);
|
|
|
|
// Organic "rustle" animation on click
|
|
if (copyBtnRef.current) {
|
|
gsap.to(copyBtnRef.current, {
|
|
rotate: 15,
|
|
scale: 1.1,
|
|
duration: 0.1,
|
|
yoyo: true,
|
|
repeat: 1,
|
|
ease: "sine.inOut"
|
|
});
|
|
}
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
useLayoutEffect(() => {
|
|
const ctx = gsap.context(() => {
|
|
// Growth animation using a "spring-like" organic entry
|
|
gsap.fromTo(containerRef.current,
|
|
{
|
|
opacity: 0,
|
|
y: 40,
|
|
scale: 0.95,
|
|
skewX: -1
|
|
},
|
|
{
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
skewX: 0,
|
|
duration: 1,
|
|
ease: "elastic.out(1, 0.75)",
|
|
scrollTrigger: {
|
|
trigger: containerRef.current,
|
|
start: "top 92%",
|
|
}
|
|
}
|
|
);
|
|
}, containerRef);
|
|
|
|
return () => ctx.revert();
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
// Using OKLCH based on the primary hue (142.77) for a perfectly matched "dark forest" look
|
|
className="my-10 rounded-[2rem] overflow-hidden border border-primary/20 shadow-2xl relative group bg-[oklch(0.18_0.02_142.77)]"
|
|
style={{
|
|
boxShadow: '0 20px 50px -12px oklch(0.15 0.05 142.77 / 0.5)'
|
|
}}
|
|
>
|
|
{/* Texture Overlay from index.html */}
|
|
<div className="absolute inset-0 grain-texture opacity-10 pointer-events-none" />
|
|
|
|
{/* Nature-inspired Gradient Mask */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 pointer-events-none" />
|
|
|
|
{/* Organic Header - "The Pebble" style */}
|
|
<div className="flex items-center justify-between px-8 py-4 bg-[oklch(0.25_0.03_142.77)]/80 backdrop-blur-md border-b border-white/5 relative z-10">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex space-x-2">
|
|
{/* Organic leaf-shaded dots */}
|
|
<div className="w-2.5 h-2.5 rounded-full bg-primary animate-pulse" style={{ animationDelay: '0s' }}></div>
|
|
<div className="w-2.5 h-2.5 rounded-full bg-accent animate-pulse" style={{ animationDelay: '0.2s' }}></div>
|
|
<div className="w-2.5 h-2.5 rounded-full bg-secondary animate-pulse" style={{ animationDelay: '0.4s' }}></div>
|
|
</div>
|
|
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
|
|
<Sprout size={14} className="text-primary" />
|
|
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-foreground/70">
|
|
{language}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
ref={copyBtnRef}
|
|
onClick={handleCopy}
|
|
className={`
|
|
relative overflow-hidden px-4 py-2 rounded-full transition-all duration-500 flex items-center gap-2 group/btn
|
|
${copied
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-white/5 hover:bg-white/10 text-white/50 hover:text-white'
|
|
}
|
|
`}
|
|
>
|
|
<div className="relative z-10 flex items-center gap-2">
|
|
{copied ? (
|
|
<>
|
|
<Check size={14} className="stroke-[3px]" />
|
|
<span className="text-[10px] font-black uppercase">Copied!</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy size={14} className="group-hover/btn:rotate-12 transition-transform" />
|
|
<span className="text-[10px] font-black uppercase">Copy</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
{/* Hover leaf effect */}
|
|
{!copied && (
|
|
<Wind
|
|
size={16}
|
|
className="absolute -right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover/btn:right-2 group-hover/btn:opacity-20 transition-all duration-500 text-primary"
|
|
/>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Code Area */}
|
|
<div className="p-8 overflow-x-auto relative z-10 custom-scrollbar">
|
|
<pre className="leading-relaxed font-mono selection:bg-primary/40 selection:text-white">
|
|
<code className="text-[oklch(0.95_0.02_142.77)]">
|
|
{code.split('\n').map((line, i) => {
|
|
const isComment = line.trim().startsWith('#');
|
|
const isCommand = line.trim().startsWith('git');
|
|
return (
|
|
<span key={i} className="block relative group/line">
|
|
{/* Subtle line highlight */}
|
|
<div className="absolute -left-8 -right-8 top-0 bottom-0 bg-primary/5 opacity-0 group-hover/line:opacity-100 transition-opacity pointer-events-none" />
|
|
|
|
<span className={isComment ? 'text-primary/50 italic' : isCommand ? 'text-accent font-bold' : ''}>
|
|
{line}
|
|
</span>
|
|
{'\n'}
|
|
</span>
|
|
);
|
|
})}
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
|
|
{/* Decorative Organic Elements */}
|
|
<div className="absolute bottom-4 right-4 opacity-5 group-hover:opacity-20 transition-all duration-1000 transform group-hover:scale-110 group-hover:rotate-6 pointer-events-none">
|
|
<Leaf size={120} className="text-primary" />
|
|
</div>
|
|
|
|
{/* "Growing" accent line at bottom */}
|
|
<div className="absolute bottom-0 left-0 h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent w-full scale-x-0 group-hover:scale-x-100 transition-transform duration-1000 origin-center" />
|
|
</div>
|
|
);
|
|
}; |