chore: stabilize apps/web (lint, build, typecheck fixes)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,254 +1,298 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
Easing,
|
||||
Img,
|
||||
staticFile,
|
||||
spring,
|
||||
random,
|
||||
} from 'remotion';
|
||||
import { MouseCursor } from '../components/MouseCursor';
|
||||
import { Button } from '@/src/components/Button';
|
||||
import { Loader2, Check, UserCheck, ShieldCheck } from 'lucide-react';
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
Easing,
|
||||
Img,
|
||||
spring,
|
||||
} from "remotion";
|
||||
import { MouseCursor } from "../components/MouseCursor";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { Loader2, Check, ShieldCheck } from "lucide-react";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
// Import logo using the alias setup in remotion.config.ts
|
||||
// We'll use the staticFile helper if it's in public, but these are in src/assets
|
||||
// So we can try to import them directly if the bundler allows, or move them to public.
|
||||
// Given Header.tsx imports them, they should be importable.
|
||||
// import IconWhite from '@/src/assets/logo/Icon White Transparent.svg'; // Not used in this version
|
||||
// Import black logo for light mode
|
||||
import IconBlack from '@/src/assets/logo/Icon Black Transparent.svg';
|
||||
import IconBlack from "@/src/assets/logo/Icon Black Transparent.svg";
|
||||
|
||||
const Background: React.FC<{ loadingOpacity: number }> = ({ loadingOpacity }) => {
|
||||
return (
|
||||
<AbsoluteFill className="bg-white">
|
||||
{/* Website-Matching Grid */}
|
||||
<div className="absolute inset-0">
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="lightGrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#f1f5f9" strokeWidth="1" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#lightGrid)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Subtle Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(255,255,255,0.8)_100%)]" />
|
||||
|
||||
{/* Dynamic "Processing" Rings (Background Activity during loading) */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
style={{ opacity: loadingOpacity * 0.1 }}
|
||||
const Background: React.FC<{ loadingOpacity: number }> = ({
|
||||
loadingOpacity,
|
||||
}) => {
|
||||
return (
|
||||
<AbsoluteFill className="bg-white">
|
||||
{/* Website-Matching Grid */}
|
||||
<div className="absolute inset-0">
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern
|
||||
id="lightGrid"
|
||||
width="40"
|
||||
height="40"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<div className="w-[500px] h-[500px] border border-slate-900 rounded-full animate-[spin_10s_linear_infinite] opacity-20 border-t-transparent" />
|
||||
<div className="absolute w-[400px] h-[400px] border border-slate-900 rounded-full animate-[spin_7s_linear_infinite_reverse] opacity-20 border-b-transparent" />
|
||||
</div>
|
||||
<path
|
||||
d="M 40 0 L 0 0 0 40"
|
||||
fill="none"
|
||||
stroke="#f1f5f9"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#lightGrid)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* STATIC Logo - Strictly no animation on the container */}
|
||||
<div className="absolute top-12 left-12 z-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 flex items-center justify-center bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||
<Img src={IconBlack} className="w-8 h-8 opacity-90" />
|
||||
</div>
|
||||
<div className="flex flex-col opacity-80">
|
||||
<span className="text-slate-900 font-sans font-bold text-lg tracking-tight leading-none">Mintel.me</span>
|
||||
<span className="text-slate-400 font-serif italic text-xs mt-1">Component Library</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
{/* Subtle Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(255,255,255,0.8)_100%)]" />
|
||||
|
||||
{/* Dynamic "Processing" Rings (Background Activity during loading) */}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
style={{ opacity: loadingOpacity * 0.1 }}
|
||||
>
|
||||
<div className="w-[500px] h-[500px] border border-slate-900 rounded-full animate-[spin_10s_linear_infinite] opacity-20 border-t-transparent" />
|
||||
<div className="absolute w-[400px] h-[400px] border border-slate-900 rounded-full animate-[spin_7s_linear_infinite_reverse] opacity-20 border-b-transparent" />
|
||||
</div>
|
||||
|
||||
{/* STATIC Logo - Strictly no animation on the container */}
|
||||
<div className="absolute top-12 left-12 z-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 flex items-center justify-center bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||
<Img src={IconBlack} className="w-8 h-8 opacity-90" />
|
||||
</div>
|
||||
<div className="flex flex-col opacity-80">
|
||||
<span className="text-slate-900 font-sans font-bold text-lg tracking-tight leading-none">
|
||||
Mintel.me
|
||||
</span>
|
||||
<span className="text-slate-400 font-serif italic text-xs mt-1">
|
||||
Component Library
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Toast Notification Component
|
||||
const Toast: React.FC<{ show: boolean; text: string }> = ({ show, text }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const Toast: React.FC<{ _show: boolean; _text: string }> = ({
|
||||
_show,
|
||||
_text,
|
||||
}) => {
|
||||
const _frame = useCurrentFrame();
|
||||
const _fps = useVideoConfig();
|
||||
|
||||
// Animate in/out based on 'show' prop would require state tracking or precise frame logic
|
||||
// We'll trust the parent to mount/unmount or pass an animatable value
|
||||
// For video, deterministic frame-based spring is best.
|
||||
|
||||
// We'll actually control position purely by parent for simplicity in this demo context
|
||||
return (
|
||||
<div className="flex items-center gap-3 bg-slate-900 text-white px-6 py-4 rounded-xl shadow-2xl border border-slate-800">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center text-green-400">
|
||||
<ShieldCheck size={18} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm tracking-wide">Authentication Successful</span>
|
||||
<span className="text-slate-400 text-xs font-medium">Access granted to secure portal</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-3 bg-slate-900 text-white px-6 py-4 rounded-xl shadow-2xl border border-slate-800">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center text-green-400">
|
||||
<ShieldCheck size={18} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm tracking-wide">
|
||||
Authentication Successful
|
||||
</span>
|
||||
<span className="text-slate-400 text-xs font-medium">
|
||||
Access granted to secure portal
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonShowcase: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height, fps } = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
const { width, height, fps } = useVideoConfig();
|
||||
|
||||
// ---- SEQUENCE TIMELINE (300 frames / 5s) ----
|
||||
const ENTER_START = 20;
|
||||
const HOVER_START = 70;
|
||||
const CLICK_FRAME = 90;
|
||||
const LOADING_START = 95;
|
||||
const SUCCESS_START = 180;
|
||||
const TOAST_START = 190;
|
||||
const TOAST_END = 260;
|
||||
const EXIT_START = 220;
|
||||
// ---- SEQUENCE TIMELINE (300 frames / 5s) ----
|
||||
const ENTER_START = 20;
|
||||
const HOVER_START = 70;
|
||||
const CLICK_FRAME = 90;
|
||||
const LOADING_START = 95;
|
||||
const SUCCESS_START = 180;
|
||||
const TOAST_START = 190;
|
||||
const TOAST_END = 260;
|
||||
const EXIT_START = 220;
|
||||
|
||||
// 1. Mouse Animation (Bézier Path)
|
||||
const getMousePos = (f: number) => {
|
||||
const startX = width * 1.2;
|
||||
const startY = height * 1.2;
|
||||
const targetX = width / 2;
|
||||
const targetY = height / 2;
|
||||
// 1. Mouse Animation (Bézier Path)
|
||||
const getMousePos = (f: number) => {
|
||||
const startX = width * 1.2;
|
||||
const startY = height * 1.2;
|
||||
const targetX = width / 2;
|
||||
const targetY = height / 2;
|
||||
|
||||
if (f < ENTER_START) return { x: startX, y: startY, vx: 0 };
|
||||
if (f < ENTER_START) return { x: startX, y: startY, vx: 0 };
|
||||
|
||||
// Approach
|
||||
if (f < HOVER_START) {
|
||||
const t = interpolate(f, [ENTER_START, HOVER_START], [0, 1], { extrapolateRight: 'clamp' });
|
||||
const ease = Easing.bezier(0.16, 1, 0.3, 1)(t);
|
||||
const x = interpolate(ease, [0, 1], [startX, targetX]);
|
||||
const y = interpolate(ease, [0, 1], [startY, targetY]);
|
||||
return { x, y, vx: -10 }; // Approximate Velocity
|
||||
}
|
||||
|
||||
// Hover
|
||||
if (f < EXIT_START) {
|
||||
const noise = Math.sin(f * 0.1) * 2;
|
||||
return { x: targetX + noise, y: targetY + noise, vx: 0 };
|
||||
}
|
||||
|
||||
// Exit
|
||||
const t = interpolate(f, [EXIT_START, EXIT_START + 30], [0, 1], { extrapolateLeft: 'clamp' });
|
||||
const ease = Easing.exp(t);
|
||||
return {
|
||||
x: interpolate(ease, [0, 1], [targetX, width * 0.9]),
|
||||
y: interpolate(ease, [0, 1], [targetY, -100]),
|
||||
vx: 10
|
||||
};
|
||||
};
|
||||
|
||||
const { x: mouseX, y: mouseY, vx } = getMousePos(frame);
|
||||
|
||||
// 3D Cursor Skew
|
||||
const cursorSkew = interpolate(vx, [-20, 20], [20, -20], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
|
||||
const isClicking = frame >= CLICK_FRAME && frame < CLICK_FRAME + 8;
|
||||
const clickRotate = isClicking ? 30 : 0;
|
||||
|
||||
// 2. Button State Logic
|
||||
const isLoading = frame >= LOADING_START && frame < SUCCESS_START;
|
||||
const isSuccess = frame >= SUCCESS_START;
|
||||
|
||||
// Loading Spinner Rotation
|
||||
const spinnerRot = interpolate(frame, [LOADING_START, SUCCESS_START], [0, 720]);
|
||||
|
||||
// Button Scale Physics
|
||||
const pressSpring = spring({ frame: frame - CLICK_FRAME, fps, config: { stiffness: 400, damping: 20 } });
|
||||
const successSpring = spring({ frame: frame - SUCCESS_START, fps, config: { stiffness: 200, damping: 15 } });
|
||||
|
||||
// Morph scale: Click(Compress) -> Loading(Normal) -> Success(Pop)
|
||||
let buttonScale = 1;
|
||||
if (frame >= CLICK_FRAME && frame < LOADING_START) {
|
||||
buttonScale = 1 - (pressSpring * 0.05);
|
||||
} else if (isSuccess) {
|
||||
buttonScale = 1 + (successSpring * 0.05);
|
||||
// Approach
|
||||
if (f < HOVER_START) {
|
||||
const t = interpolate(f, [ENTER_START, HOVER_START], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const ease = Easing.bezier(0.16, 1, 0.3, 1)(t);
|
||||
const x = interpolate(ease, [0, 1], [startX, targetX]);
|
||||
const y = interpolate(ease, [0, 1], [startY, targetY]);
|
||||
return { x, y, vx: -10 }; // Approximate Velocity
|
||||
}
|
||||
|
||||
// Button Width Morph (Optional: Make it circle on load? Keeping it wide for consistency is safer)
|
||||
// Hover
|
||||
if (f < EXIT_START) {
|
||||
const noise = Math.sin(f * 0.1) * 2;
|
||||
return { x: targetX + noise, y: targetY + noise, vx: 0 };
|
||||
}
|
||||
|
||||
// 3. Toast Animation
|
||||
const toastSpring = spring({ frame: frame - TOAST_START, fps, config: { stiffness: 100, damping: 15 } });
|
||||
const toastExit = spring({ frame: frame - TOAST_END, fps, config: { stiffness: 100, damping: 20 } });
|
||||
const toastY = interpolate(toastSpring, [0, 1], [100, -80]) + interpolate(toastExit, [0, 1], [0, 200]);
|
||||
const toastOpacity = interpolate(toastSpring, [0, 1], [0, 1]) - interpolate(toastExit, [0, 0.5], [0, 1]);
|
||||
// Exit
|
||||
const t = interpolate(f, [EXIT_START, EXIT_START + 30], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
});
|
||||
const ease = Easing.exp(t);
|
||||
return {
|
||||
x: interpolate(ease, [0, 1], [targetX, width * 0.9]),
|
||||
y: interpolate(ease, [0, 1], [targetY, -100]),
|
||||
vx: 10,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center overflow-hidden bg-white">
|
||||
<Background loadingOpacity={isLoading ? 1 : 0} />
|
||||
const { x: mouseX, y: mouseY, vx } = getMousePos(frame);
|
||||
|
||||
{/* Main Stage */}
|
||||
<div style={{ perspective: '1000px', zIndex: 10 }}>
|
||||
{/* Button Container */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${buttonScale})`,
|
||||
transition: 'transform 0.1s'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
href="#"
|
||||
variant="primary"
|
||||
showArrow={!isLoading && !isSuccess}
|
||||
className={`
|
||||
// 3D Cursor Skew
|
||||
const cursorSkew = interpolate(vx, [-20, 20], [20, -20], {
|
||||
extrapolateRight: "clamp",
|
||||
extrapolateLeft: "clamp",
|
||||
});
|
||||
const isClicking = frame >= CLICK_FRAME && frame < CLICK_FRAME + 8;
|
||||
const clickRotate = isClicking ? 30 : 0;
|
||||
|
||||
// 2. Button State Logic
|
||||
const isLoading = frame >= LOADING_START && frame < SUCCESS_START;
|
||||
const isSuccess = frame >= SUCCESS_START;
|
||||
|
||||
// Loading Spinner Rotation
|
||||
const spinnerRot = interpolate(
|
||||
frame,
|
||||
[LOADING_START, SUCCESS_START],
|
||||
[0, 720],
|
||||
);
|
||||
|
||||
// Button Scale Physics
|
||||
const pressSpring = spring({
|
||||
frame: frame - CLICK_FRAME,
|
||||
fps,
|
||||
config: { stiffness: 400, damping: 20 },
|
||||
});
|
||||
const successSpring = spring({
|
||||
frame: frame - SUCCESS_START,
|
||||
fps,
|
||||
config: { stiffness: 200, damping: 15 },
|
||||
});
|
||||
|
||||
// Morph scale: Click(Compress) -> Loading(Normal) -> Success(Pop)
|
||||
let buttonScale = 1;
|
||||
if (frame >= CLICK_FRAME && frame < LOADING_START) {
|
||||
buttonScale = 1 - pressSpring * 0.05;
|
||||
} else if (isSuccess) {
|
||||
buttonScale = 1 + successSpring * 0.05;
|
||||
}
|
||||
|
||||
// 3. Toast Animation
|
||||
const toastSpring = spring({
|
||||
frame: frame - TOAST_START,
|
||||
fps,
|
||||
config: { stiffness: 100, damping: 15 },
|
||||
});
|
||||
const toastExit = spring({
|
||||
frame: frame - TOAST_END,
|
||||
fps,
|
||||
config: { stiffness: 100, damping: 20 },
|
||||
});
|
||||
const toastY =
|
||||
interpolate(toastSpring, [0, 1], [100, -80]) +
|
||||
interpolate(toastExit, [0, 1], [0, 200]);
|
||||
const toastOpacity =
|
||||
interpolate(toastSpring, [0, 1], [0, 1]) -
|
||||
interpolate(toastExit, [0, 0.5], [0, 1]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="items-center justify-center overflow-hidden bg-white">
|
||||
<Background loadingOpacity={isLoading ? 1 : 0} />
|
||||
|
||||
{/* Main Stage */}
|
||||
<div style={{ perspective: "1000px", zIndex: 10 }}>
|
||||
{/* Button Container */}
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${buttonScale})`,
|
||||
transition: "transform 0.1s",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
href="#"
|
||||
variant="primary"
|
||||
showArrow={!isLoading && !isSuccess}
|
||||
className={`
|
||||
!transition-all !duration-500
|
||||
!px-16 !py-8 !text-2xl !font-bold
|
||||
${isSuccess
|
||||
? '!bg-white !text-slate-900 !border-slate-900 shadow-none' // Success: Outline/Minimal
|
||||
: '!bg-slate-900 !text-white !border-none !shadow-2xl !shadow-slate-300' // Default/Load: Solid
|
||||
${
|
||||
isSuccess
|
||||
? "!bg-white !text-slate-900 !border-slate-900 shadow-none" // Success: Outline/Minimal
|
||||
: "!bg-slate-900 !text-white !border-none !shadow-2xl !shadow-slate-300" // Default/Load: Solid
|
||||
}
|
||||
!rounded-full
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-4 min-w-[240px] justify-center text-center">
|
||||
{/* Default State */}
|
||||
{!isLoading && !isSuccess && (
|
||||
<span className="animate-fade-in">Start Verification</span>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4 min-w-[240px] justify-center text-center">
|
||||
{/* Default State */}
|
||||
{!isLoading && !isSuccess && (
|
||||
<span className="animate-fade-in">Start Verification</span>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Loader2
|
||||
className="animate-spin text-slate-400"
|
||||
size={32}
|
||||
style={{ transform: `rotate(${spinnerRot}deg)` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success State */}
|
||||
{isSuccess && (
|
||||
<div className="flex items-center gap-3 animate-slide-up text-slate-900">
|
||||
<Check size={28} strokeWidth={3} />
|
||||
<span>Verified</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Loader2
|
||||
className="animate-spin text-slate-400"
|
||||
size={32}
|
||||
style={{ transform: `rotate(${spinnerRot}deg)` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast Notification Layer - Bottom Center */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 flex justify-center pb-20"
|
||||
style={{
|
||||
transform: `translateY(${toastY}px)`,
|
||||
opacity: Math.max(0, toastOpacity)
|
||||
}}
|
||||
>
|
||||
<Toast show={true} text="" />
|
||||
{/* Success State */}
|
||||
{isSuccess && (
|
||||
<div className="flex items-center gap-3 animate-slide-up text-slate-900">
|
||||
<Check size={28} strokeWidth={3} />
|
||||
<span>Verified</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3D Cursor */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0,
|
||||
transform: `translate(${mouseX}px, ${mouseY}px) skewX(${cursorSkew}deg) rotateX(${clickRotate}deg)`,
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
<MouseCursor x={0} y={0} isClicking={isClicking} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
{/* Toast Notification Layer - Bottom Center */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 flex justify-center pb-20"
|
||||
style={{
|
||||
transform: `translateY(${toastY}px)`,
|
||||
opacity: Math.max(0, toastOpacity),
|
||||
}}
|
||||
>
|
||||
<Toast _show={true} _text="" />
|
||||
</div>
|
||||
|
||||
{/* 3D Cursor */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
transform: `translate(${mouseX}px, ${mouseY}px) skewX(${cursorSkew}deg) rotateX(${clickRotate}deg)`,
|
||||
zIndex: 1000,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<MouseCursor x={0} y={0} isClicking={isClicking} />
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user