"use client";
import React, { useState, useEffect, useRef } from "react";
export interface VaultUnlockCardProps {
onSuccess?: () => void;
}
export default function VaultUnlockCard({ onSuccess }: VaultUnlockCardProps) {
const [pin, setPin] = useState("");
const [isUnlocked, setIsUnlocked] = useState(false);
const [isShutterClosed, setIsShutterClosed] = useState(false);
const [isEmpActive, setIsEmpActive] = useState(false);
const [progressWidth, setProgressWidth] = useState(0);
const [isResetOpen, setIsResetOpen] = useState(false);
const [glarePos, setGlarePos] = useState({ x: 50, y: 50 });
const cardRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
const handleGlobalMouseMove = (e: MouseEvent) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const cardCenterX = rect.left + rect.width / 2;
const cardCenterY = rect.top + rect.height / 2;
const deltaX = (e.clientX - cardCenterX) / (window.innerWidth / 2);
const deltaY = (e.clientY - cardCenterY) / (window.innerHeight / 2);
const maxTilt = 9;
const rx = Math.max(-maxTilt, Math.min(maxTilt, -deltaY * maxTilt));
const ry = Math.max(-maxTilt, Math.min(maxTilt, deltaX * maxTilt));
const gx = ((e.clientX - rect.left) / rect.width) * 100;
const gy = ((e.clientY - rect.top) / rect.height) * 100;
setGlarePos({ x: gx, y: gy });
card.style.transform = `perspective(1000px) rotateX(${rx.toFixed(2)}deg) rotateY(${ry.toFixed(2)}deg)`;
};
window.addEventListener("mousemove", handleGlobalMouseMove);
return () => {
window.removeEventListener("mousemove", handleGlobalMouseMove);
};
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (isUnlocked || isShutterClosed) return;
if (e.key === "Backspace") {
setPin((prev) => prev.slice(0, -1));
return;
}
if (/^[0-9]$/.test(e.key)) {
if (pin.length < 4) {
const nextPin = pin + e.key;
setPin(nextPin);
if (nextPin.length === 4) {
triggerUnlockSequence();
}
}
}
};
const triggerUnlockSequence = () => {
setTimeout(() => {
setIsShutterClosed(true);
setTimeout(() => {
setIsEmpActive(true);
setTimeout(() => {
setIsUnlocked(true);
setIsShutterClosed(false);
setIsEmpActive(false);
setTimeout(() => {
setProgressWidth(100);
}, 80);
onSuccess?.();
}, 480);
}, 320);
}, 300);
};
const resetVault = () => {
setPin("");
setIsUnlocked(false);
setIsShutterClosed(false);
setIsEmpActive(false);
setProgressWidth(0);
setTimeout(() => inputRef.current?.focus(), 80);
};
return (
<div
style={{
minHeight: "100vh",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "24px",
backgroundColor: "#03060a",
backgroundImage:
"radial-gradient(circle at 50% 25%, rgba(16, 227, 150, 0.16) 0%, rgba(0, 240, 255, 0.05) 30%, transparent 70%), radial-gradient(ellipse 80% 50% at 50% 90%, rgba(10, 16, 28, 0.85) 0%, transparent 80%)",
userSelect: "none",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
}}
onClick={() => inputRef.current?.focus()}
>
<input
ref={inputRef}
type="password"
inputMode="numeric"
maxLength={4}
value={pin}
onChange={(e) => {
const val = e.target.value.replace(/\D/g, "").slice(0, 4);
setPin(val);
if (val.length === 4) triggerUnlockSequence();
}}
onKeyDown={handleKeyDown}
style={{
position: "absolute",
opacity: 0,
pointerEvents: "none",
top: "-100px",
}}
autoFocus
/>
<div
ref={cardRef}
style={{
maxWidth: "530px",
width: "100%",
borderRadius: "36px",
padding: "24px 44px 34px 44px",
backgroundColor: "rgba(10, 15, 24, 0.65)",
backdropFilter: "blur(24px)",
WebkitBackdropFilter: "blur(24px)",
border: "1px solid rgba(255, 255, 255, 0.12)",
borderTop: "1.5px solid rgba(16, 227, 150, 0.55)",
boxShadow: isEmpActive
? "0 0 50px rgba(16, 227, 150, 0.8), 0 25px 70px -15px rgba(0, 0, 0, 0.8)"
: "0 25px 70px -15px rgba(0, 0, 0, 0.8), 0 0 45px -10px rgba(16, 227, 150, 0.3), inset 0 1px 1px 0 rgba(255, 255, 255, 0.22)",
position: "relative",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
transformStyle: "preserve-3d",
transition: "transform 0.15s ease-out, box-shadow 0.3s ease",
}}
>
<div
style={{
position: "absolute",
inset: 0,
borderRadius: "inherit",
background: `radial-gradient(480px circle at ${glarePos.x}% ${glarePos.y}%, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.02) 40%, transparent 75%)`,
pointerEvents: "none",
mixBlendMode: "screen",
zIndex: 6,
}}
/>
{isShutterClosed && (
<>
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "50%",
background:
"radial-gradient(circle at 50% 100%, #121824 0%, #06080d 100%)",
borderBottom: "2px solid #10e396",
boxShadow: "0 10px 35px rgba(16, 227, 150, 0.6)",
zIndex: 40,
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
paddingBottom: "8px",
}}
>
<div
style={{
fontFamily: "monospace",
fontSize: "10px",
letterSpacing: "0.15em",
color: "#10e396",
fontWeight: "bold",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "#10e396",
}}
/>
SHUTTER ENGAGED // KINETIC SECURE
</div>
</div>
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "50%",
background:
"radial-gradient(circle at 50% 0%, #121824 0%, #06080d 100%)",
borderTop: "2px solid #10e396",
boxShadow: "0 -10px 35px rgba(16, 227, 150, 0.6)",
zIndex: 40,
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
paddingTop: "8px",
}}
>
<div
style={{
fontFamily: "monospace",
fontSize: "9px",
letterSpacing: "0.15em",
color: "rgba(16, 227, 150, 0.8)",
}}
>
QUANTUM CRYPTOGRAPHIC PROTOCOL
</div>
</div>
</>
)}
<div
style={{
width: "48px",
height: "4px",
borderRadius: "9999px",
backgroundColor: "#00f0ff",
boxShadow: "0 0 14px rgba(0, 240, 255, 0.85)",
marginBottom: "28px",
marginTop: "2px",
position: "relative",
zIndex: 10,
}}
/>
{!isUnlocked ? (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
position: "relative",
zIndex: 10,
}}
>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "4px 12px",
borderRadius: "9999px",
backgroundColor: "rgba(255, 255, 255, 0.04)",
border: "1px solid rgba(255, 255, 255, 0.1)",
marginBottom: "12px",
}}
>
<span
style={{
width: "6px",
height: "6px",
borderRadius: "9999px",
backgroundColor: "#00f0ff",
boxShadow: "0 0 8px #00f0ff",
}}
/>
<span
style={{
fontFamily: "monospace",
fontSize: "11px",
fontWeight: "bold",
letterSpacing: "0.08em",
color: "#67e8f9",
}}
>
SKINNY OTP // V4.8
</span>
</div>
<h1
style={{
fontFamily: "'Newsreader', Georgia, serif",
fontStyle: "italic",
fontSize: "33px",
fontWeight: "800",
color: "#ffffff",
letterSpacing: "-0.02em",
marginBottom: "28px",
lineHeight: 1.15,
textShadow: "0 2px 14px rgba(255, 255, 255, 0.2)",
}}
>
Desbloquear Bóveda
</h1>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "16px",
width: "100%",
maxWidth: "380px",
marginBottom: "28px",
cursor: "pointer",
}}
onClick={() => inputRef.current?.focus()}
>
{[0, 1, 2, 3].map((idx) => {
const isFilled = pin.length > idx;
const isActive = pin.length === idx;
return (
<div
key={idx}
style={{
height: "88px",
minHeight: "88px",
borderRadius: "22px",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
overflow: "hidden",
backgroundColor: isActive
? "rgba(255, 255, 255, 0.05)"
: "rgba(255, 255, 255, 0.02)",
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
border: isActive
? "1.5px solid rgba(255, 255, 255, 0.35)"
: "1.5px solid rgba(255, 255, 255, 0.12)",
boxShadow: isActive
? "0 0 20px rgba(255, 255, 255, 0.12), inset 0 2px 6px rgba(0, 0, 0, 0.75)"
: "inset 0 3px 6px rgba(0, 0, 0, 0.75), 0 4px 10px rgba(0, 0, 0, 0.35)",
transition: "all 0.2s ease",
}}
>
{isActive && (
<div
style={{
width: "3px",
height: "32px",
borderRadius: "9999px",
backgroundColor: "#00f0ff",
boxShadow: "0 0 12px #00f0ff, 0 0 24px #00f0ff",
}}
/>
)}
{isFilled && (
<span
style={{
color: "#ffffff",
fontFamily: "monospace",
fontSize: "24px",
fontWeight: "bold",
textShadow: "0 0 12px rgba(0, 240, 255, 0.9)",
}}
>
●
</span>
)}
</div>
);
})}
</div>
<footer style={{ paddingTop: "8px" }}>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsResetOpen(true);
}}
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
fontSize: "13px",
color: "#a1a1aa",
background: "transparent",
border: "none",
outline: "none",
cursor: "pointer",
}}
>
<span
style={{
width: "14px",
height: "14px",
borderRadius: "50%",
border: "1.5px solid rgba(161, 161, 170, 0.7)",
display: "inline-block",
flexShrink: 0,
}}
/>
<span>Restablecer Bóveda (Olvidé mi PIN)</span>
</button>
</footer>
</div>
) : (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "8px 0",
position: "relative",
zIndex: 10,
}}
>
<div
style={{
width: "100px",
height: "100px",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "16px",
}}
>
<div
style={{
position: "absolute",
inset: 0,
borderRadius: "50%",
border: "2px dashed rgba(52, 211, 153, 0.4)",
}}
/>
<div
style={{
position: "absolute",
inset: "8px",
borderRadius: "50%",
border: "1px dotted rgba(6, 182, 212, 0.5)",
}}
/>
<div
style={{
width: "72px",
height: "72px",
borderRadius: "50%",
backgroundColor: "rgba(6, 78, 59, 0.85)",
border: "2px solid #10e396",
boxShadow: "0 0 35px rgba(16, 227, 150, 0.6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 2,
}}
>
<svg
style={{ width: "36px", height: "36px" }}
fill="none"
stroke="#10e396"
strokeWidth="3"
viewBox="0 0 24 24"
>
<path
d="M4.5 12.75l6 6 9-13.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
<div
style={{
width: "100%",
maxWidth: "280px",
height: "8px",
borderRadius: "9999px",
backgroundColor: "rgba(255, 255, 255, 0.04)",
border: "1px solid rgba(16, 227, 150, 0.3)",
padding: "2px",
marginBottom: "14px",
overflow: "hidden",
}}
>
<div
style={{
height: "100%",
width: `${progressWidth}%`,
borderRadius: "9999px",
background:
"linear-gradient(to right, #00f0ff, #10e396, #6ee7b7)",
boxShadow: "0 0 12px #10e396",
transition: "width 0.7s ease",
}}
/>
</div>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "4px 12px",
borderRadius: "9999px",
backgroundColor: "rgba(6, 78, 59, 0.8)",
border: "1px solid rgba(16, 227, 150, 0.5)",
color: "#6ee7b7",
fontSize: "12px",
fontFamily: "monospace",
fontWeight: "bold",
marginBottom: "8px",
boxShadow: "0 0 16px rgba(16, 227, 150, 0.3)",
}}
>
<span
style={{
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: "#10e396",
}}
/>
AUTORIZACIÓN COMPLETA
</div>
<h2
style={{
fontSize: "26px",
fontWeight: "800",
color: "#ffffff",
marginBottom: "20px",
fontFamily: "'Newsreader', Georgia, serif",
fontStyle: "italic",
textShadow: "0 0 12px rgba(16, 227, 150, 0.3)",
}}
>
¡Bóveda Desbloqueada!
</h2>
<button
type="button"
onClick={resetVault}
style={{
width: "100%",
maxWidth: "320px",
padding: "14px 24px",
borderRadius: "16px",
background:
"linear-gradient(to right, #10e396, #2dd4bf, #10e396)",
color: "#020617",
fontWeight: "800",
fontSize: "14px",
boxShadow: "0 0 30px rgba(16, 227, 150, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
cursor: "pointer",
border: "none",
outline: "none",
}}
>
<span>Continuar a la Bóveda</span>
<svg
style={{ width: "16px", height: "16px" }}
fill="none"
stroke="currentColor"
strokeWidth="2.5"
viewBox="0 0 24 24"
>
<path
d="M14 5l7 7m0 0l-7 7m7-7H3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
)}
</div>
{isResetOpen && (
<div
style={{
position: "fixed",
inset: 0,
zIndex: 50,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "16px",
backgroundColor: "rgba(0, 0, 0, 0.75)",
backdropFilter: "blur(20px)",
}}
onClick={() => setIsResetOpen(false)}
>
<div
style={{
maxWidth: "380px",
width: "100%",
borderRadius: "24px",
padding: "28px",
backgroundColor: "rgba(10, 15, 24, 0.95)",
border: "1px solid rgba(255, 255, 255, 0.12)",
boxShadow: "0 25px 60px rgba(0, 0, 0, 0.8)",
textAlign: "left",
}}
onClick={(e) => e.stopPropagation()}
>
<div
style={{
width: "40px",
height: "40px",
borderRadius: "14px",
backgroundColor: "rgba(0, 240, 255, 0.15)",
border: "1px solid rgba(0, 240, 255, 0.3)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#00f0ff",
marginBottom: "16px",
}}
>
<svg
style={{ width: "20px", height: "20px" }}
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h3
style={{
fontSize: "18px",
fontWeight: "bold",
color: "#ffffff",
marginBottom: "6px",
}}
>
Recuperación de PIN
</h3>
<p
style={{
fontSize: "12px",
color: "#a1a1aa",
marginBottom: "16px",
lineHeight: 1.5,
}}
>
Para restaurar tu PIN maestro necesitas tu frase de respaldo de 12
palabras o tu llave de seguridad hardware.
</p>
<button
type="button"
onClick={() => {
setIsResetOpen(false);
resetVault();
}}
style={{
width: "100%",
fontWeight: 600,
padding: "10px 16px",
borderRadius: "12px",
backgroundColor: "rgba(255, 255, 255, 0.1)",
color: "#ffffff",
border: "1px solid rgba(255, 255, 255, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
fontSize: "12px",
cursor: "pointer",
marginBottom: "10px",
}}
>
<span>Usar Frase Semilla de 12 Palabras</span>
<span>→</span>
</button>
<button
type="button"
onClick={() => {
setIsResetOpen(false);
inputRef.current?.focus();
}}
style={{
width: "100%",
padding: "10px 16px",
borderRadius: "12px",
backgroundColor: "transparent",
color: "#a1a1aa",
border: "none",
fontSize: "12px",
cursor: "pointer",
}}
>
Regresar a Desbloqueo
</button>
</div>
</div>
)}
</div>
);
}
export { VaultUnlockCard };