diff --git a/src/app/globals.css b/src/app/globals.css
index 69cf256..22eb91b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -119,12 +119,8 @@
@layer base {
* {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
- html {
- @apply font-sans;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
}
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 0cc9a68..9ffecdd 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,24 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Space_Grotesk, Space_Mono } from "next/font/google";
import "./globals.css";
+import "./site.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+const spaceGrotesk = Space_Grotesk({
+ variable: "--font-space-grotesk",
subsets: ["latin"],
+ weight: ["300", "400", "500", "600", "700"],
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const spaceMono = Space_Mono({
+ variable: "--font-space-mono",
subsets: ["latin"],
+ weight: ["400", "700"],
});
export const metadata: Metadata = {
- title: "Website Clone",
- description: "Pixel-perfect website clone",
+ title: "3D Web Prompts — Build Stunning 3D Sites",
+ description:
+ "Ready-to-use prompts for Three.js, CSS 3D, Canvas, Spline, and GSAP. Copy a prompt, paste it into Claude or ChatGPT, and ship your 3D hero in minutes.",
};
export default function RootLayout({
@@ -25,9 +29,9 @@ export default function RootLayout({
return (
-
{children}
+ {children}
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index f3a0f80..0644d8b 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,31 @@
+import { Divider } from "@/components/divider";
+import { Footer } from "@/components/footer";
+import { HeroSection } from "@/components/hero-section";
+import { HowItWorksSection } from "@/components/how-it-works-section";
+import { Navigation } from "@/components/navigation";
+import { Orbs } from "@/components/orbs";
+import { ParticleCanvas } from "@/components/particle-canvas";
+import { PromptGrid } from "@/components/prompt-grid";
+import { TipsSection } from "@/components/tips-section";
+import prompts from "@/data/prompts.json";
+import type { Prompt } from "@/types/prompt";
+
export default function Home() {
+ const promptData = prompts as Prompt[];
+
return (
-
-
- Clone target not yet built. Run /clone-website to start.
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/src/app/site.css b/src/app/site.css
new file mode 100644
index 0000000..0fa2489
--- /dev/null
+++ b/src/app/site.css
@@ -0,0 +1,665 @@
+/* 3D Web Prompts — ported site styles */
+
+:root {
+ --bg: #050508;
+ --surface: #0d0d14;
+ --border: rgba(255, 255, 255, 0.08);
+ --accent: #7c6dfa;
+ --accent2: #fa6d8a;
+ --accent3: #6dfacc;
+ --text: #e8e8f0;
+ --muted: #7070a0;
+ --card: #0f0f1a;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-space-grotesk), sans-serif;
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+#hero-canvas {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ pointer-events: none;
+}
+
+nav {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 48px;
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-bottom: 1px solid var(--border);
+ background: rgba(5, 5, 8, 0.6);
+}
+
+.logo {
+ font-family: var(--font-space-mono), monospace;
+ font-size: 14px;
+ letter-spacing: 0.08em;
+ color: var(--text);
+ text-decoration: none;
+}
+
+.logo span {
+ color: var(--accent);
+}
+
+nav ul {
+ list-style: none;
+ display: flex;
+ gap: 32px;
+}
+
+nav ul a {
+ color: var(--muted);
+ text-decoration: none;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ transition: color 0.2s;
+}
+
+nav ul a:hover {
+ color: var(--text);
+}
+
+.hero {
+ position: relative;
+ z-index: 10;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ padding: 80px 24px 0;
+}
+
+.hero-eyebrow {
+ font-family: var(--font-space-mono), monospace;
+ font-size: 11px;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.hero-eyebrow::before,
+.hero-eyebrow::after {
+ content: "";
+ width: 32px;
+ height: 1px;
+ background: var(--accent);
+ opacity: 0.5;
+}
+
+.hero h1 {
+ font-size: clamp(48px, 8vw, 108px);
+ font-weight: 700;
+ line-height: 0.95;
+ letter-spacing: -0.03em;
+ margin-bottom: 28px;
+}
+
+.hero h1 .line1 {
+ display: block;
+ color: var(--text);
+}
+
+.hero h1 .line2 {
+ display: block;
+ background: linear-gradient(
+ 135deg,
+ var(--accent) 0%,
+ var(--accent2) 50%,
+ var(--accent3) 100%
+ );
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.hero p {
+ font-size: clamp(16px, 2vw, 20px);
+ color: var(--muted);
+ max-width: 540px;
+ line-height: 1.65;
+ margin-bottom: 48px;
+}
+
+.hero-actions {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-family: var(--font-space-grotesk), sans-serif;
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+ transition:
+ opacity 0.2s,
+ transform 0.2s;
+ text-decoration: none;
+ display: inline-block;
+}
+
+.btn-primary:hover {
+ opacity: 0.85;
+ transform: translateY(-2px);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-family: var(--font-space-grotesk), sans-serif;
+ font-size: 15px;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ border-color 0.2s,
+ background 0.2s;
+ text-decoration: none;
+ display: inline-block;
+}
+
+.btn-ghost:hover {
+ border-color: var(--accent);
+ background: rgba(124, 109, 250, 0.08);
+}
+
+.scroll-hint {
+ position: absolute;
+ bottom: 36px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ font-family: var(--font-space-mono), monospace;
+ animation: bobble 2s ease-in-out infinite;
+}
+
+.scroll-hint svg {
+ opacity: 0.5;
+}
+
+.section {
+ position: relative;
+ z-index: 10;
+ padding: 80px 48px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.section-label {
+ font-family: var(--font-space-mono), monospace;
+ font-size: 11px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--accent3);
+ margin-bottom: 16px;
+}
+
+.section-title {
+ font-size: clamp(28px, 4vw, 48px);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ margin-bottom: 16px;
+}
+
+.section-sub {
+ font-size: 17px;
+ color: var(--muted);
+ max-width: 520px;
+ line-height: 1.6;
+ margin-bottom: 56px;
+}
+
+.prompt-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ gap: 20px;
+}
+
+.prompt-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 28px;
+ transition:
+ border-color 0.25s,
+ transform 0.25s,
+ box-shadow 0.25s;
+ cursor: default;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.prompt-card:hover {
+ border-color: var(--accent);
+ transform: translateY(-4px);
+ box-shadow: 0 20px 60px rgba(124, 109, 250, 0.12);
+}
+
+.card-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-family: var(--font-space-mono), monospace;
+ font-size: 10px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ padding: 4px 10px;
+ border-radius: 4px;
+ width: fit-content;
+}
+
+.tag-three {
+ background: rgba(124, 109, 250, 0.15);
+ color: var(--accent);
+}
+
+.tag-css {
+ background: rgba(250, 109, 138, 0.15);
+ color: var(--accent2);
+}
+
+.tag-canvas {
+ background: rgba(109, 250, 204, 0.15);
+ color: var(--accent3);
+}
+
+.tag-spline {
+ background: rgba(250, 200, 100, 0.15);
+ color: #fac864;
+}
+
+.tag-gsap {
+ background: rgba(100, 200, 250, 0.15);
+ color: #64c8fa;
+}
+
+.card-title {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.card-desc {
+ font-size: 14px;
+ color: var(--muted);
+ line-height: 1.6;
+ flex: 1;
+}
+
+.card-prompt {
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 8px;
+ padding: 14px 16px;
+ font-family: var(--font-space-mono), monospace;
+ font-size: 12px;
+ color: #b0b0d0;
+ line-height: 1.7;
+ white-space: pre-wrap;
+ word-break: break-word;
+ user-select: none;
+}
+
+.copy-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--muted);
+ padding: 9px 18px;
+ border-radius: 6px;
+ font-family: var(--font-space-grotesk), sans-serif;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ align-self: flex-start;
+ margin-top: 4px;
+}
+
+.copy-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.copy-btn.copied {
+ border-color: var(--accent3);
+ color: var(--accent3);
+ animation: successPulse 0.6s ease;
+}
+
+.divider {
+ position: relative;
+ z-index: 10;
+ width: 100%;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, var(--border), transparent);
+ margin: 0 auto;
+ max-width: 1200px;
+}
+
+.how-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 2px;
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ overflow: hidden;
+}
+
+.how-step {
+ background: var(--card);
+ padding: 36px 28px;
+ transition: background 0.2s;
+}
+
+.how-step:hover {
+ background: rgba(124, 109, 250, 0.06);
+}
+
+.step-num {
+ font-family: var(--font-space-mono), monospace;
+ font-size: 11px;
+ color: var(--accent);
+ letter-spacing: 0.12em;
+ margin-bottom: 20px;
+}
+
+.step-title {
+ font-size: 17px;
+ font-weight: 600;
+ margin-bottom: 10px;
+ letter-spacing: -0.01em;
+}
+
+.step-desc {
+ font-size: 13px;
+ color: var(--muted);
+ line-height: 1.6;
+}
+
+footer {
+ position: relative;
+ z-index: 10;
+ border-top: 1px solid var(--border);
+ padding: 32px 48px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.footer-logo {
+ font-family: var(--font-space-mono), monospace;
+ font-size: 13px;
+}
+
+.footer-logo span {
+ color: var(--accent);
+}
+
+.orb {
+ position: fixed;
+ border-radius: 50%;
+ filter: blur(80px);
+ pointer-events: none;
+ z-index: 1;
+ animation: drift 12s ease-in-out infinite;
+}
+
+.orb-1 {
+ width: 500px;
+ height: 500px;
+ background: rgba(124, 109, 250, 0.18);
+ top: -100px;
+ left: -100px;
+ animation-delay: 0s;
+}
+
+.orb-2 {
+ width: 400px;
+ height: 400px;
+ background: rgba(250, 109, 138, 0.12);
+ bottom: 20%;
+ right: -80px;
+ animation-delay: -4s;
+}
+
+.orb-3 {
+ width: 300px;
+ height: 300px;
+ background: rgba(109, 250, 204, 0.1);
+ top: 40%;
+ left: 30%;
+ animation-delay: -8s;
+}
+
+@keyframes bobble {
+ 0%,
+ 100% {
+ transform: translateX(-50%) translateY(0);
+ }
+ 50% {
+ transform: translateX(-50%) translateY(6px);
+ }
+}
+
+@keyframes drift {
+ 0%,
+ 100% {
+ transform: translate(0, 0);
+ }
+ 33% {
+ transform: translate(30px, -20px);
+ }
+ 66% {
+ transform: translate(-20px, 30px);
+ }
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(24px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero-eyebrow {
+ animation: fadeInUp 0.6s ease both;
+ animation-delay: 0.1s;
+}
+
+.hero h1 {
+ animation: fadeInUp 0.6s ease both;
+ animation-delay: 0.2s;
+}
+
+.hero p {
+ animation: fadeInUp 0.6s ease both;
+ animation-delay: 0.35s;
+}
+
+.hero-actions {
+ animation: fadeInUp 0.6s ease both;
+ animation-delay: 0.5s;
+}
+
+.reveal {
+ opacity: 0;
+ transform: translateY(32px);
+ transition:
+ opacity 0.6s ease,
+ transform 0.6s ease;
+}
+
+.reveal.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+@keyframes successPulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(109, 250, 204, 0.4);
+ }
+ 70% {
+ box-shadow: 0 0 0 8px rgba(109, 250, 204, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(109, 250, 204, 0);
+ }
+}
+
+@media (max-width: 1024px) {
+ .section {
+ padding: 64px 32px;
+ }
+
+ .prompt-grid {
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ }
+}
+
+@media (max-width: 768px) {
+ nav {
+ padding: 16px 20px;
+ }
+
+ nav ul {
+ display: none;
+ }
+
+ .hero {
+ padding-top: 72px;
+ }
+
+ .hero-actions {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .btn-primary,
+ .btn-ghost {
+ width: 100%;
+ max-width: 280px;
+ text-align: center;
+ }
+
+ .section {
+ padding: 56px 20px;
+ }
+
+ .section-sub {
+ font-size: 15px;
+ margin-bottom: 36px;
+ }
+
+ .prompt-grid,
+ .how-grid {
+ grid-template-columns: 1fr;
+ }
+
+ footer {
+ flex-direction: column;
+ gap: 12px;
+ text-align: center;
+ padding: 24px 20px;
+ }
+
+ .orb-1 {
+ width: 300px;
+ height: 300px;
+ }
+
+ .orb-2 {
+ width: 240px;
+ height: 240px;
+ }
+
+ .orb-3 {
+ width: 180px;
+ height: 180px;
+ }
+}
+
+@media (max-width: 480px) {
+ .hero h1 {
+ font-size: clamp(36px, 12vw, 56px);
+ }
+
+ .card-prompt {
+ font-size: 11px;
+ }
+
+ .how-step {
+ padding: 24px 20px;
+ }
+}
+
+@media (min-width: 1440px) {
+ .section {
+ padding: 100px 64px;
+ }
+
+ .prompt-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/src/components/copy-button.tsx b/src/components/copy-button.tsx
new file mode 100644
index 0000000..c6fdbc2
--- /dev/null
+++ b/src/components/copy-button.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useState } from "react";
+
+function CopyIcon() {
+ return (
+
+
+
+
+ );
+}
+
+function CheckIcon() {
+ return (
+
+
+
+ );
+}
+
+interface CopyButtonProps {
+ text: string;
+}
+
+export function CopyButton({ text }: CopyButtonProps) {
+ const [copied, setCopied] = useState(false);
+ const [error, setError] = useState(false);
+
+ async function handleCopy() {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setError(false);
+ window.setTimeout(() => setCopied(false), 2000);
+ } catch {
+ setError(true);
+ window.setTimeout(() => setError(false), 2000);
+ }
+ }
+
+ return (
+
+ {error ? (
+ "Select & copy manually"
+ ) : copied ? (
+ <>
+
+ Copied!
+ >
+ ) : (
+ <>
+
+ Copy prompt
+ >
+ )}
+
+ );
+}
diff --git a/src/components/divider.tsx b/src/components/divider.tsx
new file mode 100644
index 0000000..4ab08bd
--- /dev/null
+++ b/src/components/divider.tsx
@@ -0,0 +1,3 @@
+export function Divider() {
+ return
;
+}
diff --git a/src/components/footer.tsx b/src/components/footer.tsx
new file mode 100644
index 0000000..a475f22
--- /dev/null
+++ b/src/components/footer.tsx
@@ -0,0 +1,10 @@
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/src/components/hero-section.tsx b/src/components/hero-section.tsx
new file mode 100644
index 0000000..dd41376
--- /dev/null
+++ b/src/components/hero-section.tsx
@@ -0,0 +1,42 @@
+function ScrollDownIcon() {
+ return (
+
+
+
+ );
+}
+
+export function HeroSection() {
+ return (
+
+ Copy · Paste · Build
+
+ Build stunning
+ 3D websites
+
+
+ Ready-to-use prompts for Three.js, CSS 3D, Canvas, Spline, and GSAP.
+ Copy a prompt, paste it into Claude or ChatGPT, and ship your 3D hero in
+ minutes.
+
+
+
+
+ Scroll
+
+
+ );
+}
diff --git a/src/components/how-it-works-section.tsx b/src/components/how-it-works-section.tsx
new file mode 100644
index 0000000..9abcd5b
--- /dev/null
+++ b/src/components/how-it-works-section.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useScrollReveal } from "@/hooks/use-scroll-reveal";
+
+const steps = [
+ {
+ num: "01",
+ title: "Pick a prompt",
+ desc: "Choose an effect that matches your site's vibe — particles, morphing geometry, scroll-driven 3D, or pure CSS depth.",
+ },
+ {
+ num: "02",
+ title: "Paste & customise",
+ desc: "Drop the prompt into Claude or ChatGPT. Append your brand colours, headline copy, and any layout constraints.",
+ },
+ {
+ num: "03",
+ title: "Drop into your site",
+ desc: "The AI returns a self-contained HTML/JS file. Drop it straight into your project or an iframe and you're live.",
+ },
+ {
+ num: "04",
+ title: "Iterate fast",
+ desc: "Don't like the colours or speed? Ask the AI to adjust in a follow-up. No rebuilding from scratch every time.",
+ },
+] as const;
+
+export function HowItWorksSection() {
+ useScrollReveal();
+
+ return (
+
+ Workflow
+ Three steps to a 3D site
+
+ No prior experience with Three.js or WebGL required. AI does the heavy
+ lifting.
+
+
+ {steps.map((step) => (
+
+
{step.num}
+
{step.title}
+
{step.desc}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
new file mode 100644
index 0000000..1c2d706
--- /dev/null
+++ b/src/components/navigation.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useActiveNav } from "@/hooks/use-active-nav";
+import { useNavShadow } from "@/hooks/use-nav-shadow";
+
+export function Navigation() {
+ useActiveNav();
+ useNavShadow();
+
+ return (
+
+
+ 3D. prompts
+
+
+
+ );
+}
diff --git a/src/components/orbs.tsx b/src/components/orbs.tsx
new file mode 100644
index 0000000..4fbd249
--- /dev/null
+++ b/src/components/orbs.tsx
@@ -0,0 +1,9 @@
+export function Orbs() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/components/particle-canvas.tsx b/src/components/particle-canvas.tsx
new file mode 100644
index 0000000..ce6e2b6
--- /dev/null
+++ b/src/components/particle-canvas.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import { useEffect } from "react";
+import { startParticleField } from "@/lib/particle-field";
+
+export function ParticleCanvas() {
+ useEffect(() => {
+ const canvas = document.getElementById("hero-canvas") as HTMLCanvasElement | null;
+ if (!canvas) return;
+
+ return startParticleField(canvas);
+ }, []);
+
+ return ;
+}
diff --git a/src/components/prompt-grid.tsx b/src/components/prompt-grid.tsx
new file mode 100644
index 0000000..72994ca
--- /dev/null
+++ b/src/components/prompt-grid.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { CopyButton } from "@/components/copy-button";
+import { useScrollReveal } from "@/hooks/use-scroll-reveal";
+import type { Prompt } from "@/types/prompt";
+
+interface PromptCardProps {
+ prompt: Prompt;
+}
+
+function PromptCard({ prompt }: PromptCardProps) {
+ return (
+
+
{prompt.tagLabel}
+
{prompt.title}
+
{prompt.desc}
+
{prompt.prompt}
+
+
+ );
+}
+
+interface PromptGridProps {
+ prompts: Prompt[];
+}
+
+export function PromptGrid({ prompts }: PromptGridProps) {
+ useScrollReveal();
+
+ return (
+
+ Ready-made prompts
+ Pick your effect, copy the prompt
+
+ Each prompt is engineered to produce production-ready 3D code. Paste
+ directly into any AI assistant.
+
+
+ {prompts.map((prompt) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/tips-section.tsx b/src/components/tips-section.tsx
new file mode 100644
index 0000000..7654c70
--- /dev/null
+++ b/src/components/tips-section.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { useScrollReveal } from "@/hooks/use-scroll-reveal";
+
+const tips = [
+ {
+ tag: "tag-three",
+ tagLabel: "Performance",
+ title: "Always set a pixel ratio cap",
+ desc: 'Add "cap devicePixelRatio at 2 for performance" to any Three.js prompt. High-DPI displays can halve your frame rate on heavy scenes.',
+ },
+ {
+ tag: "tag-css",
+ tagLabel: "Accessibility",
+ title: "Request a reduced-motion fallback",
+ desc: 'Append "add a prefers-reduced-motion media query that stops all animation" to every prompt to avoid excluding users.',
+ },
+ {
+ tag: "tag-canvas",
+ tagLabel: "Mobile",
+ title: "Ask for mobile particle reduction",
+ desc: 'Add "halve particle count on screens narrower than 768 px" — mobile GPUs can\'t handle the same density as desktop.',
+ },
+] as const;
+
+export function TipsSection() {
+ useScrollReveal();
+
+ return (
+
+ Pro tips
+ Get better results
+
+ {tips.map((tip) => (
+
+
{tip.tagLabel}
+
{tip.title}
+
{tip.desc}
+
+ ))}
+
+
+ );
+}
diff --git a/src/data/prompts.json b/src/data/prompts.json
new file mode 100644
index 0000000..37de6d3
--- /dev/null
+++ b/src/data/prompts.json
@@ -0,0 +1,98 @@
+[
+ {
+ "id": 1,
+ "tag": "tag-three",
+ "tagLabel": "Three.js",
+ "title": "Floating particle galaxy",
+ "desc": "Thousands of particles forming a swirling galaxy with mouse-reactive depth and bloom glow.",
+ "prompt": "Build a full-viewport Three.js hero section with:\n- 8,000 particles in a galaxy spiral (3 arms)\n- Depth-based size and opacity (parallax feel)\n- Mouse move rotates the camera slightly (lerped)\n- Soft purple/blue color gradient across arms\n- requestAnimationFrame loop, auto-resize\n- Cap renderer.setPixelRatio at 2\n- Reduced-motion: pause rotation if prefers-reduced-motion"
+ },
+ {
+ "id": 2,
+ "tag": "tag-three",
+ "tagLabel": "Three.js",
+ "title": "Morphing blob geometry",
+ "desc": "An organic sphere that breathes and morphs using Perlin noise displacement on vertices.",
+ "prompt": "Create a Three.js animated blob hero in a single HTML file:\n- IcosahedronGeometry (detail 6) with vertex displacement\n- Perlin/simplex noise applied to each vertex per frame\n- Iridescent MeshPhysicalMaterial (transmission, roughness 0.1)\n- 3-point HDRI-style lighting rig\n- Subtle mouse-tracking rotation (lerp factor 0.05)\n- Black background, bloom post-processing via UnrealBloomPass\n- Mobile: halve geometry detail on screens < 768px"
+ },
+ {
+ "id": 3,
+ "tag": "tag-three",
+ "tagLabel": "Three.js",
+ "title": "Scroll-driven 3D tunnel",
+ "desc": "A neon tunnel that the camera flies through as the user scrolls down the page.",
+ "prompt": "Build a scroll-driven Three.js tunnel hero:\n- TubeGeometry along a CatmullRom curve (50 points, radius 2)\n- Camera position mapped to window.scrollY (0 → end of tube)\n- Neon grid texture on tube interior (ShaderMaterial)\n- Emissive glow rings every 10 units along the tube\n- Page content layered above canvas with mix-blend-mode\n- Pause animation when tab is hidden (visibilitychange)\n- Works from 320px to 2560px wide"
+ },
+ {
+ "id": 4,
+ "tag": "tag-css",
+ "tagLabel": "CSS 3D",
+ "title": "CSS perspective card stack",
+ "desc": "Layered cards that fan out and lift on hover using pure CSS perspective transforms.",
+ "prompt": "Build a pure-CSS 3D card-stack hero section:\n- 5 stacked cards with translateZ, rotateX, rotateY offsets\n- Hover on the stack fans cards out in 3D space (CSS transitions 0.5s ease)\n- Each card has a glassmorphism surface (backdrop-filter blur)\n- Perspective 1200px set on the parent container\n- transform-style: preserve-3d throughout\n- Responsive: flatten to 2D on prefers-reduced-motion\n- No JavaScript required"
+ },
+ {
+ "id": 5,
+ "tag": "tag-css",
+ "tagLabel": "CSS 3D",
+ "title": "3D text extrusion hero",
+ "desc": "Headline text with a CSS 3D extrusion effect that reacts to mouse position.",
+ "prompt": "Create a CSS 3D extruded-text hero:\n- Headline text with 20 layered text-shadows simulating extrusion depth\n- Mouse move updates CSS custom properties --rx and --ry\n- The text block rotates via rotateX(var(--rx)) rotateY(var(--ry))\n- Extrusion colour is 30% darker than the face colour\n- Smooth perspective tilt (lerp in requestAnimationFrame)\n- Font: system-ui or a single Google Font import\n- Reduced-motion: disable tilt, keep static extrusion"
+ },
+ {
+ "id": 6,
+ "tag": "tag-canvas",
+ "tagLabel": "Canvas 2D",
+ "title": "Interactive fluid simulation",
+ "desc": "Silky fluid that ripples and splashes wherever the cursor moves using 2D canvas.",
+ "prompt": "Implement a 2D canvas fluid-ripple hero background:\n- Grid of velocity/density cells (128×128 resolution)\n- Mouse drag injects dye and velocity into the field\n- Advection + diffusion solved each frame (Jos Stam-style)\n- Render cells as coloured rectangles scaled to viewport\n- 60 fps locked with requestAnimationFrame + delta capping\n- Purple → teal colour map for density values\n- Mobile: respond to touchmove events instead of mousemove"
+ },
+ {
+ "id": 7,
+ "tag": "tag-canvas",
+ "tagLabel": "Canvas 2D",
+ "title": "Generative wave landscape",
+ "desc": "Stacked sine-wave mountain ridges in a looping canvas animation.",
+ "prompt": "Build a canvas 2D animated landscape hero:\n- 6 layered sine-wave ridgelines, each with different frequency/amplitude/speed\n- Ridges filled with semi-transparent gradients (dark purple at top → transparent)\n- Ridges scroll horizontally at different speeds (parallax depth)\n- Stars layer: 200 static dots with twinkle opacity animation\n- Entire scene loops seamlessly (x offset wraps at wave repeat width)\n- Add a subtle radial gradient moon/sun behind the ridges\n- requestAnimationFrame loop, canvas fills 100vw × 100vh"
+ },
+ {
+ "id": 8,
+ "tag": "tag-spline",
+ "tagLabel": "Spline",
+ "title": "Embed a Spline 3D scene",
+ "desc": "Load a Spline scene via the runtime SDK and sync it with scroll or cursor.",
+ "prompt": "Show me how to embed a Spline 3D scene as a hero section:\n- Import @splinetool/runtime via CDN\n- Load a scene URL into a full-viewport \n- Map window.scrollY to a Spline variable named \"scrollProgress\"\n- Map mousemove to Spline variables \"mouseX\" and \"mouseY\"\n- Show a loading skeleton until the scene is ready\n- Make the canvas position: fixed behind page content\n- Include a fallback static image for browsers without WebGL"
+ },
+ {
+ "id": 9,
+ "tag": "tag-gsap",
+ "tagLabel": "GSAP",
+ "title": "GSAP ScrollTrigger 3D reveal",
+ "desc": "Section elements fly in from 3D space as the user scrolls, orchestrated by GSAP.",
+ "prompt": "Build a GSAP ScrollTrigger hero with 3D reveals:\n- Hero headline splits into words, each flies in from rotateX(90deg) → 0\n- Sub-heading fades from opacity 0 + translateZ(-80px) → normal\n- CTA button scales from 0.6 → 1 with a spring ease\n- A background mesh (SVG or canvas) parallaxes at 30% scroll speed\n- Use gsap.registerPlugin(ScrollTrigger) from CDN\n- All animations use perspective on the parent (1000px)\n- Stagger word reveals by 0.06s each"
+ },
+ {
+ "id": 10,
+ "tag": "tag-three",
+ "tagLabel": "Three.js",
+ "title": "Holographic product display",
+ "desc": "A rotating 3D object with a holographic shader, grid lines, and scan animation.",
+ "prompt": "Build a Three.js holographic product-display hero:\n- Load a GLB model (or use a TorusKnotGeometry as placeholder)\n- Custom ShaderMaterial with:\n • Fresnel edge glow (rim lighting)\n • Horizontal scan-line animation moving upward\n • Grid overlay in UV space\n • Iridescent colour shift based on view angle\n- Model rotates slowly on Y axis\n- Click/drag to orbit (OrbitControls, damping enabled)\n- Dark background with a faint circular ground shadow\n- Export as a single HTML file with CDN imports"
+ },
+ {
+ "id": 11,
+ "tag": "tag-css",
+ "tagLabel": "CSS 3D",
+ "title": "Infinite 3D logo cube",
+ "desc": "A CSS cube rotating in 3D with each face showing a different brand asset or gradient.",
+ "prompt": "Create a CSS 3D rotating cube logo hero:\n- A cube with 6 faces, each a different gradient or brand colour\n- Auto-rotates on X and Y axes simultaneously (CSS @keyframes)\n- Face labels (\"About\", \"Work\", \"Contact\", etc.) centred on each face\n- Hover pauses rotation and highlights the nearest face\n- Cube size: 160px on desktop, 100px on mobile\n- transform-style: preserve-3d, perspective: 600px on wrapper\n- Clicking a face dispatches a custom event with the face label"
+ },
+ {
+ "id": 12,
+ "tag": "tag-canvas",
+ "tagLabel": "Canvas 2D",
+ "title": "Matrix rain with custom glyphs",
+ "desc": "The classic falling-code rain, customised with your own characters and colours.",
+ "prompt": "Create a canvas Matrix-rain hero background:\n- Columns of falling characters (mix of katakana + Latin + digits)\n- Each column has random speed (1–3 cells/frame) and start delay\n- Lead character is bright white; trail fades to your accent colour\n- Background: black with 0.05 alpha fill each frame (motion blur effect)\n- Font: monospace 14px\n- On mousemove, columns near cursor temporarily turn accent2 colour\n- Add a CSS overlay gradient (transparent → bg colour) at the bottom"
+ }
+]
diff --git a/src/hooks/use-active-nav.ts b/src/hooks/use-active-nav.ts
new file mode 100644
index 0000000..ccd2937
--- /dev/null
+++ b/src/hooks/use-active-nav.ts
@@ -0,0 +1,32 @@
+"use client";
+
+import { useEffect } from "react";
+
+export function useActiveNav() {
+ useEffect(() => {
+ const sections = document.querySelectorAll("section[id]");
+ const navLinks = document.querySelectorAll('nav ul a[href^="#"]');
+
+ if (!sections.length || !navLinks.length) return;
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ const id = entry.target.id;
+ navLinks.forEach((link) => {
+ const anchor = link as HTMLAnchorElement;
+ anchor.style.color =
+ anchor.getAttribute("href") === `#${id}` ? "var(--text)" : "";
+ });
+ }
+ });
+ },
+ { threshold: 0.4 },
+ );
+
+ sections.forEach((section) => observer.observe(section));
+
+ return () => observer.disconnect();
+ }, []);
+}
diff --git a/src/hooks/use-nav-shadow.ts b/src/hooks/use-nav-shadow.ts
new file mode 100644
index 0000000..b6fba9e
--- /dev/null
+++ b/src/hooks/use-nav-shadow.ts
@@ -0,0 +1,20 @@
+"use client";
+
+import { useEffect } from "react";
+
+export function useNavShadow() {
+ useEffect(() => {
+ const nav = document.querySelector("nav");
+ if (!nav) return;
+
+ const onScroll = () => {
+ nav.style.boxShadow =
+ window.scrollY > 80 ? "0 4px 32px rgba(0,0,0,0.4)" : "none";
+ };
+
+ window.addEventListener("scroll", onScroll, { passive: true });
+ onScroll();
+
+ return () => window.removeEventListener("scroll", onScroll);
+ }, []);
+}
diff --git a/src/hooks/use-scroll-reveal.ts b/src/hooks/use-scroll-reveal.ts
new file mode 100644
index 0000000..abca750
--- /dev/null
+++ b/src/hooks/use-scroll-reveal.ts
@@ -0,0 +1,26 @@
+"use client";
+
+import { useEffect } from "react";
+
+export function useScrollReveal(selector = ".reveal") {
+ useEffect(() => {
+ const targets = document.querySelectorAll(`${selector}:not(.visible)`);
+ if (!targets.length) return;
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add("visible");
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ { threshold: 0.12, rootMargin: "0px 0px -40px 0px" },
+ );
+
+ targets.forEach((el) => observer.observe(el));
+
+ return () => observer.disconnect();
+ });
+}
diff --git a/src/lib/particle-field.ts b/src/lib/particle-field.ts
new file mode 100644
index 0000000..a631cfb
--- /dev/null
+++ b/src/lib/particle-field.ts
@@ -0,0 +1,162 @@
+interface MousePosition {
+ x: number;
+ y: number;
+}
+
+class Particle {
+ x = 0;
+ y = 0;
+ z = 0;
+ vx = 0;
+ vy = 0;
+ r = 0;
+ alpha = 0;
+ hue = 0;
+
+ constructor(initial = false, width = 0, height = 0) {
+ this.reset(initial, width, height);
+ }
+
+ reset(initial: boolean, width: number, height: number) {
+ this.x = Math.random() * width;
+ this.y = initial ? Math.random() * height : height + 10;
+ this.z = Math.random();
+ this.vx = (Math.random() - 0.5) * 0.3;
+ this.vy = -(0.15 + Math.random() * 0.35) * (0.4 + this.z * 0.6);
+ this.r = 0.8 + this.z * 2;
+ this.alpha = 0.15 + this.z * 0.5;
+ const hues = [260, 320, 170];
+ this.hue = hues[Math.floor(Math.random() * hues.length)] ?? 260;
+ }
+
+ step(mouse: MousePosition, width: number, height: number) {
+ const dx = this.x - mouse.x;
+ const dy = this.y - mouse.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+
+ if (dist < 120) {
+ const force = ((120 - dist) / 120) * 0.4;
+ this.vx += (dx / dist) * force;
+ this.vy += (dy / dist) * force;
+ }
+
+ this.vx *= 0.98;
+ this.vy *= 0.98;
+ this.x += this.vx;
+ this.y += this.vy;
+
+ if (this.y < -20 || this.x < -20 || this.x > width + 20) {
+ this.reset(false, width, height);
+ }
+ }
+
+ draw(ctx: CanvasRenderingContext2D) {
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
+ ctx.fillStyle = `hsla(${this.hue}, 80%, 70%, ${this.alpha})`;
+ ctx.fill();
+ }
+}
+
+function drawConnections(
+ ctx: CanvasRenderingContext2D,
+ particles: Particle[],
+) {
+ for (let i = 0; i < particles.length; i++) {
+ for (let j = i + 1; j < particles.length; j++) {
+ const dx = particles[i].x - particles[j].x;
+ const dy = particles[i].y - particles[j].y;
+ const d = Math.sqrt(dx * dx + dy * dy);
+
+ if (d < 90) {
+ ctx.beginPath();
+ ctx.moveTo(particles[i].x, particles[i].y);
+ ctx.lineTo(particles[j].x, particles[j].y);
+ ctx.strokeStyle = `rgba(124, 109, 250, ${(1 - d / 90) * 0.12})`;
+ ctx.lineWidth = 0.5;
+ ctx.stroke();
+ }
+ }
+ }
+}
+
+export function startParticleField(canvas: HTMLCanvasElement) {
+ const context = canvas.getContext("2d");
+ if (!context) return () => undefined;
+
+ const ctx: CanvasRenderingContext2D = context;
+
+ let width = 0;
+ let height = 0;
+ let particles: Particle[] = [];
+ const mouse: MousePosition = { x: -9999, y: -9999 };
+ let lastTime = 0;
+ let animationId = 0;
+
+ function resize() {
+ width = canvas.width = window.innerWidth;
+ height = canvas.height = window.innerHeight;
+ }
+
+ function initParticles() {
+ const count = Math.min(200, Math.floor((width * height) / 8000));
+ particles = Array.from(
+ { length: count },
+ () => new Particle(true, width, height),
+ );
+ }
+
+ function loop(ts: number) {
+ if (ts - lastTime < 16) {
+ animationId = requestAnimationFrame(loop);
+ return;
+ }
+ lastTime = ts;
+
+ ctx.clearRect(0, 0, width, height);
+ drawConnections(ctx, particles);
+ particles.forEach((particle) => {
+ particle.step(mouse, width, height);
+ particle.draw(ctx);
+ });
+ animationId = requestAnimationFrame(loop);
+ }
+
+ const onResize = () => {
+ resize();
+ initParticles();
+ };
+
+ const onMouseMove = (event: MouseEvent) => {
+ mouse.x = event.clientX;
+ mouse.y = event.clientY;
+ };
+
+ const onTouchMove = (event: TouchEvent) => {
+ const touch = event.touches[0];
+ if (!touch) return;
+ mouse.x = touch.clientX;
+ mouse.y = touch.clientY;
+ };
+
+ const onVisibilityChange = () => {
+ if (document.hidden) lastTime = performance.now();
+ };
+
+ resize();
+ initParticles();
+ animationId = requestAnimationFrame(loop);
+
+ window.addEventListener("resize", onResize);
+ window.addEventListener("mousemove", onMouseMove);
+ window.addEventListener("touchmove", onTouchMove, { passive: true });
+ document.addEventListener("visibilitychange", onVisibilityChange);
+
+ return () => {
+ cancelAnimationFrame(animationId);
+ window.removeEventListener("resize", onResize);
+ window.removeEventListener("mousemove", onMouseMove);
+ window.removeEventListener("touchmove", onTouchMove);
+ document.removeEventListener("visibilitychange", onVisibilityChange);
+ };
+}
diff --git a/src/types/prompt.ts b/src/types/prompt.ts
new file mode 100644
index 0000000..5a7c624
--- /dev/null
+++ b/src/types/prompt.ts
@@ -0,0 +1,8 @@
+export interface Prompt {
+ id: number;
+ tag: string;
+ tagLabel: string;
+ title: string;
+ desc: string;
+ prompt: string;
+}