added a little css
This commit is contained in:
parent
d6ae81d11a
commit
06fe062114
137
package-lock.json
generated
137
package-lock.json
generated
@ -10,7 +10,9 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@opennextjs/cloudflare": "^1.10.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
@ -19,6 +21,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "15.5.4",
|
||||
"open-next": "^3.1.3",
|
||||
@ -6009,6 +6012,37 @@
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
|
||||
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
@ -6032,6 +6066,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
|
||||
"integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
@ -6206,6 +6270,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
|
||||
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
@ -10507,6 +10602,33 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.24",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
|
||||
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
@ -12605,6 +12727,21 @@
|
||||
"obliterator": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.23.6",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@opennextjs/cloudflare": "^1.10.1",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
@ -22,6 +24,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "15.5.4",
|
||||
"open-next": "^3.1.3",
|
||||
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 402 KiB |
@ -59,6 +59,12 @@
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-brand: var(--font-bebas);
|
||||
--font-orbitron: var(--font-orbitron);
|
||||
--font-inter: var(--font-inter);
|
||||
--font-jetbrains-mono: var(--font-jetbrains-mono);
|
||||
--font-space-mono: var(--font-space-mono);
|
||||
--font-rajdhani: var(--font-rajdhani);
|
||||
--font-exo-2: var(--font-exo-2);
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
@ -67,7 +73,7 @@
|
||||
body {
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
font-family: var(--font-jetbrains-mono), Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.font-brand {
|
||||
@ -76,3 +82,87 @@ body {
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-orbitron {
|
||||
font-family: var(--font-orbitron), var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.font-inter {
|
||||
font-family: var(--font-inter), Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.font-terminal {
|
||||
font-family: var(--font-jetbrains-mono), monospace;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-space-mono {
|
||||
font-family: var(--font-space-mono), monospace;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-rajdhani {
|
||||
font-family: var(--font-rajdhani), sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-exo-2 {
|
||||
font-family: var(--font-exo-2), sans-serif;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Dot effect for black text - simplified approach */
|
||||
.text-dots {
|
||||
position: relative;
|
||||
color: #000000;
|
||||
background-image:
|
||||
radial-gradient(circle, #000000 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
background-position: 0 0;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Custom dotted border with solid corner dots */
|
||||
.dotted-border-corners {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dotted-border-corners::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px dotted white;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dotted-border-corners::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background:
|
||||
/* Corner dots */
|
||||
radial-gradient(circle, white 1px, transparent 1px) 0 0 / 4px 4px,
|
||||
radial-gradient(circle, white 1px, transparent 1px) 100% 0 / 4px 4px,
|
||||
radial-gradient(circle, white 1px, transparent 1px) 0 100% / 4px 4px,
|
||||
radial-gradient(circle, white 1px, transparent 1px) 100% 100% / 4px 4px;
|
||||
background-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Bebas_Neue } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Bebas_Neue, Orbitron, Inter, JetBrains_Mono, Space_Mono, Rajdhani, Exo_2 } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Navigation } from "@/components/Navigation";
|
||||
import { Footer } from "@/components/Footer";
|
||||
@ -26,6 +26,51 @@ const bebasNeue = Bebas_Neue({
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const orbitron = Orbitron({
|
||||
variable: "--font-orbitron",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-jetbrains-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const spaceMono = Space_Mono({
|
||||
variable: "--font-space-mono",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const rajdhani = Rajdhani({
|
||||
variable: "--font-rajdhani",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const exo2 = Exo_2({
|
||||
variable: "--font-exo-2",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700", "800", "900"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Biohazard VFX - Visual Effects Studio",
|
||||
description: "Creating stunning visual effects for commercials, music videos, and digital media. Expert VFX, motion graphics, compositing, and 3D animation services.",
|
||||
@ -79,7 +124,7 @@ export default function RootLayout({
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${bebasNeue.variable} antialiased bg-black text-white`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${bebasNeue.variable} ${orbitron.variable} ${inter.variable} ${jetbrainsMono.variable} ${spaceMono.variable} ${rajdhani.variable} ${exo2.variable} antialiased bg-black text-white`}
|
||||
>
|
||||
<main className="min-h-screen">
|
||||
{children}
|
||||
|
||||
120
src/components/CursorDotBackground.tsx
Normal file
120
src/components/CursorDotBackground.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
interface CursorDotBackgroundProps {
|
||||
dotSize?: number;
|
||||
dotSpacing?: number;
|
||||
fadeDistance?: number;
|
||||
opacity?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CursorDotBackground({
|
||||
dotSize = 1,
|
||||
dotSpacing = 20,
|
||||
fadeDistance = 100,
|
||||
opacity = 0.3,
|
||||
className = "",
|
||||
}: CursorDotBackgroundProps) {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
setMousePosition({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => setIsHovering(true);
|
||||
const handleMouseLeave = () => setIsHovering(false);
|
||||
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
container.addEventListener("mousemove", handleMouseMove);
|
||||
container.addEventListener("mouseenter", handleMouseEnter);
|
||||
container.addEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener("mousemove", handleMouseMove);
|
||||
container.removeEventListener("mouseenter", handleMouseEnter);
|
||||
container.removeEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Generate dots based on container size
|
||||
const generateDots = () => {
|
||||
if (!containerRef.current) return [];
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
const dots = [];
|
||||
const cols = Math.ceil(width / dotSpacing);
|
||||
const rows = Math.ceil(height / dotSpacing);
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const x = col * dotSpacing;
|
||||
const y = row * dotSpacing;
|
||||
|
||||
// Calculate distance from mouse position
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(x - mousePosition.x, 2) + Math.pow(y - mousePosition.y, 2)
|
||||
);
|
||||
|
||||
// Calculate opacity based on distance and hover state
|
||||
let dotOpacity = 0;
|
||||
if (isHovering && distance <= fadeDistance) {
|
||||
dotOpacity = opacity * (1 - distance / fadeDistance);
|
||||
}
|
||||
|
||||
if (dotOpacity > 0) {
|
||||
dots.push({
|
||||
x,
|
||||
y,
|
||||
opacity: dotOpacity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dots;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px)`,
|
||||
backgroundSize: `${dotSpacing}px ${dotSpacing}px`,
|
||||
}}
|
||||
>
|
||||
{generateDots().map((dot, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-full bg-white transition-opacity duration-150 ease-out"
|
||||
style={{
|
||||
left: dot.x,
|
||||
top: dot.y,
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
opacity: dot.opacity,
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/components/HorizontalAccordion.tsx
Normal file
83
src/components/HorizontalAccordion.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
interface HorizontalAccordionProps {
|
||||
trigger: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HorizontalAccordion({
|
||||
trigger,
|
||||
children,
|
||||
className = ""
|
||||
}: HorizontalAccordionProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-4 ${className}`}>
|
||||
{/* Trigger Button */}
|
||||
<motion.button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors whitespace-nowrap text-lg font-medium"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{trigger}
|
||||
<motion.div
|
||||
animate={{ rotate: isOpen ? 90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
|
||||
{/* Animated Content */}
|
||||
<AnimatePresence mode="wait">
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
y: -10
|
||||
}}
|
||||
animate={{
|
||||
height: "auto",
|
||||
opacity: 1,
|
||||
y: 0
|
||||
}}
|
||||
exit={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
y: -10
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
opacity: { duration: 0.3 }
|
||||
}}
|
||||
className="overflow-hidden w-full"
|
||||
style={{ willChange: "height, opacity, transform" }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -20, opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.15,
|
||||
ease: "easeOut"
|
||||
}}
|
||||
className="w-full max-w-2xl"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,67 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { CursorDotBackground } from "./CursorDotBackground";
|
||||
import { HorizontalAccordion } from "./HorizontalAccordion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function TempPlaceholder() {
|
||||
const titleRef = useRef<HTMLHeadingElement | null>(null);
|
||||
const titleInnerRef = useRef<HTMLSpanElement | null>(null);
|
||||
const bioTextRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [titleWidth, setTitleWidth] = useState<number | null>(null);
|
||||
const [bioFontSizePx, setBioFontSizePx] = useState<number | null>(null);
|
||||
const baseBioFontSizeRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const measure = () => {
|
||||
const measuredTitleWidth = titleInnerRef.current?.offsetWidth ?? null;
|
||||
setTitleWidth(measuredTitleWidth);
|
||||
|
||||
if (measuredTitleWidth && bioTextRef.current) {
|
||||
const element = bioTextRef.current;
|
||||
if (baseBioFontSizeRef.current === null) {
|
||||
const initialFontSize = parseFloat(getComputedStyle(element).fontSize);
|
||||
baseBioFontSizeRef.current = isNaN(initialFontSize) ? 16 : initialFontSize;
|
||||
}
|
||||
|
||||
// Temporarily ensure we measure at base font size
|
||||
const baseFontSize = baseBioFontSizeRef.current ?? 16;
|
||||
const previousInlineFontSize = element.style.fontSize;
|
||||
element.style.fontSize = `${baseFontSize}px`;
|
||||
const bioNaturalWidth = element.offsetWidth;
|
||||
// Restore previous inline style before we set state (will update after render)
|
||||
element.style.fontSize = previousInlineFontSize;
|
||||
|
||||
if (bioNaturalWidth > 0) {
|
||||
const scale = measuredTitleWidth / bioNaturalWidth;
|
||||
setBioFontSizePx(baseFontSize * scale);
|
||||
}
|
||||
}
|
||||
};
|
||||
measure();
|
||||
window.addEventListener("resize", measure);
|
||||
return () => window.removeEventListener("resize", measure);
|
||||
}, []);
|
||||
return (
|
||||
<section className="py-16 bg-black text-white min-h-screen">
|
||||
<div className="container mx-auto px-4 max-w-3xl">
|
||||
<div className="p-8 relative">
|
||||
<CursorDotBackground
|
||||
dotSize={2}
|
||||
dotSpacing={20}
|
||||
fadeDistance={80}
|
||||
opacity={0.5}
|
||||
className="rounded"
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<p className="text-sm text-gray-500 mb-6">10-12-2025</p>
|
||||
|
||||
<h1 className="text-5xl font-bold mb-4 leading-tight">
|
||||
You've gotta be <em className="text-gray-400">fucking</em> me.
|
||||
<h1 ref={titleRef} className="text-5xl font-bold mb-4 leading-tight">
|
||||
<span ref={titleInnerRef} className="inline-block">
|
||||
You've gotta be <em className="text-gray-400">fucking</em> me.
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg mb-2 text-gray-300">This is the 20th fucking time this has happened.</p>
|
||||
<p className="text-lg mb-12 text-gray-400">
|
||||
<p className="text-lg mb-8 text-gray-400">
|
||||
<em>Nicholai broke the website, again.</em>
|
||||
</p>
|
||||
|
||||
<hr className="border-gray-800 mb-4" />
|
||||
<hr className="border-gray-800 mb-8" />
|
||||
<div className="mb-8">
|
||||
<HorizontalAccordion trigger="How did we get here?">
|
||||
<div className="w-full">
|
||||
<p className="mb-4 text-gray-400 text-sm">
|
||||
<em>(TLDR: perfectionism is the mind killer)</em>
|
||||
</p>
|
||||
<ol className="list-decimal list-inside space-y-3 text-gray-300 leading-relaxed break-words">
|
||||
<li>We needed a website (circa January 2023)</li>
|
||||
<li>We tried to build one on squarespace (that shit sucks)</li>
|
||||
<li>
|
||||
Nicholai figured "I know some html and javascript, why not just{" "}
|
||||
<em>make</em> one."
|
||||
</li>
|
||||
<li>
|
||||
But of course, <strong>the html site sucked</strong> and was
|
||||
difficult to host.
|
||||
</li>
|
||||
<li>
|
||||
And naturally, the website for some reason <em>needed</em> to look
|
||||
good.
|
||||
</li>
|
||||
<li>
|
||||
So then began a longwinded journey of Nicholai learning <em>react</em>
|
||||
</li>
|
||||
<li>Nicholai should've stuck to python.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</HorizontalAccordion>
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-lg">
|
||||
<strong>How did we get here?</strong> Lets break it down:
|
||||
</p>
|
||||
<p className="mb-6 text-gray-400">
|
||||
<em>(TLDR: perfectionism is the mind killer)</em>
|
||||
</p>
|
||||
<p className="mb-8 text-gray-400">Anyway, heres all you assholes need for right now:</p>
|
||||
|
||||
<ol className="list-decimal list-inside mb-8 space-y-3 text-gray-300 leading-relaxed">
|
||||
<li>We needed a website (circa January 2023)</li>
|
||||
<li>We tried to build one on squarespace (that shit sucks)</li>
|
||||
<li>
|
||||
Nicholai figured "I know some html and javascript, why not just{" "}
|
||||
<em>make</em> one."
|
||||
</li>
|
||||
<li>
|
||||
But of course, <strong>the html site sucked</strong> and was
|
||||
difficult to host.
|
||||
</li>
|
||||
<li>
|
||||
And naturally, the website for some reason <em>needed</em> to look
|
||||
good.
|
||||
</li>
|
||||
<li>
|
||||
So then began a longwinded journey of Nicholai learning <em>react</em>
|
||||
</li>
|
||||
<li>Nicholai should've stuck to python.</li>
|
||||
</ol>
|
||||
|
||||
<hr className="border-gray-800 mb-10" />
|
||||
|
||||
<p className="mb-10 text-gray-400">Anyway, heres all you assholes need for right now:</p>
|
||||
|
||||
<h1 className="text-6xl font-bold mb-6 tracking-wider">BIOHAZARD VFX</h1>
|
||||
<h1
|
||||
className="text-5xl font-black mb-6 font-exo-2 text-center mx-auto leading-none"
|
||||
style={{
|
||||
color: '#000000',
|
||||
textShadow: '2px 2px 0px #ff4d00, 4px 4px 0px #ff4d00',
|
||||
width: titleWidth ? `${titleWidth}px` : undefined
|
||||
}}
|
||||
>
|
||||
<span ref={bioTextRef} className="inline-block" style={{ fontSize: bioFontSizePx ? `${bioFontSizePx}px` : undefined }}>
|
||||
BIOHAZARD
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mb-8 text-lg text-gray-300">
|
||||
<strong>Who we are:</strong> artists and technical people, we're
|
||||
better at VFX than we are at web design, I promise.
|
||||
</p>
|
||||
|
||||
<hr className="border-gray-800 mb-4" />
|
||||
<hr className="border-gray-800 mb-8" />
|
||||
|
||||
<p className="mb-4 text-lg">
|
||||
<strong>Here's our reel:</strong>{" "}
|
||||
<a
|
||||
href="https://f.io/Wgx3EAHu"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-300 hover:brightness-110 hover:scale-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -79,15 +138,12 @@ export function TempPlaceholder() {
|
||||
<strong>Some projects we've worked on:</strong>
|
||||
</p>
|
||||
|
||||
<hr className="border-gray-800 mb-4" />
|
||||
<hr className="border-gray-800 mb-8" />
|
||||
|
||||
<ul className="list-disc list-inside mb-8 space-y-3 text-gray-300">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.instagram.com/p/DMykfRxssMR/?utm_source=ig_web_copy_link&igsh=Nmg0YXFuOG9yNGE2"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -97,8 +153,8 @@ export function TempPlaceholder() {
|
||||
<li>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=4QIZE708gJ4"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -108,8 +164,8 @@ export function TempPlaceholder() {
|
||||
<li>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=z2tUpLHdd4M"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -119,8 +175,8 @@ export function TempPlaceholder() {
|
||||
<li>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=RCZ9wl1Up40"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -130,8 +186,8 @@ export function TempPlaceholder() {
|
||||
<li>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=yLxoVrFpLmQ"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -141,8 +197,8 @@ export function TempPlaceholder() {
|
||||
<li>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=a2Zqdo9RbNs"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -156,8 +212,8 @@ export function TempPlaceholder() {
|
||||
Here's our{" "}
|
||||
<a
|
||||
href="https://www.instagram.com/biohazardvfx/"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
className="hover:underline transition-all duration-200 hover:brightness-105 inline-block"
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -175,13 +231,15 @@ export function TempPlaceholder() {
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
className="hover:underline"
|
||||
style={{ color: '#ff6b35' }}
|
||||
style={{ color: '#ff4d00' }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
help@biohazardvfx.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
57
src/components/ui/accordion.tsx
Normal file
57
src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
29
src/components/ui/hover-card.tsx
Normal file
29
src/components/ui/hover-card.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
Loading…
x
Reference in New Issue
Block a user