3D Tilt
Perspective tilt with glare effect for depth
@@ -63,129 +112,71 @@ const tiltSpotlightCode = `
-
-
-
-
- Preview
-
-
-
-
-
- Spotlight Effect
-
- Hover over this card to see the spotlight follow your cursor
-
-
-
-
- This card features an animated gradient border and a soft
- spotlight glow that tracks your mouse movement for an
- interactive experience.
-
-
-
-
-
- Move your cursor over the card to see the spotlight effect in
- action.
-
-
-
+
+
+
+
-
-
- Custom
-
-
-
-
-
- Custom Colors
-
- Fully customizable spotlight color and intensity
-
-
-
-
- Customize the spotlight color, glow intensity, border radius,
- and more to match your design system.
-
-
-
+
+
Default
+
+
+
+ Spotlight Effect
+
+ Hover over this card to see the spotlight follow your cursor
+
+
+
+
+ This card features an animated gradient border and a soft
+ spotlight glow that tracks your mouse movement.
+
+
+
+
+
-
- All props are customizable to match your brand colors.
-
-
-
-
-
- Install
-
-
-
-
- Requires shadcn CLI. Run{" "}
- npx shadcn@latest init{" "}
- first if not set up.
-
-
-
-
-
-
- Code
-
-
-
-
- Basic usage with the structured card components.
-
-
-
+
+
Custom Colors
+
+
+
+ Custom Colors
+
+ Fully customizable spotlight color and intensity
+
+
+
+
+ Customize the spotlight color, glow intensity, border radius,
+ and more to match your design system.
+
+
+
+
+
+
-
-
- Examples
-
-
-
Multi Spotlight
-
+
- Multi Spotlight
-
+
Multi Spotlight
+
Multiple colored spotlight sources follow your cursor
@@ -195,10 +186,10 @@ export default function SpotlightCardPage(): React.JSX.Element {
Beam Spotlight
-
+
- Beam Spotlight
-
+
Beam Spotlight
+
Crossing light beams create a dramatic effect
@@ -208,10 +199,10 @@ export default function SpotlightCardPage(): React.JSX.Element {
Gradient Follow
-
+
- Gradient Follow
-
+
Gradient Follow
+
Dynamic gradient background follows cursor position
@@ -221,10 +212,10 @@ export default function SpotlightCardPage(): React.JSX.Element {
3D Tilt
-
+
- 3D Tilt
-
+
3D Tilt
+
Perspective tilt with glare effect for depth
@@ -233,89 +224,79 @@ export default function SpotlightCardPage(): React.JSX.Element {
-
+
-
-
- Features
-
-
-
-
-
Cursor Tracking
-
- Spotlight effect smoothly follows mouse position across the card
-
+
+
+
+
Cursor Tracking
+
+ Spotlight effect smoothly follows mouse position across the card
-
-
Animated Border
-
- Rotating gradient border creates a subtle animated glow
-
+
+
+
Animated Border
+
+ Rotating gradient border creates a subtle animated glow
-
-
3D Tilt Effect
-
- Perspective-based tilt with glare for realistic depth
-
+
+
+
3D Tilt Effect
+
+ Perspective-based tilt with glare for realistic depth
-
-
Multiple Variants
-
- Choose from spotlight, multi-spotlight, beam, gradient, or tilt
-
+
+
+
Multiple Variants
+
+ Choose from spotlight, multi-spotlight, beam, gradient, or tilt
-
-
Customizable
-
- Fully configurable colors, intensity, radius, and animation
-
+
+
+
Customizable
+
+ Fully configurable colors, intensity, radius, and animation
-
-
Performance
-
- GPU-accelerated animations with smooth 60fps transitions
-
+
+
+
Performance
+
+ GPU-accelerated animations with smooth 60fps transitions
-
+
-
-
- Props
-
-
-
-
-
spotlightColor
-
- Color of the spotlight effect (default: rgba(120, 119, 198,
- 0.3))
-
+
+
+
+
spotlightColor
+
+ Color of the spotlight effect (default: rgba(120, 119, 198,
+ 0.3))
-
-
glowIntensity
-
- Intensity of the glow effect 0-1 (default: 0.15)
-
+
+
+
glowIntensity
+
+ Intensity of the glow effect 0-1 (default: 0.15)
-
-
borderRadius
-
- Border radius in pixels (default: 16)
-
+
+
+
borderRadius
+
+ Border radius in pixels (default: 16)
-
-
maxTilt
-
- Maximum tilt angle in degrees for TiltSpotlightCard (default:
- 10)
-
+
+
+
maxTilt
+
+ Maximum tilt angle in degrees for TiltSpotlightCard (default:
+ 10)
-
-
+
+
)
}
diff --git a/apps/web/app/docs/layout.tsx b/apps/web/app/docs/layout.tsx
index ce4681c..25e25df 100644
--- a/apps/web/app/docs/layout.tsx
+++ b/apps/web/app/docs/layout.tsx
@@ -1,4 +1,5 @@
import type React from "react"
+import type { Metadata } from "next"
import Link from "next/link"
import { ThemeToggle } from "@/components/theme-toggle"
import { CommandMenu } from "@/components/command-menu"
@@ -6,6 +7,18 @@ import { DocsSidebar } from "@/components/docs-sidebar"
import { TableOfContents } from "@/components/table-of-contents"
import { Logomark } from "@/components/logos/logomark"
+export const metadata: Metadata = {
+ title: "Components Documentation",
+ description: "Browse the complete collection of Componentry UI components. Free, open-source React components with copy-paste code, Tailwind CSS styling, and Framer Motion animations by Harsh Jadhav.",
+ openGraph: {
+ title: "UI Components Documentation | Componentry by Harsh Jadhav",
+ description: "Browse all React UI components. Copy-paste ready code with Tailwind CSS and Framer Motion.",
+ },
+ alternates: {
+ canonical: "https://componentry.fun/docs",
+ },
+}
+
export default function DocsLayout({
children,
}: {
@@ -33,20 +46,22 @@ export default function DocsLayout({
-
diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx
index 4b673a2..4b59992 100644
--- a/apps/web/app/docs/page.tsx
+++ b/apps/web/app/docs/page.tsx
@@ -1,9 +1,50 @@
+"use client"
+
import type React from "react"
import Link from "next/link"
+import { motion } from "framer-motion"
+import { ArrowRight, Box, Component, Zap, Layout, ArrowUpRight } from "lucide-react"
+
+const container = {
+ hidden: { opacity: 0 },
+ show: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1
+ }
+ }
+}
+
+const item = {
+ hidden: { opacity: 0, y: 20 },
+ show: { opacity: 1, y: 0 }
+}
+
+function BentoCard({
+ children,
+ className = "",
+ href
+}: {
+ children: React.ReactNode
+ className?: string
+ href: string
+}) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
export default function DocsIntroPage(): React.JSX.Element {
return (
-
+
+ {/* Manifesto Section */}
Manifesto
@@ -23,46 +64,96 @@ export default function DocsIntroPage(): React.JSX.Element {
-
-
-
- “Progress over polish.”
-
-
+
+ {/* Main Feature Card - Flight Status */}
+
+
+
+
+
+
+ Featured Component
+
+
Flight Status Card
+
+ A complex, real-world data visualization component with dot-matrix typography and smooth animations.
+
+
+
+ {/* Abstract visual representation */}
+
+
+
+
+
+
-
-
-
Values
-
- Clarity
- Experimentation
- Learning
-
-
-
-
Not
-
- A design system
- A UI kit
- Stable
-
-
-
+ {/* Button Card */}
+
+
+
+
+
+
+
+
Buttons
+
Interactive variants
+
+
+
+
-
-
- 001
- Button
- →
-
-
-
+ {/* Spotlight Card */}
+
+
+
+
+
+
+
+
Spotlight
+
Cursor tracking effects
+
+
+
+
+
+ {/* Shimmer Button Card */}
+
+
+
+
+
+
+
+
Shimmer
+
Loading & highlight states
+
+
+
+
+
+ {/* Coming Soon / More */}
+
+
+ More components in the works...
+
+ Request a component
+
+
+
+
)
}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index dca8450..fd4f01f 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,9 +1,10 @@
import type React from "react"
-import type { Metadata } from "next"
+import type { Metadata, Viewport } from "next"
import { Inter, Instrument_Serif, Syne } from "next/font/google"
-
+import { Analytics } from "@vercel/analytics/next"
import "@workspace/ui/globals.css"
import { Providers } from "@/components/providers"
+import { JsonLd } from "@/components/seo/json-ld"
const fontSans = Inter({
subsets: ["latin"],
@@ -21,35 +22,128 @@ const fontDisplay = Syne({
variable: "--font-display",
})
+const siteUrl = "https://www.componentry.fun"
+
+export const viewport: Viewport = {
+ width: "device-width",
+ initialScale: 1,
+ maximumScale: 5,
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "#ffffff" },
+ { media: "(prefers-color-scheme: dark)", color: "#000000" },
+ ],
+}
+
export const metadata: Metadata = {
- title: "Component Playground",
- description: "A personal workshop for handcrafted UI components.",
- metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || ""),
+ metadataBase: new URL(siteUrl),
+ title: {
+ default: "Componentry - Premium React UI Component Library by Harsh Jadhav",
+ template: "%s | Componentry - UI Component Library",
+ },
+ description: "Componentry is a free, open-source React UI component library by Harsh Jadhav. Beautiful, animated, copy-paste components built with Tailwind CSS, TypeScript, and Framer Motion. The best UI components for modern web applications.",
+ keywords: [
+ "UI component library",
+ "React components",
+ "React UI library",
+ "UI components",
+ "component library",
+ "Tailwind CSS components",
+ "TypeScript components",
+ "Framer Motion",
+ "Next.js components",
+ "animated components",
+ "copy paste components",
+ "free UI components",
+ "open source components",
+ "modern UI",
+ "web components",
+ "frontend components",
+ "design system",
+ "Harsh Jadhav",
+ "Harsh Jadhav developer",
+ "Harsh Jadhav portfolio",
+ "harshjdhv",
+ "React developer",
+ "frontend developer",
+ "shadcn alternative",
+ "radix ui",
+ "beautiful UI",
+ "premium components",
+ "handcrafted components",
+ ],
+ authors: [
+ { name: "Harsh Jadhav", url: "https://twitter.com/harshjdhv" },
+ { name: "Harsh Jadhav", url: "https://github.com/harshjdhv" },
+ ],
+ creator: "Harsh Jadhav",
+ publisher: "Harsh Jadhav",
+ formatDetection: {
+ email: false,
+ address: false,
+ telephone: false,
+ },
+ alternates: {
+ canonical: siteUrl,
+ },
openGraph: {
- title: "Component Playground",
- description: "A personal workshop for handcrafted UI components.",
- siteName: "Component Playground",
+ type: "website",
+ locale: "en_US",
+ url: siteUrl,
+ title: "Componentry - Premium React UI Component Library by Harsh Jadhav",
+ description: "Free, open-source React UI components. Beautiful, animated, copy-paste components built with Tailwind CSS, TypeScript & Framer Motion by Harsh Jadhav.",
+ siteName: "Componentry",
images: [
{
- url: "/preview.png",
+ url: `${siteUrl}/preview-wa.jpg`,
width: 1200,
height: 630,
- alt: "Component Playground Preview",
- type: "image/png",
+ alt: "Componentry - Premium React UI Component Library",
+ type: "image/jpeg",
},
],
},
twitter: {
card: "summary_large_image",
- title: "Component Playground",
- description: "A personal workshop for handcrafted UI components.",
- images: ["/preview.png"],
+ title: "Componentry - Premium React UI Component Library",
+ description: "Free, open-source React UI components by Harsh Jadhav. Beautiful, animated, copy-paste components.",
+ images: [
+ {
+ url: `${siteUrl}/preview-wa.jpg`,
+ width: 1200,
+ height: 630,
+ alt: "Componentry - Premium React UI Component Library",
+ },
+ ],
+ creator: "@harshjdhv",
+ site: "@harshjdhv",
+ },
+ robots: {
+ index: true,
+ follow: true,
+ nocache: false,
+ googleBot: {
+ index: true,
+ follow: true,
+ noimageindex: false,
+ "max-video-preview": -1,
+ "max-image-preview": "large",
+ "max-snippet": -1,
+ },
},
icons: {
icon: "/icon.svg",
shortcut: "/icon.svg",
apple: "/icon.svg",
},
+ manifest: "/manifest.json",
+ category: "technology",
+ classification: "UI Component Library",
+ other: {
+ "msapplication-TileImage": "/preview.png",
+ ...(process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION && {
+ "google-site-verification": process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION,
+ }),
+ },
}
export default function RootLayout({
@@ -59,10 +153,14 @@ export default function RootLayout({
}>): React.JSX.Element {
return (
+
+
+
{children}
+
)
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 7ef0027..2e08e5d 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -2,151 +2,811 @@
import type React from "react"
import Link from "next/link"
-import { useEffect, useState } from "react"
+import { useEffect, useState, useRef } from "react"
+import { motion, useScroll, useTransform, useInView } from "framer-motion"
+import { ArrowRight, Sparkles, Layers, Zap, Box, Github, ArrowUpRight, Copy, Palette, Code2, Blocks, MousePointerClick, Gauge, Users, Star, GitFork, Download } from "lucide-react"
+import { Logomark } from "@/components/logos/logomark"
+import Lenis from "lenis"
-function GridBackground() {
+function useSmoothScroll() {
+ useEffect(() => {
+ const lenis = new Lenis({
+ duration: 1.2,
+ easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
+ smoothWheel: true,
+ })
+
+ function raf(time: number) {
+ lenis.raf(time)
+ requestAnimationFrame(raf)
+ }
+
+ requestAnimationFrame(raf)
+
+ return () => {
+ lenis.destroy()
+ }
+ }, [])
+}
+
+function NoiseOverlay() {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function GradientOrbs() {
+ return (
+
+
+
+
+
+ )
+}
+
+function GridLines() {
return (
-
+
+ )
+}
+
+function AnimatedText({ text, className = "" }: { text: string; className?: string }) {
+ return (
+
+ {text.split("").map((char, i) => (
+
+ {char === " " ? "\u00A0" : char}
+
+ ))}
+
+ )
+}
+
+function FloatingElement({ children, className = "", delay = 0 }: { children: React.ReactNode; className?: string; delay?: number }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function OrbitingElement({ className = "", duration = 20, radius = 300, startAngle = 0 }: { className?: string; duration?: number; radius?: number; startAngle?: number }) {
+ return (
+
+
+
+ )
+}
+
+function CodePreviewCard({ className = "", delay = 0 }: { className?: string; delay?: number }) {
+ return (
+
+
+
+
+
{"// Premium UI"}
+
import {"{ Button }"}
+
{"// ..."}
+
+ {"<"}
+ Button
+ {" "}
+ variant
+ {"="}
+ {'"shine"'}
+ {">"}
+
+
Click me
+
+ {""}
+ Button
+ {">"}
+
+
+
+
+ )
+}
+
+function ComponentPreviewCard({ className = "", delay = 0 }: { className?: string; delay?: number }) {
+ return (
+
+
+
+
+
+
+
+
+
Border Beam
+
Animated effect
+
+
+
+
+
+
+
+ )
+}
+
+function FloatingBadge({ text, className = "", delay = 0 }: { text: string; className?: string; delay?: number }) {
+ return (
+
+
+ {text}
+
+
+ )
+}
+
+function ConnectingLines() {
+ return (
+
+
+
+
-
-
+
+
+ )
+}
+
+function MarqueeItem({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
)
}
-function FloatingCard({ className, offset = 0 }: { className?: string; offset?: number }) {
- const [y, setY] = useState(0)
+function InfiniteMarquee() {
+ const items = [
+ "Handcrafted with precision",
+ "Built for developers",
+ "Open source forever",
+ "Pixel-perfect details",
+ "Smooth animations",
+ "Dark mode ready",
+ "TypeScript first",
+ "Tailwind powered",
+ ]
+ return (
+
+
+
+
+ {[...items, ...items, ...items, ...items].map((item, i) => (
+
+
+ {item}
+
+ ))}
+
+
+ )
+}
+
+function FeatureCard({ icon: Icon, title, description, delay = 0 }: { icon: React.ElementType; title: string; description: string; delay?: number }) {
+ return (
+
+
+
+
+
+
+
+
{title}
+
{description}
+
+
+
+ )
+}
+
+function BentoCard({ children, className = "", delay = 0 }: { children: React.ReactNode; className?: string; delay?: number }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function AnimatedCounter({ value, suffix = "", prefix = "" }: { value: number; suffix?: string; prefix?: string }) {
+ const ref = useRef
(null)
+ const inView = useInView(ref, { once: true, margin: "-100px" })
+ const [count, setCount] = useState(0)
+
useEffect(() => {
- let frame: number
- const animate = () => {
- setY(Math.sin((Date.now() + offset * 1000) / 1000) * 10)
- frame = requestAnimationFrame(animate)
+ if (inView) {
+ const duration = 2000
+ const steps = 60
+ const increment = value / steps
+ let current = 0
+ const timer = setInterval(() => {
+ current += increment
+ if (current >= value) {
+ setCount(value)
+ clearInterval(timer)
+ } else {
+ setCount(Math.floor(current))
+ }
+ }, duration / steps)
+ return () => clearInterval(timer)
}
- animate()
- return () => cancelAnimationFrame(frame)
- }, [offset])
-
+ }, [inView, value])
+
+ return (
+
+ {prefix}{count.toLocaleString()}{suffix}
+
+ )
+}
+
+function StatCard({ icon: Icon, value, suffix, label, delay = 0 }: { icon: React.ElementType; value: number; suffix?: string; label: string; delay?: number }) {
return (
-
-
+
+
{label}
+
)
}
-function FadeIn({ children, delay = 0, className = "" }: { children: React.ReactNode; delay?: number; className?: string }) {
- const [visible, setVisible] = useState(false)
-
- useEffect(() => {
- const timer = setTimeout(() => setVisible(true), delay)
- return () => clearTimeout(timer)
- }, [delay])
-
+function TestimonialCard({ quote, author, role, delay = 0 }: { quote: string; author: string; role: string; delay?: number }) {
return (
-
- {children}
-
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
“{quote}”
+
+
+
)
}
export default function Page(): React.JSX.Element {
+ useSmoothScroll()
+ const { scrollYProgress } = useScroll()
+ const heroOpacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
+ const heroScale = useTransform(scrollYProgress, [0, 0.2], [1, 0.95])
+
return (
-
-
+
+
+
+
-
-
-
-
+ {/* Navigation */}
+
+
+
+
+
+
+
Componentry
+
+
+
+
-
-
-
-
- Experiments in UI
+ {/* Hero Section */}
+
+
+
+
+
+ Now Open Source
-
-
-
-
+
+
+
+
+
- Component
-
- Playground
-
-
-
-
-
- A personal workshop for handcrafted UI components. Not a design
- system. Just experiments, ideas, and iterations built in public.
-
-
-
-
+ Components
+
+
+
+
+ A curated collection of handcrafted React components. Meticulously designed,
+ beautifully animated, and built for modern interfaces.
+
+
+
- Browse Components
-
-
-
+ Explore Components
+
+
-
-
-
- GitHub
+
+ Star on GitHub
-
+
+
+
+ {/* Connecting Lines */}
+
-
-
-
-
Actively building
+ {/* Left Side Elements */}
+
+
+
+
+
+
+
+
+
+ {/* Right Side Elements */}
+
+
+
+
+
+
+
+
+
+ {/* Small Floating Icons */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Orbiting Dots */}
+
+
+
+
+
+
+
+ {/* Scroll Indicator */}
+
+
+
+
+
+
+
+ {/* Marquee */}
+
+
+ {/* Features Section */}
+
+
+
+
+ Why Componentry
+
+
+ Built for developers who care
+
+
+ Every component is crafted with attention to detail, performance, and developer experience.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Testimonials */}
+
+
+
+
+ Testimonials
+
+
+ Loved by developers
+
+
+ See what others are saying about Componentry.
+
+
+
+
+
+
+
+
+
+
+
+ {/* CTA Section */}
+
+
+
+
+ Start building today
+
+
+ Copy, paste, and customize. Every component is yours to use and modify freely.
+
+
-
- Progress > polish
-
+
-
+
-
)
diff --git a/apps/web/app/preview/page.tsx b/apps/web/app/preview/page.tsx
index 147a058..01ec08f 100644
--- a/apps/web/app/preview/page.tsx
+++ b/apps/web/app/preview/page.tsx
@@ -1,101 +1,219 @@
import type React from "react"
-import { cn } from "@workspace/ui/lib/utils"
+import { Sparkles, Layers, Zap, Box } from "lucide-react"
-function BackgroundGrid({ className }: { className?: string }) {
+function NoiseOverlay() {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function GradientOrbs() {
+ return (
+
)
}
-function HalftoneEffect({ className }: { className?: string }) {
+function GridLines() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ )
+}
+
+function CodePreviewCard({ className = "" }: { className?: string }) {
+ return (
+
+
+
+
+
{"// Premium UI"}
+
import {"{ Button }"}
+
{"// ..."}
+
+ {"<"}
+ Button
+ {" "}
+ variant
+ {"="}
+ {'"shine"'}
+ {">"}
+
+
Click me
+
+ {""}
+ Button
+ {">"}
+
+
+
+
+ )
+}
+
+function ComponentPreviewCard({ className = "" }: { className?: string }) {
+ return (
+
+
+
+
+
+
+
+
+
Border Beam
+
Animated effect
+
+
+
+
+
+
+
+ )
+}
+
+function FloatingBadge({ text, className = "" }: { text: string; className?: string }) {
+ return (
+
+ )
+}
+
+function FloatingIcon({ icon: Icon, className = "" }: { icon: React.ElementType; className?: string }) {
+ return (
+
+ )
+}
+
+function ConnectingLines() {
+ return (
+
+
+
+
+
)
}
export default function PreviewPage(): React.JSX.Element {
return (
-
- {/* Backgrounds */}
-
-
-
- {/* Frame Border */}
-
-
- {/* Main Layout */}
-
- {/* Center Content */}
-
-
-
+
+
+
+
+
+ {/* Border Frame */}
+
+
+ {/* Heading Glow */}
+
+
+ {/* Main Content */}
+
+
+ {/* Badge */}
+
+
+
+ Now Open Source
+
+
+
+ {/* Heading */}
+
+
- Component
-
- Playground
-
+ Premium
+
+
UI
+
+
Components
+
+
+ {/* Divider Line */}
+
-
-
-
-
- Precision Crafted React Components
+
+ {/* Tagline */}
+
+ Handcrafted React components. Beautifully animated.
-
+
+
+ {/* Floating Elements - Larger for visibility */}
+
+
+
+
+
+ {/* Corner Icons */}
+
+
+
+
)
}
diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts
new file mode 100644
index 0000000..290be0f
--- /dev/null
+++ b/apps/web/app/robots.ts
@@ -0,0 +1,27 @@
+import { MetadataRoute } from "next"
+
+export default function robots(): MetadataRoute.Robots {
+ const baseUrl = "https://componentry.fun"
+
+ return {
+ rules: [
+ {
+ userAgent: "*",
+ allow: "/",
+ disallow: ["/api/", "/_next/", "/preview"],
+ },
+ {
+ userAgent: "Googlebot",
+ allow: "/",
+ disallow: ["/api/", "/_next/"],
+ },
+ {
+ userAgent: "Bingbot",
+ allow: "/",
+ disallow: ["/api/", "/_next/"],
+ },
+ ],
+ sitemap: `${baseUrl}/sitemap.xml`,
+ host: baseUrl,
+ }
+}
diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts
new file mode 100644
index 0000000..185534d
--- /dev/null
+++ b/apps/web/app/sitemap.ts
@@ -0,0 +1,48 @@
+import { MetadataRoute } from "next"
+
+const baseUrl = "https://componentry.fun"
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const currentDate = new Date()
+
+ const staticPages: MetadataRoute.Sitemap = [
+ {
+ url: baseUrl,
+ lastModified: currentDate,
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ {
+ url: `${baseUrl}/docs`,
+ lastModified: currentDate,
+ changeFrequency: "weekly",
+ priority: 0.9,
+ },
+ {
+ url: `${baseUrl}/preview`,
+ lastModified: currentDate,
+ changeFrequency: "monthly",
+ priority: 0.5,
+ },
+ ]
+
+ const componentPages = [
+ "flight-status-card",
+ "border-beam",
+ "spotlight-card",
+ "circuit-board",
+ "command-menu",
+ "dither-gradient",
+ "liquid-blob",
+ "noise-texture",
+ ]
+
+ const componentSitemap: MetadataRoute.Sitemap = componentPages.map((component) => ({
+ url: `${baseUrl}/docs/components/${component}`,
+ lastModified: currentDate,
+ changeFrequency: "weekly" as const,
+ priority: 0.8,
+ }))
+
+ return [...staticPages, ...componentSitemap]
+}
diff --git a/apps/web/components/code-block.tsx b/apps/web/components/code-block.tsx
index 6f53493..1ae57c9 100644
--- a/apps/web/components/code-block.tsx
+++ b/apps/web/components/code-block.tsx
@@ -1,4 +1,5 @@
import { codeToHtml } from "shiki"
+import { CopyButton } from "./copy-button"
interface CodeBlockProps {
code: string
@@ -57,9 +58,11 @@ export async function CodeBlock({ code, lang = "tsx", className }: CodeBlockProp
}
`}
+ className={`relative rounded-lg text-sm w-full [&_pre]:p-4 [&_pre]:overflow-x-auto bg-zinc-100 dark:bg-zinc-900 max-h-[400px] overflow-auto ${className || ""}`}
+ >
+
+
+
>
)
}
diff --git a/apps/web/components/command-menu.tsx b/apps/web/components/command-menu.tsx
index cbe218b..cdcb5de 100644
--- a/apps/web/components/command-menu.tsx
+++ b/apps/web/components/command-menu.tsx
@@ -1,15 +1,18 @@
"use client"
import * as React from "react"
+import * as ReactDOM from "react-dom"
import { useRouter } from "next/navigation"
import { Command } from "cmdk"
-import { Search } from "lucide-react"
+import { Search, FileText, Hash, ArrowRight } from "lucide-react"
+import { motion, AnimatePresence } from "framer-motion"
import { docsConfig } from "@/config/docs"
-import { cn } from "@workspace/ui/lib/utils"
export function CommandMenu() {
const router = useRouter()
const [open, setOpen] = React.useState(false)
+ const [query, setQuery] = React.useState("")
+ const inputRef = React.useRef
(null)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
@@ -17,12 +20,25 @@ export function CommandMenu() {
e.preventDefault()
setOpen((open) => !open)
}
+ if (e.key === "Escape") {
+ setOpen(false)
+ }
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
+ React.useEffect(() => {
+ if (open) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 0)
+ } else {
+ setQuery("")
+ }
+ }, [open])
+
const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false)
command()
@@ -32,51 +48,131 @@ export function CommandMenu() {
<>
setOpen(true)}
- className="inline-flex items-center gap-2 whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input hover:bg-accent hover:text-accent-foreground px-4 py-2 relative h-9 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-muted-foreground sm:pr-12 md:w-40 lg:w-64"
+ className="group inline-flex items-center gap-2 whitespace-nowrap transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 border border-input/50 hover:border-input hover:bg-accent/50 px-3 py-2 relative h-9 w-full justify-start rounded-lg bg-muted/30 text-sm font-normal text-muted-foreground sm:pr-12 md:w-40 lg:w-56"
>
- Search documentation...
- Search...
-
+
+ Search...
+ Search
+
⌘ K
-
-
-
-
-
-
- No results found.
- {docsConfig.nav.map((group) => (
-
- {group.items.map((navItem) => (
- {
- runCommand(() => router.push(navItem.href))
- }}
- className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
- >
-
- {navItem.title}
-
- ))}
-
- ))}
-
-
-
- {open && (
- setOpen(false)} />
+
+ {typeof document !== "undefined" && ReactDOM.createPortal(
+
+ {open && (
+ <>
+ setOpen(false)}
+ />
+
+
+
+
+
+
+
+ {query && (
+
setQuery("")}
+ className="rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
+ >
+ Clear
+
+ )}
+
+ ESC
+
+
+
+
+
+
+
+
+ No results found
+ Try searching for something else
+
+
+ {docsConfig.nav.map((group) => (
+
+ {group.items.map((navItem) => (
+ {
+ runCommand(() => router.push(navItem.href))
+ }}
+ className="group/item relative flex cursor-pointer select-none items-center gap-3 rounded-xl px-3 py-2.5 text-sm outline-none transition-colors hover:bg-accent/70 hover:text-accent-foreground aria-[selected='true']:bg-accent aria-[selected='true']:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50"
+ >
+
+ {group.title === "Getting Started" ? (
+
+ ) : (
+
+ )}
+
+
+ {navItem.title}
+ {group.title}
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+ ↑↓
+ Navigate
+
+
+ ↵
+ Select
+
+
+
Componentry
+
+
+
+ >
+ )}
+ ,
+ document.body
)}
>
)
diff --git a/apps/web/components/component-layout.tsx b/apps/web/components/component-layout.tsx
new file mode 100644
index 0000000..dbf5aa7
--- /dev/null
+++ b/apps/web/components/component-layout.tsx
@@ -0,0 +1,58 @@
+import type React from "react"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface ComponentLayoutProps {
+ title: string
+ description: string
+ children: React.ReactNode
+}
+
+export function ComponentLayout({
+ title,
+ description,
+ children,
+}: ComponentLayoutProps) {
+ return (
+
+
+
+
+
+ {title}
+
+
{description}
+
+
+
+ {children}
+
+ )
+}
+
+interface SectionProps {
+ title: string
+ children: React.ReactNode
+ id?: string
+}
+
+export function Section({ title, children, id }: SectionProps) {
+ const sectionId = id || title.toLowerCase().replace(/\s+/g, "-")
+
+ return (
+
+
+ {title}
+
+
+ {children}
+
+
+ )
+}
diff --git a/apps/web/components/copy-button.tsx b/apps/web/components/copy-button.tsx
new file mode 100644
index 0000000..73e3963
--- /dev/null
+++ b/apps/web/components/copy-button.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { useState } from "react"
+import { Check, Copy } from "lucide-react"
+
+interface CopyButtonProps {
+ code: string
+}
+
+export function CopyButton({ code }: CopyButtonProps) {
+ const [copied, setCopied] = useState(false)
+
+ const handleCopy = async () => {
+ await navigator.clipboard.writeText(code)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ return (
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/apps/web/components/docs-sidebar.tsx b/apps/web/components/docs-sidebar.tsx
index 172573b..244f39a 100644
--- a/apps/web/components/docs-sidebar.tsx
+++ b/apps/web/components/docs-sidebar.tsx
@@ -16,7 +16,7 @@ export function DocsSidebar() {
{group.title}
-
+
{group.items.map((item) => {
const isActive = pathname === item.href
return (
@@ -24,7 +24,7 @@ export function DocsSidebar() {
diff --git a/apps/web/components/seo/json-ld.tsx b/apps/web/components/seo/json-ld.tsx
new file mode 100644
index 0000000..9b378e5
--- /dev/null
+++ b/apps/web/components/seo/json-ld.tsx
@@ -0,0 +1,143 @@
+export function JsonLd() {
+ const siteUrl = "https://componentry.fun"
+
+ const organizationSchema = {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ name: "Componentry",
+ url: siteUrl,
+ logo: `${siteUrl}/icon.svg`,
+ sameAs: [
+ "https://github.com/harshjdhv/componentry",
+ "https://twitter.com/harshjdhv",
+ ],
+ founder: {
+ "@type": "Person",
+ name: "Harsh Jadhav",
+ url: "https://twitter.com/harshjdhv",
+ sameAs: [
+ "https://twitter.com/harshjdhv",
+ "https://github.com/harshjdhv",
+ ],
+ },
+ }
+
+ const websiteSchema = {
+ "@context": "https://schema.org",
+ "@type": "WebSite",
+ name: "Componentry",
+ alternateName: ["Componentry UI", "Componentry Components"],
+ url: siteUrl,
+ description:
+ "Free, open-source React UI component library by Harsh Jadhav. Beautiful, animated, copy-paste components.",
+ publisher: {
+ "@type": "Person",
+ name: "Harsh Jadhav",
+ url: "https://twitter.com/harshjdhv",
+ },
+ potentialAction: {
+ "@type": "SearchAction",
+ target: {
+ "@type": "EntryPoint",
+ urlTemplate: `${siteUrl}/docs?search={search_term_string}`,
+ },
+ "query-input": "required name=search_term_string",
+ },
+ }
+
+ const softwareApplicationSchema = {
+ "@context": "https://schema.org",
+ "@type": "SoftwareSourceCode",
+ name: "Componentry",
+ description:
+ "Premium React UI component library with beautiful animations. Copy-paste components built with Tailwind CSS, TypeScript, and Framer Motion.",
+ url: siteUrl,
+ codeRepository: "https://github.com/harshjdhv/componentry",
+ programmingLanguage: ["TypeScript", "JavaScript", "React", "CSS"],
+ runtimePlatform: "Node.js",
+ author: {
+ "@type": "Person",
+ name: "Harsh Jadhav",
+ url: "https://twitter.com/harshjdhv",
+ },
+ license: "https://opensource.org/licenses/MIT",
+ operatingSystem: "Cross-platform",
+ applicationCategory: "DeveloperApplication",
+ keywords:
+ "React, UI components, Tailwind CSS, TypeScript, Framer Motion, Next.js, component library",
+ }
+
+ const personSchema = {
+ "@context": "https://schema.org",
+ "@type": "Person",
+ name: "Harsh Jadhav",
+ alternateName: ["harshjdhv", "Harsh"],
+ url: "https://twitter.com/harshjdhv",
+ jobTitle: "Frontend Developer",
+ knowsAbout: [
+ "React",
+ "TypeScript",
+ "JavaScript",
+ "Tailwind CSS",
+ "Next.js",
+ "UI/UX Design",
+ "Web Development",
+ "Frontend Development",
+ ],
+ sameAs: [
+ "https://twitter.com/harshjdhv",
+ "https://github.com/harshjdhv",
+ siteUrl,
+ ],
+ mainEntityOfPage: {
+ "@type": "WebPage",
+ "@id": siteUrl,
+ },
+ }
+
+ const breadcrumbSchema = {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ position: 1,
+ name: "Home",
+ item: siteUrl,
+ },
+ {
+ "@type": "ListItem",
+ position: 2,
+ name: "Components",
+ item: `${siteUrl}/docs`,
+ },
+ ],
+ }
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/web/components/table-of-contents.tsx b/apps/web/components/table-of-contents.tsx
index ce061c0..c253335 100644
--- a/apps/web/components/table-of-contents.tsx
+++ b/apps/web/components/table-of-contents.tsx
@@ -93,18 +93,19 @@ export function TableOfContents(): React.JSX.Element | null {
return () => observer.disconnect()
}, [pathname])
- if (headings.length === 0) return null
-
return (
)
}
diff --git a/apps/web/config/docs.ts b/apps/web/config/docs.ts
index 3fe6691..205cb59 100644
--- a/apps/web/config/docs.ts
+++ b/apps/web/config/docs.ts
@@ -12,10 +12,6 @@ export const docsConfig = {
{
title: "Components",
items: [
- {
- title: "Button",
- href: "/docs/components/button",
- },
{
title: "Flight Status Card",
href: "/docs/components/flight-status-card",
@@ -24,7 +20,44 @@ export const docsConfig = {
title: "Spotlight Card",
href: "/docs/components/spotlight-card",
},
+ {
+ title: "Showcase Card",
+ href: "/docs/components/showcase-card",
+ },
+ {
+ title: "Border Beam",
+ href: "/docs/components/border-beam",
+ },
+ {
+ title: "Circuit Board",
+ href: "/docs/components/circuit-board",
+ },
+ {
+ title: "Command Menu",
+ href: "/docs/components/command-menu",
+ },
+ {
+ title: "Magnetic Dock",
+ href: "/docs/components/magnetic-dock",
+ },
+ ],
+ },
+ {
+ title: "Dither Effects",
+ items: [
+ {
+ title: "Dither Gradient",
+ href: "/docs/components/dither-gradient",
+ },
+ {
+ title: "Liquid Blob",
+ href: "/docs/components/liquid-blob",
+ },
+ {
+ title: "Noise Texture",
+ href: "/docs/components/noise-texture",
+ },
],
},
],
-}
+};
diff --git a/apps/web/package.json b/apps/web/package.json
index 3dca5d5..4ec162e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,8 +12,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@vercel/analytics": "^1.6.1",
"@workspace/ui": "workspace:*",
"cmdk": "^1.1.1",
+ "framer-motion": "^12.23.26",
+ "lenis": "^1.3.17",
"lucide-react": "^0.475.0",
"next": "16.0.10",
"next-themes": "^0.4.6",
diff --git a/apps/web/public/home.png b/apps/web/public/home.png
new file mode 100644
index 0000000..f4da8ca
Binary files /dev/null and b/apps/web/public/home.png differ
diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json
new file mode 100644
index 0000000..1dcc622
--- /dev/null
+++ b/apps/web/public/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "Componentry - Premium React UI Component Library",
+ "short_name": "Componentry",
+ "description": "Free, open-source React UI component library by Harsh Jadhav. Beautiful, animated, copy-paste components built with Tailwind CSS, TypeScript, and Framer Motion.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#000000",
+ "theme_color": "#000000",
+ "orientation": "portrait-primary",
+ "icons": [
+ {
+ "src": "/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ }
+ ],
+ "categories": ["developer tools", "productivity", "utilities"],
+ "lang": "en",
+ "dir": "ltr",
+ "scope": "/",
+ "prefer_related_applications": false
+}
diff --git a/apps/web/public/preview-wa.jpg b/apps/web/public/preview-wa.jpg
new file mode 100644
index 0000000..0f2aa7a
Binary files /dev/null and b/apps/web/public/preview-wa.jpg differ
diff --git a/apps/web/public/preview.png b/apps/web/public/preview.png
index 35b92f8..7c5f307 100644
Binary files a/apps/web/public/preview.png and b/apps/web/public/preview.png differ
diff --git a/apps/web/public/preview_original.png b/apps/web/public/preview_original.png
new file mode 100644
index 0000000..dfa2d19
Binary files /dev/null and b/apps/web/public/preview_original.png differ
diff --git a/apps/web/public/r/border-beam.json b/apps/web/public/r/border-beam.json
new file mode 100644
index 0000000..5f2bec3
--- /dev/null
+++ b/apps/web/public/r/border-beam.json
@@ -0,0 +1,14 @@
+{
+ "name": "border-beam",
+ "type": "registry:ui",
+ "dependencies": ["framer-motion"],
+ "registryDependencies": [],
+ "description": "A moving gradient beam that travels along the border of its container.",
+ "files": [
+ {
+ "path": "components/ui/border-beam.tsx",
+ "content": "\"use client\"\n\nimport { motion } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\ninterface BorderBeamProps {\n className?: string\n size?: number\n duration?: number\n borderWidth?: number\n anchor?: number\n colorFrom?: string\n colorTo?: string\n delay?: number\n}\n\nexport function BorderBeam({\n className,\n size = 200,\n duration = 15,\n anchor = 90,\n borderWidth = 1.5,\n colorFrom = \"#ffaa40\",\n colorTo = \"#9c40ff\",\n delay = 0,\n}: BorderBeamProps) {\n return (\n \n \n
\n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
diff --git a/apps/web/public/r/circuit-board.json b/apps/web/public/r/circuit-board.json
new file mode 100644
index 0000000..1647db5
--- /dev/null
+++ b/apps/web/public/r/circuit-board.json
@@ -0,0 +1,16 @@
+{
+ "name": "circuit-board",
+ "type": "registry:ui",
+ "dependencies": [
+ "framer-motion"
+ ],
+ "registryDependencies": [],
+ "description": "An interactive circuit board layout component with animated electricity paths that pulse between connected nodes. Fully theme-aware with automatic light/dark mode support.",
+ "files": [
+ {
+ "path": "components/ui/circuit-board.tsx",
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\ninterface CircuitNode {\n id: string\n x: number\n y: number\n label?: string\n icon?: React.ReactNode\n status?: \"active\" | \"inactive\" | \"processing\" | \"error\"\n size?: \"sm\" | \"md\" | \"lg\"\n}\n\ninterface CircuitConnection {\n from: string\n to: string\n animated?: boolean\n bidirectional?: boolean\n color?: string\n pulseColor?: string\n}\n\ninterface CircuitBoardProps extends React.HTMLAttributes {\n nodes: CircuitNode[]\n connections: CircuitConnection[]\n width?: number\n height?: number\n gridSize?: number\n showGrid?: boolean\n gridColor?: string\n traceColor?: string\n pulseColor?: string\n nodeColor?: string\n pulseSpeed?: number\n traceWidth?: number\n /** Force a specific theme variant. Defaults to auto-detect from system. */\n variant?: \"light\" | \"dark\" | \"auto\"\n}\n\nfunction CircuitBoard({\n nodes,\n connections,\n width = 600,\n height = 400,\n gridSize = 20,\n showGrid = true,\n gridColor,\n traceColor,\n pulseColor,\n nodeColor,\n pulseSpeed = 2,\n traceWidth = 2,\n variant = \"auto\",\n className,\n ...props\n}: CircuitBoardProps) {\n // Theme-aware color defaults\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n if (variant !== \"auto\") {\n setIsDark(variant === \"dark\")\n return\n }\n\n // Check for dark class on html/body\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n // Listen for changes\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [variant])\n\n // Compute theme-aware colors\n const computedGridColor = gridColor || (isDark ? \"rgba(163, 163, 163, 0.08)\" : \"rgba(64, 64, 64, 0.12)\")\n const computedTraceColor = traceColor || (isDark ? \"rgba(163, 163, 163, 0.25)\" : \"rgba(64, 64, 64, 0.35)\")\n const computedPulseColor = pulseColor || (isDark ? \"rgba(163, 163, 163, 0.6)\" : \"rgba(64, 64, 64, 0.7)\")\n const computedNodeColor = nodeColor || (isDark ? \"rgba(163, 163, 163, 0.5)\" : \"rgba(64, 64, 64, 0.6)\")\n const nodeMap = React.useMemo(() => {\n return new Map(nodes.map((node) => [node.id, node]))\n }, [nodes])\n\n const getNodeSize = React.useCallback((size?: CircuitNode[\"size\"]) => {\n switch (size) {\n case \"sm\":\n return 24\n case \"lg\":\n return 48\n default:\n return 36\n }\n }, [])\n\n const calculatePath = React.useCallback(\n (from: CircuitNode, to: CircuitNode): string => {\n const fromSize = getNodeSize(from.size) / 2 + 4\n const toSize = getNodeSize(to.size) / 2 + 4\n\n const dx = to.x - from.x\n const dy = to.y - from.y\n\n // Calculate start and end points offset from node centers\n let startX = from.x\n let startY = from.y\n let endX = to.x\n let endY = to.y\n\n // Create circuit-like paths with right angles\n if (Math.abs(dx) > Math.abs(dy)) {\n // Horizontal first, then vertical\n startX = from.x + (dx > 0 ? fromSize : -fromSize)\n endX = to.x + (dx > 0 ? -toSize : toSize)\n const midX = from.x + dx / 2\n return `M ${startX} ${startY} H ${midX} V ${endY} H ${endX}`\n } else {\n // Vertical first, then horizontal\n startY = from.y + (dy > 0 ? fromSize : -fromSize)\n endY = to.y + (dy > 0 ? -toSize : toSize)\n const midY = from.y + dy / 2\n return `M ${startX} ${startY} V ${midY} H ${endX} V ${endY}`\n }\n },\n [getNodeSize]\n )\n\n const getStatusColor = (status?: CircuitNode[\"status\"]) => {\n if (isDark) {\n switch (status) {\n case \"active\":\n return \"rgba(163, 163, 163, 0.7)\"\n case \"processing\":\n return \"rgba(163, 163, 163, 0.5)\"\n case \"error\":\n return \"rgba(120, 113, 108, 0.6)\"\n default:\n return computedNodeColor\n }\n } else {\n switch (status) {\n case \"active\":\n return \"rgba(64, 64, 64, 0.8)\"\n case \"processing\":\n return \"rgba(64, 64, 64, 0.6)\"\n case \"error\":\n return \"rgba(180, 83, 83, 0.7)\"\n default:\n return computedNodeColor\n }\n }\n }\n\n return (\n \n
\n \n {/* Glow filter for the pulse effect */}\n \n \n \n \n \n \n \n\n {/* Grid pattern */}\n {showGrid && (\n \n \n \n )}\n\n {/* Animated gradient for electricity effect */}\n {connections.map((conn, i) => (\n \n \n \n \n \n \n \n ))}\n \n\n {/* Grid background */}\n {showGrid && (\n \n )}\n\n {/* Connection traces */}\n {connections.map((conn, i) => {\n const fromNode = nodeMap.get(conn.from)\n const toNode = nodeMap.get(conn.to)\n if (!fromNode || !toNode) return null\n\n const path = calculatePath(fromNode, toNode)\n const pathLength = 500 // Approximate path length for animation\n\n return (\n \n {/* Base trace */}\n \n\n {/* Animated electricity pulse */}\n {conn.animated !== false && (\n \n )}\n\n {/* Bidirectional pulse */}\n {conn.bidirectional && (\n \n )}\n\n\n \n )\n })}\n \n\n {/* Nodes */}\n {nodes.map((node, i) => {\n const size = getNodeSize(node.size)\n const statusColor = getStatusColor(node.status)\n\n return (\n
\n {/* Node background with pulse */}\n \n\n {/* Node border */}\n
\n\n {/* Inner glow for active nodes */}\n {node.status === \"active\" && (\n \n )}\n\n {/* Node content */}\n \n {node.icon && (\n
{node.icon}
\n )}\n
\n\n {/* Label */}\n {node.label && (\n \n {node.label}\n
\n )}\n \n )\n })}\n
\n )\n}\n\n// Pre-built circuit patterns\ninterface CircuitPatternProps extends Omit {\n pattern: \"data-flow\" | \"network\" | \"processor\" | \"tree\"\n}\n\nfunction CircuitPattern({ pattern, ...props }: CircuitPatternProps) {\n const patterns = {\n \"data-flow\": {\n nodes: [\n { id: \"input\", x: 50, y: 200, label: \"Input\", status: \"active\" as const },\n { id: \"process1\", x: 200, y: 100, label: \"Process\", status: \"processing\" as const },\n { id: \"process2\", x: 200, y: 300, label: \"Validate\", status: \"active\" as const },\n { id: \"merge\", x: 400, y: 200, label: \"Merge\", status: \"active\" as const },\n { id: \"output\", x: 550, y: 200, label: \"Output\", status: \"active\" as const },\n ],\n connections: [\n { from: \"input\", to: \"process1\", animated: true },\n { from: \"input\", to: \"process2\", animated: true },\n { from: \"process1\", to: \"merge\", animated: true },\n { from: \"process2\", to: \"merge\", animated: true },\n { from: \"merge\", to: \"output\", animated: true },\n ],\n },\n network: {\n nodes: [\n { id: \"server\", x: 300, y: 80, label: \"Server\", status: \"active\" as const, size: \"lg\" as const },\n { id: \"client1\", x: 100, y: 200, label: \"Client 1\", status: \"active\" as const },\n { id: \"client2\", x: 300, y: 250, label: \"Client 2\", status: \"processing\" as const },\n { id: \"client3\", x: 500, y: 200, label: \"Client 3\", status: \"active\" as const },\n { id: \"db\", x: 300, y: 350, label: \"Database\", status: \"active\" as const },\n ],\n connections: [\n { from: \"server\", to: \"client1\", bidirectional: true },\n { from: \"server\", to: \"client2\", bidirectional: true },\n { from: \"server\", to: \"client3\", bidirectional: true },\n { from: \"server\", to: \"db\", bidirectional: true },\n ],\n },\n processor: {\n nodes: [\n { id: \"alu\", x: 300, y: 200, label: \"ALU\", status: \"processing\" as const, size: \"lg\" as const },\n { id: \"reg1\", x: 150, y: 100, label: \"R1\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"reg2\", x: 150, y: 200, label: \"R2\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"reg3\", x: 150, y: 300, label: \"R3\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"cache\", x: 450, y: 200, label: \"Cache\", status: \"active\" as const },\n { id: \"out\", x: 550, y: 200, label: \"Out\", status: \"active\" as const, size: \"sm\" as const },\n ],\n connections: [\n { from: \"reg1\", to: \"alu\", animated: true },\n { from: \"reg2\", to: \"alu\", animated: true },\n { from: \"reg3\", to: \"alu\", animated: true },\n { from: \"alu\", to: \"cache\", animated: true },\n { from: \"cache\", to: \"out\", animated: true },\n ],\n },\n tree: {\n nodes: [\n { id: \"root\", x: 300, y: 50, label: \"Root\", status: \"active\" as const },\n { id: \"l1\", x: 150, y: 150, label: \"L1\", status: \"active\" as const },\n { id: \"r1\", x: 450, y: 150, label: \"R1\", status: \"processing\" as const },\n { id: \"l1l\", x: 80, y: 280, label: \"L1L\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"l1r\", x: 220, y: 280, label: \"L1R\", status: \"active\" as const, size: \"sm\" as const },\n { id: \"r1l\", x: 380, y: 280, label: \"R1L\", status: \"error\" as const, size: \"sm\" as const },\n { id: \"r1r\", x: 520, y: 280, label: \"R1R\", status: \"active\" as const, size: \"sm\" as const },\n ],\n connections: [\n { from: \"root\", to: \"l1\", animated: true },\n { from: \"root\", to: \"r1\", animated: true },\n { from: \"l1\", to: \"l1l\", animated: true },\n { from: \"l1\", to: \"l1r\", animated: true },\n { from: \"r1\", to: \"r1l\", animated: true },\n { from: \"r1\", to: \"r1r\", animated: true },\n ],\n },\n }\n\n const selectedPattern = patterns[pattern]\n return \n}\n\n// Interactive circuit node for building custom circuits\ninterface CircuitNodeComponentProps {\n status?: \"active\" | \"inactive\" | \"processing\" | \"error\"\n size?: \"sm\" | \"md\" | \"lg\"\n glowColor?: string\n children?: React.ReactNode\n className?: string\n onClick?: () => void\n}\n\nfunction CircuitNode({\n status = \"inactive\",\n size = \"md\",\n glowColor,\n children,\n className,\n onClick,\n}: CircuitNodeComponentProps) {\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [])\n\n const sizeClasses = {\n sm: \"w-8 h-8\",\n md: \"w-12 h-12\",\n lg: \"w-16 h-16\",\n }\n\n const statusColors = isDark\n ? {\n active: \"rgba(163, 163, 163, 0.7)\",\n inactive: \"rgba(115, 115, 115, 0.4)\",\n processing: \"rgba(163, 163, 163, 0.5)\",\n error: \"rgba(120, 113, 108, 0.6)\",\n }\n : {\n active: \"rgba(64, 64, 64, 0.8)\",\n inactive: \"rgba(100, 100, 100, 0.5)\",\n processing: \"rgba(64, 64, 64, 0.6)\",\n error: \"rgba(180, 83, 83, 0.7)\",\n }\n\n const color = glowColor || statusColors[status]\n\n return (\n \n {/* Pulse animation for processing state */}\n {status === \"processing\" && (\n \n )}\n\n {/* Active glow */}\n {status === \"active\" && (\n
\n )}\n\n {/* Error pulse */}\n {status === \"error\" && (\n \n )}\n\n \n {children}\n
\n \n )\n}\n\n// Animated trace line component for custom layouts\ninterface CircuitTraceProps {\n path: string\n animated?: boolean\n color?: string\n pulseColor?: string\n width?: number\n pulseSpeed?: number\n}\n\nfunction CircuitTrace({\n path,\n animated = true,\n color,\n pulseColor,\n width = 2,\n pulseSpeed = 2,\n}: CircuitTraceProps) {\n const [isDark, setIsDark] = React.useState(true)\n\n React.useEffect(() => {\n const checkTheme = () => {\n const isDarkMode = document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n setIsDark(isDarkMode)\n }\n\n checkTheme()\n\n const observer = new MutationObserver(checkTheme)\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] })\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] })\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n mediaQuery.addEventListener(\"change\", checkTheme)\n\n return () => {\n observer.disconnect()\n mediaQuery.removeEventListener(\"change\", checkTheme)\n }\n }, [])\n\n const computedColor = color || (isDark ? \"rgba(163, 163, 163, 0.25)\" : \"rgba(64, 64, 64, 0.35)\")\n const computedPulseColor = pulseColor || (isDark ? \"rgba(163, 163, 163, 0.6)\" : \"rgba(64, 64, 64, 0.7)\")\n const pathLength = 500\n\n return (\n \n \n \n \n \n \n \n \n \n \n\n {/* Base trace */}\n \n\n {/* Animated pulse */}\n {animated && (\n \n )}\n \n )\n}\n\nexport {\n CircuitBoard,\n CircuitPattern,\n CircuitNode,\n CircuitTrace,\n type CircuitNode as CircuitNodeType,\n type CircuitConnection,\n type CircuitBoardProps,\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/web/public/r/command-menu.json b/apps/web/public/r/command-menu.json
new file mode 100644
index 0000000..4d85768
--- /dev/null
+++ b/apps/web/public/r/command-menu.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "command-menu",
+ "type": "registry:ui",
+ "title": "Command Menu",
+ "description": "A macOS Spotlight-style command menu with animated search, keyboard navigation, and customizable groups. Features backdrop blur, smooth animations, and full keyboard support.",
+ "dependencies": ["cmdk", "framer-motion", "lucide-react"],
+ "devDependencies": [],
+ "registryDependencies": [],
+ "files": [
+ {
+ "path": "components/ui/command-menu.tsx",
+ "type": "registry:ui",
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactDOM from \"react-dom\"\nimport { Command } from \"cmdk\"\nimport { Search, ArrowRight, X } from \"lucide-react\"\nimport { motion, AnimatePresence } from \"framer-motion\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface CommandMenuItem {\n id: string\n title: string\n group?: string\n icon?: React.ReactNode\n onSelect?: () => void\n}\n\nexport interface CommandMenuGroup {\n title: string\n items: CommandMenuItem[]\n}\n\nexport interface CommandMenuProps {\n groups: CommandMenuGroup[]\n placeholder?: string\n emptyMessage?: string\n brandName?: string\n triggerClassName?: string\n triggerLabel?: string\n shortcutKey?: string\n open?: boolean\n onOpenChange?: (open: boolean) => void\n}\n\nfunction CommandMenu({\n groups,\n placeholder = \"Search...\",\n emptyMessage = \"No results found\",\n brandName = \"Command Menu\",\n triggerClassName,\n triggerLabel = \"Search...\",\n shortcutKey = \"K\",\n open: controlledOpen,\n onOpenChange,\n}: CommandMenuProps) {\n const [internalOpen, setInternalOpen] = React.useState(false)\n const [query, setQuery] = React.useState(\"\")\n const inputRef = React.useRef(null)\n\n const isControlled = controlledOpen !== undefined\n const open = isControlled ? controlledOpen : internalOpen\n const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen\n\n React.useEffect(() => {\n const down = (e: KeyboardEvent) => {\n if (e.key.toLowerCase() === shortcutKey.toLowerCase() && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n setOpen(!open)\n }\n if (e.key === \"Escape\") {\n setOpen(false)\n }\n }\n\n document.addEventListener(\"keydown\", down)\n return () => document.removeEventListener(\"keydown\", down)\n }, [open, setOpen, shortcutKey])\n\n React.useEffect(() => {\n if (open) {\n setTimeout(() => {\n inputRef.current?.focus()\n }, 0)\n } else {\n setQuery(\"\")\n }\n }, [open])\n\n const handleSelect = React.useCallback((item: CommandMenuItem) => {\n setOpen(false)\n item.onSelect?.()\n }, [setOpen])\n\n return (\n <>\n setOpen(true)}\n className={cn(\n \"group inline-flex items-center gap-2 whitespace-nowrap transition-all duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"border border-input/50 hover:border-input hover:bg-accent/50\",\n \"px-3 py-2 relative h-9 w-full justify-start rounded-lg bg-muted/30\",\n \"text-sm font-normal text-muted-foreground sm:pr-12 md:w-40 lg:w-56\",\n triggerClassName\n )}\n >\n \n {triggerLabel} \n Search \n \n ⌘ {shortcutKey}\n \n \n\n {typeof document !== \"undefined\" && ReactDOM.createPortal(\n \n {open && (\n <>\n setOpen(false)}\n />\n \n \n \n
\n \n
\n
\n {query && (\n
setQuery(\"\")}\n className=\"rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors\"\n >\n Clear\n \n )}\n
\n ESC\n \n
\n\n \n \n \n \n
\n {emptyMessage}
\n Try searching for something else
\n \n\n {groups.map((group) => (\n \n {group.items.map((item) => (\n handleSelect(item)}\n className=\"group/item relative flex cursor-pointer select-none items-center gap-3 rounded-xl px-3 py-2.5 text-sm outline-none transition-colors hover:bg-accent/70 hover:text-accent-foreground aria-[selected='true']:bg-accent aria-[selected='true']:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50\"\n >\n \n {item.icon}\n
\n \n {item.title} \n {group.title} \n
\n \n \n ))}\n \n ))}\n \n\n \n
\n \n ↑↓ \n Navigate\n \n \n ↵ \n Select\n \n
\n
{brandName} \n
\n \n \n >\n )}\n ,\n document.body\n )}\n >\n )\n}\n\nCommandMenu.displayName = \"CommandMenu\"\n\nexport { CommandMenu }\n"
+ }
+ ],
+ "tailwind": {},
+ "cssVars": {},
+ "meta": {}
+}
diff --git a/apps/web/public/r/dither-gradient.json b/apps/web/public/r/dither-gradient.json
new file mode 100644
index 0000000..dbcb782
--- /dev/null
+++ b/apps/web/public/r/dither-gradient.json
@@ -0,0 +1,14 @@
+{
+ "name": "dither-gradient",
+ "type": "registry:ui",
+ "dependencies": [],
+ "registryDependencies": [],
+ "description": "An animated dithered gradient background effect using canvas with Bayer matrix dithering.",
+ "files": [
+ {
+ "path": "components/ui/dither-gradient.tsx",
+ "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface DitherGradientProps {\n className?: string\n colorFrom?: string\n colorTo?: string\n colorMid?: string\n intensity?: number\n speed?: number\n angle?: number\n}\n\nexport function DitherGradient({\n className,\n colorFrom = \"#4f46e5\",\n colorTo = \"#ec4899\",\n colorMid = \"#a855f7\",\n intensity = 0.15,\n speed = 3,\n angle = 45,\n}: DitherGradientProps) {\n const canvasRef = useRef(null)\n const animationRef = useRef(0)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n\n const resize = () => {\n const rect = canvas.getBoundingClientRect()\n canvas.width = rect.width\n canvas.height = rect.height\n }\n\n resize()\n window.addEventListener(\"resize\", resize)\n\n let time = 0\n const bayerMatrix = [\n [0, 8, 2, 10],\n [12, 4, 14, 6],\n [3, 11, 1, 9],\n [15, 7, 13, 5],\n ]\n\n const hexToRgb = (hex: string) => {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return { r: 0, g: 0, b: 0 }\n return {\n r: parseInt(result[1]!, 16),\n g: parseInt(result[2]!, 16),\n b: parseInt(result[3]!, 16),\n }\n }\n\n const smoothstep = (t: number) => t * t * (3 - 2 * t)\n\n const animate = () => {\n const { width, height } = canvas\n const imageData = ctx.createImageData(width, height)\n const data = imageData.data\n\n const from = hexToRgb(colorFrom)\n const mid = hexToRgb(colorMid)\n const to = hexToRgb(colorTo)\n const rad = (angle * Math.PI) / 180\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const normalizedX = x / width\n const normalizedY = y / height\n \n const gradientPos = \n (normalizedX * Math.cos(rad) + normalizedY * Math.sin(rad)) * 0.8 + \n 0.1 + \n Math.sin(time * speed * 0.0008) * 0.1\n\n const clampedPos = Math.max(0, Math.min(1, gradientPos))\n\n let r: number, g: number, b: number\n if (clampedPos < 0.5) {\n const t = smoothstep(clampedPos * 2)\n r = from.r + (mid.r - from.r) * t\n g = from.g + (mid.g - from.g) * t\n b = from.b + (mid.b - from.b) * t\n } else {\n const t = smoothstep((clampedPos - 0.5) * 2)\n r = mid.r + (to.r - mid.r) * t\n g = mid.g + (to.g - mid.g) * t\n b = mid.b + (to.b - mid.b) * t\n }\n\n const threshold = (bayerMatrix[y % 4]![x % 4]! / 16 - 0.5) * intensity * 180\n const noise = (Math.random() - 0.5) * intensity * 60\n\n const idx = (y * width + x) * 4\n data[idx] = Math.min(255, Math.max(0, r + threshold + noise))\n data[idx + 1] = Math.min(255, Math.max(0, g + threshold + noise))\n data[idx + 2] = Math.min(255, Math.max(0, b + threshold + noise))\n data[idx + 3] = 255\n }\n }\n\n ctx.putImageData(imageData, 0, 0)\n time += 16\n animationRef.current = requestAnimationFrame(animate)\n }\n\n animate()\n\n return () => {\n window.removeEventListener(\"resize\", resize)\n cancelAnimationFrame(animationRef.current)\n }\n }, [colorFrom, colorTo, colorMid, intensity, speed, angle])\n\n return (\n \n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
diff --git a/apps/web/public/r/flight-status-card.json b/apps/web/public/r/flight-status-card.json
index ba0fb5a..4e0f4c8 100644
--- a/apps/web/public/r/flight-status-card.json
+++ b/apps/web/public/r/flight-status-card.json
@@ -4,14 +4,14 @@
"type": "registry:ui",
"title": "Flight Status Card",
"description": "A detailed flight status widget with dot-matrix airport codes, progress tracking, and ETA information.",
- "dependencies": [],
+ "dependencies": ["framer-motion"],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/ui/flight-status-card.tsx",
"type": "registry:ui",
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nconst DOT_MATRIX: Record = {\n Y: [[1,0,0,0,1],[0,1,0,1,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]],\n Z: [[1,1,1,1,1],[0,0,0,1,0],[0,0,1,0,0],[0,1,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n H: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n N: [[1,0,0,0,1],[1,1,0,0,1],[1,0,1,0,1],[1,0,0,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n D: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0]],\n A: [[0,0,1,0,0],[0,1,0,1,0],[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n B: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0]],\n C: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,1],[0,1,1,1,0]],\n E: [[1,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n F: [[1,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0]],\n G: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,0],[1,0,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n I: [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[1,1,1,1,1]],\n J: [[0,0,1,1,1],[0,0,0,1,0],[0,0,0,1,0],[0,0,0,1,0],[1,0,0,1,0],[1,0,0,1,0],[0,1,1,0,0]],\n K: [[1,0,0,0,1],[1,0,0,1,0],[1,0,1,0,0],[1,1,0,0,0],[1,0,1,0,0],[1,0,0,1,0],[1,0,0,0,1]],\n L: [[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n M: [[1,0,0,0,1],[1,1,0,1,1],[1,0,1,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n O: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n P: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0]],\n Q: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,1,0],[0,1,1,0,1]],\n R: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,1,0,0],[1,0,0,1,0],[1,0,0,0,1]],\n S: [[0,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[0,1,1,1,0],[0,0,0,0,1],[0,0,0,0,1],[1,1,1,1,0]],\n T: [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]],\n U: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n V: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,0,1,0],[0,1,0,1,0],[0,0,1,0,0]],\n W: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,1,0,1],[1,1,0,1,1],[1,0,0,0,1]],\n X: [[1,0,0,0,1],[0,1,0,1,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,1,0,1,0],[1,0,0,0,1]]\n}\n\ninterface DotMatrixCharProps {\n char: string\n dotSize?: number\n gap?: number\n activeColor?: string\n inactiveColor?: string\n className?: string\n}\n\nfunction DotMatrixChar({\n char,\n dotSize = 4,\n gap = 2,\n activeColor = \"#b4f54e\",\n inactiveColor = \"rgba(180, 245, 78, 0.1)\",\n className,\n}: DotMatrixCharProps) {\n const matrix = DOT_MATRIX[char.toUpperCase()] ?? DOT_MATRIX[\"A\"]!\n const width = 5 * dotSize + 4 * gap\n const height = 7 * dotSize + 6 * gap\n\n return (\n \n {matrix!.map((row, rowIndex) =>\n row.map((cell, colIndex) => (\n \n ))\n )}\n \n )\n}\n\ninterface DotMatrixTextProps {\n text: string\n dotSize?: number\n gap?: number\n charGap?: number\n activeColor?: string\n inactiveColor?: string\n className?: string\n}\n\nfunction DotMatrixText({\n text,\n dotSize = 4,\n gap = 2,\n charGap = 8,\n activeColor = \"#b4f54e\",\n inactiveColor = \"rgba(180, 245, 78, 0.1)\",\n className,\n}: DotMatrixTextProps) {\n return (\n \n {text.split(\"\").map((char, index) => (\n \n ))}\n
\n )\n}\n\nfunction HalftonePattern({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n )\n}\n\nfunction PlaneIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\nfunction SwapIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\ninterface FlightStatusCardProps {\n departureCode?: string\n arrivalCode?: string\n departureCity?: string\n arrivalCity?: string\n departureTime?: string\n arrivalTime?: string\n eta?: string\n timezone?: string\n nextEvent?: string\n nextEventTime?: string\n progress?: number\n remainingTime?: string\n className?: string\n}\n\nfunction FlightStatusCard({\n departureCode = \"YYZ\",\n arrivalCode = \"HND\",\n departureCity = \"Toronto\",\n arrivalCity = \"Tokyo\",\n departureTime = \"MON, 6:14 PM\",\n arrivalTime = \"TUE, 7:14 AM\",\n eta = \"ETA 2:15 PM\",\n timezone = \"Tokyo Time\",\n nextEvent = \"DINNER IN\",\n nextEventTime = \"2:34H\",\n progress = 45,\n remainingTime = \"-7H 01M\",\n className,\n}: FlightStatusCardProps) {\n return (\n \n
\n
\n
\n
\n
\n \n {departureCity} \n {departureTime} \n
\n
\n
\n \n {arrivalCity} \n {arrivalTime} \n
\n
\n
\n
\n {eta} \n \n \n \n
\n
{timezone} \n
{nextEvent} {nextEventTime} \n
\n
\n
\n
\n
\n )\n}\n\nfunction FlightStatusCardLight({\n departureCode = \"YYZ\",\n arrivalCode = \"HND\",\n departureCity = \"Toronto\",\n arrivalCity = \"Tokyo\",\n departureTime = \"MON, 6:14 PM\",\n arrivalTime = \"TUE, 7:14 AM\",\n eta = \"ETA 2:15 PM\",\n timezone = \"Tokyo Time\",\n nextEvent = \"DINNER IN\",\n nextEventTime = \"2:34H\",\n progress = 45,\n remainingTime = \"-7H 01M\",\n className,\n}: FlightStatusCardProps) {\n return (\n \n
\n
\n
\n
\n
\n \n {departureCity} \n {departureTime} \n
\n
\n
\n \n {arrivalCity} \n {arrivalTime} \n
\n
\n
\n
\n {eta} \n \n \n \n
\n
{timezone} \n
{nextEvent} {nextEventTime} \n
\n
\n
\n
\n
\n )\n}\n\nfunction HalftoneLightPattern({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n )\n}\n\nfunction FlightStatusCardAdaptive(props: FlightStatusCardProps) {\n return (\n <>\n \n \n
\n \n \n
\n >\n )\n}\n\nexport { FlightStatusCard, FlightStatusCardLight, FlightStatusCardAdaptive, DotMatrixText, DotMatrixChar }\n"
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { motion } from \"framer-motion\"\n\nconst DOT_MATRIX: Record = {\n Y: [[1,0,0,0,1],[0,1,0,1,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]],\n Z: [[1,1,1,1,1],[0,0,0,1,0],[0,0,1,0,0],[0,1,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n H: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n N: [[1,0,0,0,1],[1,1,0,0,1],[1,0,1,0,1],[1,0,0,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n D: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0]],\n A: [[0,0,1,0,0],[0,1,0,1,0],[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n B: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0]],\n C: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,1],[0,1,1,1,0]],\n E: [[1,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n F: [[1,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0]],\n G: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,0],[1,0,1,1,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n I: [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[1,1,1,1,1]],\n J: [[0,0,1,1,1],[0,0,0,1,0],[0,0,0,1,0],[0,0,0,1,0],[1,0,0,1,0],[1,0,0,1,0],[0,1,1,0,0]],\n K: [[1,0,0,0,1],[1,0,0,1,0],[1,0,1,0,0],[1,1,0,0,0],[1,0,1,0,0],[1,0,0,1,0],[1,0,0,0,1]],\n L: [[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]],\n M: [[1,0,0,0,1],[1,1,0,1,1],[1,0,1,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1]],\n O: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n P: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0]],\n Q: [[0,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,1,0],[0,1,1,0,1]],\n R: [[1,1,1,1,0],[1,0,0,0,1],[1,0,0,0,1],[1,1,1,1,0],[1,0,1,0,0],[1,0,0,1,0],[1,0,0,0,1]],\n S: [[0,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[0,1,1,1,0],[0,0,0,0,1],[0,0,0,0,1],[1,1,1,1,0]],\n T: [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]],\n U: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]],\n V: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,0,1,0],[0,1,0,1,0],[0,0,1,0,0]],\n W: [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,1,0,1],[1,1,0,1,1],[1,0,0,0,1]],\n X: [[1,0,0,0,1],[0,1,0,1,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,1,0,1,0],[1,0,0,0,1]]\n}\n\ninterface DotMatrixCharProps {\n char: string\n dotSize?: number\n gap?: number\n activeColor?: string\n inactiveColor?: string\n className?: string\n delay?: number\n}\n\nfunction DotMatrixChar({\n char,\n dotSize = 4,\n gap = 2,\n activeColor = \"#b4f54e\",\n inactiveColor = \"rgba(180, 245, 78, 0.1)\",\n className,\n delay = 0,\n}: DotMatrixCharProps) {\n const matrix = DOT_MATRIX[char.toUpperCase()] ?? DOT_MATRIX[\"A\"]!\n const width = 5 * dotSize + 4 * gap\n const height = 7 * dotSize + 6 * gap\n\n return (\n \n {matrix!.map((row, rowIndex) =>\n row.map((cell, colIndex) => (\n \n ))\n )}\n \n )\n}\n\ninterface DotMatrixTextProps {\n text: string\n dotSize?: number\n gap?: number\n charGap?: number\n activeColor?: string\n inactiveColor?: string\n className?: string\n}\n\nfunction DotMatrixText({\n text,\n dotSize = 4,\n gap = 2,\n charGap = 8,\n activeColor = \"#b4f54e\",\n inactiveColor = \"rgba(180, 245, 78, 0.1)\",\n className,\n}: DotMatrixTextProps) {\n return (\n \n {text.split(\"\").map((char, index) => (\n \n ))}\n
\n )\n}\n\nfunction HalftonePattern({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n )\n}\n\nfunction PlaneIcon({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\nfunction SwapIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\ninterface FlightStatusCardProps {\n departureCode?: string\n arrivalCode?: string\n departureCity?: string\n arrivalCity?: string\n departureTime?: string\n arrivalTime?: string\n eta?: string\n timezone?: string\n nextEvent?: string\n nextEventTime?: string\n progress?: number\n remainingTime?: string\n className?: string\n}\n\nfunction FlightStatusCard({\n departureCode = \"YYZ\",\n arrivalCode = \"HND\",\n departureCity = \"Toronto\",\n arrivalCity = \"Tokyo\",\n departureTime = \"MON, 6:14 PM\",\n arrivalTime = \"TUE, 7:14 AM\",\n eta = \"ETA 2:15 PM\",\n timezone = \"Tokyo Time\",\n nextEvent = \"DINNER IN\",\n nextEventTime = \"2:34H\",\n progress = 45,\n remainingTime = \"-7H 01M\",\n className,\n}: FlightStatusCardProps) {\n return (\n \n \n \n
\n
\n
\n \n {departureCity} \n {departureTime} \n
\n
\n
\n \n {arrivalCity} \n {arrivalTime} \n
\n
\n
\n \n {eta} \n \n \n \n
\n {timezone} \n {nextEvent} {nextEventTime} \n \n
\n
\n
\n \n )\n}\n\nfunction FlightStatusCardLight({\n departureCode = \"YYZ\",\n arrivalCode = \"HND\",\n departureCity = \"Toronto\",\n arrivalCity = \"Tokyo\",\n departureTime = \"MON, 6:14 PM\",\n arrivalTime = \"TUE, 7:14 AM\",\n eta = \"ETA 2:15 PM\",\n timezone = \"Tokyo Time\",\n nextEvent = \"DINNER IN\",\n nextEventTime = \"2:34H\",\n progress = 45,\n remainingTime = \"-7H 01M\",\n className,\n}: FlightStatusCardProps) {\n return (\n \n \n \n
\n
\n
\n \n {departureCity} \n {departureTime} \n
\n
\n
\n \n {arrivalCity} \n {arrivalTime} \n
\n
\n
\n \n {eta} \n \n \n \n
\n {timezone} \n {nextEvent} {nextEventTime} \n \n
\n
\n
\n \n )\n}\n\nfunction HalftoneLightPattern({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n )\n}\n\nfunction FlightStatusCardAdaptive(props: FlightStatusCardProps) {\n return (\n <>\n \n \n
\n \n \n
\n >\n )\n}\n\nexport { FlightStatusCard, FlightStatusCardLight, FlightStatusCardAdaptive, DotMatrixText, DotMatrixChar }\n"
}
],
"tailwind": {},
diff --git a/apps/web/public/r/interactive-hover-button.json b/apps/web/public/r/interactive-hover-button.json
new file mode 100644
index 0000000..9894173
--- /dev/null
+++ b/apps/web/public/r/interactive-hover-button.json
@@ -0,0 +1,14 @@
+{
+ "name": "interactive-hover-button",
+ "type": "registry:ui",
+ "dependencies": ["lucide-react"],
+ "registryDependencies": [],
+ "description": "A button that reveals text and an arrow icon on hover.",
+ "files": [
+ {
+ "path": "components/ui/interactive-hover-button.tsx",
+ "content": "\"use client\"\n\nimport React from \"react\"\nimport { ArrowRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface InteractiveHoverButtonProps\n extends React.ButtonHTMLAttributes {\n text?: string\n}\n\nconst InteractiveHoverButton = React.forwardRef<\n HTMLButtonElement,\n InteractiveHoverButtonProps\n>(({ text = \"Button\", className, ...props }, ref) => {\n return (\n \n \n {text}\n \n \n
\n \n )\n})\n\nInteractiveHoverButton.displayName = \"InteractiveHoverButton\"\n\nexport { InteractiveHoverButton }\n",
+ "type": "registry:ui"
+ }
+ ]
+}
diff --git a/apps/web/public/r/liquid-blob.json b/apps/web/public/r/liquid-blob.json
new file mode 100644
index 0000000..dab19f4
--- /dev/null
+++ b/apps/web/public/r/liquid-blob.json
@@ -0,0 +1,14 @@
+{
+ "name": "liquid-blob",
+ "type": "registry:ui",
+ "dependencies": ["framer-motion"],
+ "registryDependencies": [],
+ "description": "Animated liquid morphing blob shapes with mouse interaction that create a beautiful organic background effect.",
+ "files": [
+ {
+ "path": "components/ui/liquid-blob.tsx",
+ "content": "\"use client\"\n\nimport { motion, useMotionValue, useSpring } from \"framer-motion\"\nimport { useEffect, useRef } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface LiquidBlobProps {\n className?: string\n color?: string\n secondaryColor?: string\n size?: number\n blur?: number\n speed?: number\n opacity?: number\n interactive?: boolean\n}\n\nexport function LiquidBlob({\n className,\n color = \"#8b5cf6\",\n secondaryColor = \"#ec4899\",\n size = 300,\n blur = 60,\n speed = 8,\n opacity = 0.7,\n interactive = true,\n}: LiquidBlobProps) {\n const containerRef = useRef(null)\n const mouseX = useMotionValue(0)\n const mouseY = useMotionValue(0)\n \n const springConfig = { damping: 25, stiffness: 150 }\n const smoothX = useSpring(mouseX, springConfig)\n const smoothY = useSpring(mouseY, springConfig)\n\n useEffect(() => {\n if (!interactive) return\n \n const container = containerRef.current\n if (!container) return\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect()\n const x = e.clientX - rect.left - rect.width / 2\n const y = e.clientY - rect.top - rect.height / 2\n mouseX.set(x * 0.15)\n mouseY.set(y * 0.15)\n }\n\n const handleMouseLeave = () => {\n mouseX.set(0)\n mouseY.set(0)\n }\n\n container.addEventListener(\"mousemove\", handleMouseMove)\n container.addEventListener(\"mouseleave\", handleMouseLeave)\n\n return () => {\n container.removeEventListener(\"mousemove\", handleMouseMove)\n container.removeEventListener(\"mouseleave\", handleMouseLeave)\n }\n }, [interactive, mouseX, mouseY])\n\n return (\n \n \n \n \n
\n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
diff --git a/apps/web/public/r/magnetic-dock.json b/apps/web/public/r/magnetic-dock.json
new file mode 100644
index 0000000..752921e
--- /dev/null
+++ b/apps/web/public/r/magnetic-dock.json
@@ -0,0 +1,16 @@
+{
+ "name": "magnetic-dock",
+ "type": "registry:ui",
+ "dependencies": [
+ "framer-motion"
+ ],
+ "registryDependencies": [],
+ "description": "A macOS-style magnetic dock with smooth scaling animations, spring physics, and premium micro-interactions. Supports both light and dark modes.",
+ "files": [
+ {
+ "path": "components/ui/magnetic-dock.tsx",
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useMotionValue, useSpring, useTransform, AnimatePresence, type MotionValue } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\ninterface MagneticDockProps {\n /** Array of dock items */\n items: DockItemData[]\n /** Size of icons in pixels */\n iconSize?: number\n /** Maximum scale on hover */\n maxScale?: number\n /** Distance of magnetic effect in pixels */\n magneticDistance?: number\n /** Show labels on hover */\n showLabels?: boolean\n /** Dock position */\n position?: \"bottom\" | \"top\" | \"left\" | \"right\"\n /** Background style */\n variant?: \"glass\" | \"solid\" | \"transparent\"\n /** Custom class name */\n className?: string\n}\n\ninterface DockItemData {\n /** Unique identifier */\n id: string\n /** Display label */\n label: string\n /** Icon component or image URL */\n icon: React.ReactNode\n /** Click handler */\n onClick?: () => void\n /** Whether item is active */\n isActive?: boolean\n /** Badge count */\n badge?: number\n}\n\ninterface DockItemProps {\n item: DockItemData\n mouseX: MotionValue\n iconSize: number\n maxScale: number\n magneticDistance: number\n showLabels: boolean\n isVertical: boolean\n}\n\nfunction DockItem({\n item,\n mouseX,\n iconSize,\n maxScale,\n magneticDistance,\n showLabels,\n isVertical,\n}: DockItemProps) {\n const ref = React.useRef(null)\n const [isHovered, setIsHovered] = React.useState(false)\n\n // Calculate distance from mouse to center of item\n const distance = useTransform(mouseX, (val: number) => {\n if (!ref.current) return magneticDistance + 1\n const rect = ref.current.getBoundingClientRect()\n const center = isVertical\n ? rect.top + rect.height / 2\n : rect.left + rect.width / 2\n return val - center\n })\n\n // Scale based on distance - closer = larger\n const scale = useTransform(distance, [-magneticDistance, 0, magneticDistance], [1, maxScale, 1])\n\n // Apply spring physics for smooth animation\n const springConfig = { damping: 20, stiffness: 300, mass: 0.5 }\n const smoothScale = useSpring(scale, springConfig)\n\n // Calculate the size based on scale\n const size = useTransform(smoothScale, (s) => s * iconSize)\n\n // Floating effect\n const y = useTransform(smoothScale, (s) => (s - 1) * -10)\n const smoothY = useSpring(y, springConfig)\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\n \"relative flex items-center justify-center\",\n \"rounded-2xl transition-colors duration-200\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 dark:focus-visible:ring-white/50\",\n item.isActive && \"bg-neutral-200/50 dark:bg-white/10\"\n )}\n style={{\n width: size,\n height: size,\n y: isVertical ? 0 : smoothY,\n x: isVertical ? smoothY : 0,\n }}\n whileTap={{ scale: 0.9 }}\n >\n {/* Icon Container */}\n \n {/* Icon */}\n \n {item.icon}\n
\n\n {/* Shine effect */}\n \n \n\n {/* Badge */}\n \n {item.badge !== undefined && item.badge > 0 && (\n \n {item.badge > 99 ? \"99+\" : item.badge}\n \n )}\n \n\n {/* Active Indicator */}\n \n {item.isActive && (\n \n )}\n \n\n {/* Tooltip */}\n \n {showLabels && isHovered && (\n \n {item.label}\n {/* Tooltip arrow */}\n
\n \n )}\n \n\n {/* Hover glow */}\n \n \n )\n}\n\nfunction MagneticDock({\n items,\n iconSize = 56,\n maxScale = 1.5,\n magneticDistance = 150,\n showLabels = true,\n position = \"bottom\",\n variant = \"glass\",\n className,\n}: MagneticDockProps) {\n const mousePosition = useMotionValue(Infinity)\n const isVertical = position === \"left\" || position === \"right\"\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (isVertical) {\n mousePosition.set(e.clientY)\n } else {\n mousePosition.set(e.clientX)\n }\n },\n [mousePosition, isVertical]\n )\n\n const handleMouseLeave = () => {\n mousePosition.set(Infinity)\n }\n\n const variantStyles = {\n glass: cn(\n \"bg-white/80 dark:bg-neutral-900/80\",\n \"backdrop-blur-xl backdrop-saturate-150\",\n \"border border-neutral-200 dark:border-neutral-700\"\n ),\n solid: cn(\n \"bg-neutral-100 dark:bg-neutral-900\",\n \"border border-neutral-300 dark:border-neutral-700\"\n ),\n transparent: \"bg-transparent border-0\",\n }\n\n const positionStyles = {\n bottom: \"flex-row\",\n top: \"flex-row\",\n left: \"flex-col\",\n right: \"flex-col\",\n }\n\n return (\n \n {items.map((item) => (\n \n ))}\n \n )\n}\n\n// Preset icons for common use cases\nfunction DockIconHome({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconSearch({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconFolder({ className }: { className?: string }) {\n return (\n \n \n \n )\n}\n\nfunction DockIconMail({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconMusic({ className }: { className?: string }) {\n return (\n \n \n \n \n \n )\n}\n\nfunction DockIconSettings({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nfunction DockIconTrash({ className }: { className?: string }) {\n return (\n \n \n \n \n )\n}\n\nexport {\n MagneticDock,\n DockIconHome,\n DockIconSearch,\n DockIconFolder,\n DockIconMail,\n DockIconMusic,\n DockIconSettings,\n DockIconTrash,\n type MagneticDockProps,\n type DockItemData,\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/web/public/r/noise-texture.json b/apps/web/public/r/noise-texture.json
new file mode 100644
index 0000000..84de862
--- /dev/null
+++ b/apps/web/public/r/noise-texture.json
@@ -0,0 +1,14 @@
+{
+ "name": "noise-texture",
+ "type": "registry:ui",
+ "dependencies": [],
+ "registryDependencies": [],
+ "description": "An animated noise/grain texture overlay effect with customizable grain size and blend modes.",
+ "files": [
+ {
+ "path": "components/ui/noise-texture.tsx",
+ "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface NoiseTextureProps {\n className?: string\n opacity?: number\n speed?: number\n grain?: \"fine\" | \"medium\" | \"coarse\"\n blend?: \"overlay\" | \"soft-light\" | \"multiply\" | \"screen\" | \"normal\"\n animate?: boolean\n}\n\nexport function NoiseTexture({\n className,\n opacity = 0.4,\n speed = 10,\n grain = \"medium\",\n blend = \"normal\",\n animate = true,\n}: NoiseTextureProps) {\n const canvasRef = useRef(null)\n const animationRef = useRef(0)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n\n const grainSizes: Record = {\n fine: 1,\n medium: 2,\n coarse: 3,\n }\n const grainSize = grainSizes[grain] ?? 2\n\n const resize = () => {\n const rect = canvas.getBoundingClientRect()\n const dpr = Math.min(window.devicePixelRatio || 1, 2)\n canvas.width = Math.ceil(rect.width / grainSize) * dpr\n canvas.height = Math.ceil(rect.height / grainSize) * dpr\n canvas.style.width = `${rect.width}px`\n canvas.style.height = `${rect.height}px`\n }\n\n resize()\n window.addEventListener(\"resize\", resize)\n\n const renderNoise = () => {\n const { width, height } = canvas\n if (width === 0 || height === 0) return\n \n const imageData = ctx.createImageData(width, height)\n const data = imageData.data\n\n for (let i = 0; i < data.length; i += 4) {\n const value = Math.random() * 255\n data[i] = value\n data[i + 1] = value\n data[i + 2] = value\n data[i + 3] = 255\n }\n\n ctx.putImageData(imageData, 0, 0)\n }\n\n renderNoise()\n\n if (animate) {\n const animateNoise = () => {\n renderNoise()\n animationRef.current = setTimeout(() => {\n requestAnimationFrame(animateNoise)\n }, 1000 / speed)\n }\n animateNoise()\n }\n\n return () => {\n window.removeEventListener(\"resize\", resize)\n if (animationRef.current) {\n clearTimeout(animationRef.current as NodeJS.Timeout)\n }\n }\n }, [grain, speed, animate])\n\n const grainSizes: Record = {\n fine: 1,\n medium: 2,\n coarse: 3,\n }\n\n return (\n \n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
diff --git a/apps/web/public/r/pulsating-button.json b/apps/web/public/r/pulsating-button.json
new file mode 100644
index 0000000..8ab9017
--- /dev/null
+++ b/apps/web/public/r/pulsating-button.json
@@ -0,0 +1,35 @@
+{
+ "name": "pulsating-button",
+ "type": "registry:ui",
+ "dependencies": [],
+ "registryDependencies": [],
+ "description": "A button with a pulsating glow effect.",
+ "files": [
+ {
+ "path": "components/ui/pulsating-button.tsx",
+ "content": "\"use client\"\n\nimport React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface PulsatingButtonProps\n extends React.ButtonHTMLAttributes {\n pulseColor?: string\n duration?: string\n}\n\nconst PulsatingButton = React.forwardRef<\n HTMLButtonElement,\n PulsatingButtonProps\n>(\n (\n {\n className,\n children,\n pulseColor = \"#0096ff\",\n duration = \"1.5s\",\n ...props\n },\n ref,\n ) => {\n return (\n \n {children}
\n
\n \n )\n },\n)\n\nPulsatingButton.displayName = \"PulsatingButton\"\n\nexport { PulsatingButton }\n",
+ "type": "registry:ui"
+ }
+ ],
+ "tailwind": {
+ "config": {
+ "theme": {
+ "extend": {
+ "animation": {
+ "pulse": "pulse var(--duration) ease-out infinite"
+ },
+ "keyframes": {
+ "pulse": {
+ "0%": {
+ "box-shadow": "0 0 0 0 var(--pulse-color)"
+ },
+ "100%": {
+ "box-shadow": "0 0 0 8px rgba(0, 0, 0, 0)"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apps/web/public/r/shimmer-button.json b/apps/web/public/r/shimmer-button.json
new file mode 100644
index 0000000..635bdcf
--- /dev/null
+++ b/apps/web/public/r/shimmer-button.json
@@ -0,0 +1,47 @@
+{
+ "name": "shimmer-button",
+ "type": "registry:ui",
+ "dependencies": [],
+ "registryDependencies": [],
+ "description": "A button with a shimmering light effect.",
+ "files": [
+ {
+ "path": "components/ui/shimmer-button.tsx",
+ "content": "\"use client\"\n\nimport React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface ShimmerButtonProps\n extends React.ButtonHTMLAttributes {\n shimmerColor?: string\n shimmerSize?: string\n borderRadius?: string\n shimmerDuration?: string\n background?: string\n className?: string\n children?: React.ReactNode\n}\n\nconst ShimmerButton = React.forwardRef(\n (\n {\n shimmerColor = \"#ffffff\",\n shimmerSize = \"0.05em\",\n shimmerDuration = \"3s\",\n borderRadius = \"100px\",\n background = \"rgba(0, 0, 0, 1)\",\n className,\n children,\n ...props\n },\n ref,\n ) => {\n return (\n \n {/* spark container */}\n \n {/* spark */}\n
\n {/* spark before */}\n
\n
\n
\n {children}\n\n {/* Highlight */}\n
\n\n {/* backdrop */}\n
\n \n )\n },\n)\n\nShimmerButton.displayName = \"ShimmerButton\"\n\nexport { ShimmerButton }\n",
+ "type": "registry:ui"
+ }
+ ],
+ "tailwind": {
+ "config": {
+ "theme": {
+ "extend": {
+ "animation": {
+ "shimmer-slide": "shimmer-slide var(--speed) ease-in-out infinite alternate",
+ "spin-around": "spin-around calc(var(--speed) * 2) infinite linear"
+ },
+ "keyframes": {
+ "shimmer-slide": {
+ "to": {
+ "transform": "translate(calc(100cqw - 100%), 0)"
+ }
+ },
+ "spin-around": {
+ "0%": {
+ "transform": "translateZ(0) rotate(0)"
+ },
+ "15%, 35%": {
+ "transform": "translateZ(0) rotate(90deg)"
+ },
+ "65%, 85%": {
+ "transform": "translateZ(0) rotate(270deg)"
+ },
+ "100%": {
+ "transform": "translateZ(0) rotate(360deg)"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apps/web/public/r/showcase-card.json b/apps/web/public/r/showcase-card.json
new file mode 100644
index 0000000..c485c3b
--- /dev/null
+++ b/apps/web/public/r/showcase-card.json
@@ -0,0 +1,16 @@
+{
+ "name": "showcase-card",
+ "type": "registry:ui",
+ "dependencies": [
+ "framer-motion"
+ ],
+ "registryDependencies": [],
+ "description": "A premium showcase card component with 3D tilt effect, parallax image, micro-interactions, and multiple variants. Perfect for portfolios, agency sites, and product showcases.",
+ "files": [
+ {
+ "path": "components/ui/showcase-card.tsx",
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useMotionValue, useSpring, useTransform } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\ninterface ShowcaseCardProps {\n /** Top tagline text */\n tagline?: string\n /** Main heading text */\n heading: string\n /** Description text below heading */\n description?: string\n /** Image URL for the hero section */\n imageUrl: string\n /** Alt text for the image */\n imageAlt?: string\n /** CTA button text */\n ctaText?: string\n /** CTA button click handler */\n onCtaClick?: () => void\n /** Brand name or logo text */\n brandName?: string\n /** Array of service tags */\n services?: string[]\n /** Custom class name */\n className?: string\n /** Enable 3D tilt effect on hover */\n enableTilt?: boolean\n /** Maximum tilt angle in degrees */\n maxTilt?: number\n /** Enable parallax effect on image */\n enableParallax?: boolean\n}\n\nfunction ShowcaseCard({\n tagline,\n heading,\n description,\n imageUrl,\n imageAlt = \"Showcase image\",\n ctaText,\n onCtaClick,\n brandName,\n services = [],\n className,\n enableTilt = true,\n maxTilt = 8,\n enableParallax = true,\n}: ShowcaseCardProps) {\n const cardRef = React.useRef(null)\n const [isHovered, setIsHovered] = React.useState(false)\n\n // Mouse position for tilt effect\n const mouseX = useMotionValue(0)\n const mouseY = useMotionValue(0)\n\n // Smooth spring animation for tilt\n const springConfig = { damping: 25, stiffness: 150 }\n const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [maxTilt, -maxTilt]), springConfig)\n const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-maxTilt, maxTilt]), springConfig)\n\n // Parallax transform for image\n const parallaxX = useSpring(useTransform(mouseX, [-0.5, 0.5], [-15, 15]), springConfig)\n const parallaxY = useSpring(useTransform(mouseY, [-0.5, 0.5], [-15, 15]), springConfig)\n\n // Glow effect position\n const glowX = useSpring(useTransform(mouseX, [-0.5, 0.5], [0, 100]), springConfig)\n const glowY = useSpring(useTransform(mouseY, [-0.5, 0.5], [0, 100]), springConfig)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!cardRef.current || !enableTilt) return\n\n const rect = cardRef.current.getBoundingClientRect()\n const x = (e.clientX - rect.left) / rect.width - 0.5\n const y = (e.clientY - rect.top) / rect.height - 0.5\n\n mouseX.set(x)\n mouseY.set(y)\n },\n [mouseX, mouseY, enableTilt]\n )\n\n const handleMouseEnter = () => {\n setIsHovered(true)\n }\n\n const handleMouseLeave = () => {\n setIsHovered(false)\n mouseX.set(0)\n mouseY.set(0)\n }\n\n return (\n \n {/* Subtle glow overlay on hover */}\n \n\n {/* Image Section */}\n \n {/* Tagline */}\n {tagline && (\n
\n \n {tagline}\n \n \n )}\n\n {/* Hero Image with Parallax */}\n
\n \n \n\n {/* Gradient overlay for text readability */}\n
\n
\n\n {/* Content Section */}\n \n {/* Heading */}\n \n {heading}\n \n\n {/* Description */}\n {description && (\n \n {description}\n \n )}\n\n {/* CTA Button */}\n {ctaText && (\n \n {/* Button hover shine effect */}\n \n {ctaText} \n \n )}\n
\n\n {/* Footer Section */}\n {(brandName || services.length > 0) && (\n \n \n {/* Brand Name */}\n {brandName && (\n
\n {brandName}\n \n )}\n\n {/* Services */}\n {services.length > 0 && (\n
\n {services.map((service, index) => (\n \n \n {service}\n \n {index < services.length - 1 && (\n \n ✦\n \n )}\n \n ))}\n
\n )}\n
\n \n )}\n\n {/* Border glow effect */}\n \n \n )\n}\n\n// Compact variant for grids\ninterface ShowcaseCardCompactProps {\n heading: string\n description?: string\n imageUrl: string\n imageAlt?: string\n className?: string\n onClick?: () => void\n}\n\nfunction ShowcaseCardCompact({\n heading,\n description,\n imageUrl,\n imageAlt = \"Showcase image\",\n className,\n onClick,\n}: ShowcaseCardCompactProps) {\n const [isHovered, setIsHovered] = React.useState(false)\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n whileHover={{ scale: 1.02, y: -4 }}\n whileTap={{ scale: 0.98 }}\n transition={{ duration: 0.3 }}\n >\n {/* Image */}\n \n\n {/* Content */}\n \n
{heading} \n {description && (\n
{description}
\n )}\n
\n\n {/* Hover indicator */}\n \n \n \n \n \n \n \n )\n}\n\n// Horizontal variant for featured sections\ninterface ShowcaseCardHorizontalProps extends Omit {\n imagePosition?: \"left\" | \"right\"\n}\n\nfunction ShowcaseCardHorizontal({\n tagline,\n heading,\n description,\n imageUrl,\n imageAlt = \"Showcase image\",\n ctaText,\n onCtaClick,\n brandName,\n services = [],\n className,\n imagePosition = \"left\",\n enableParallax = true,\n}: ShowcaseCardHorizontalProps) {\n const [isHovered, setIsHovered] = React.useState(false)\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n initial={{ opacity: 0, y: 40 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ duration: 0.6 }}\n whileHover={{ scale: 1.01 }}\n >\n {/* Image Section */}\n \n {tagline && (\n
\n {tagline} \n
\n )}\n\n
\n
\n
\n\n {/* Content Section */}\n \n
\n {heading}\n \n\n {description && (\n
\n {description}\n \n )}\n\n {ctaText && (\n
\n {ctaText}\n \n )}\n\n {(brandName || services.length > 0) && (\n
\n {brandName && (\n
{brandName} \n )}\n {services.length > 0 && (\n
\n {services.map((service, index) => (\n \n {service} \n {index < services.length - 1 && (\n ✦ \n )}\n \n ))}\n
\n )}\n
\n )}\n
\n \n )\n}\n\n// Grid container for showcase cards\ninterface ShowcaseGridProps {\n children: React.ReactNode\n columns?: 1 | 2 | 3 | 4\n gap?: \"sm\" | \"md\" | \"lg\"\n className?: string\n}\n\nfunction ShowcaseGrid({\n children,\n columns = 3,\n gap = \"md\",\n className,\n}: ShowcaseGridProps) {\n const gapClasses = {\n sm: \"gap-4\",\n md: \"gap-6\",\n lg: \"gap-8\",\n }\n\n const columnClasses = {\n 1: \"grid-cols-1\",\n 2: \"grid-cols-1 sm:grid-cols-2\",\n 3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n 4: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\",\n }\n\n return (\n \n {children}\n
\n )\n}\n\nexport {\n ShowcaseCard,\n ShowcaseCardCompact,\n ShowcaseCardHorizontal,\n ShowcaseGrid,\n type ShowcaseCardProps,\n type ShowcaseCardCompactProps,\n type ShowcaseCardHorizontalProps,\n type ShowcaseGridProps,\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/web/public/r/spotlight-card.json b/apps/web/public/r/spotlight-card.json
index f95f0a5..f801937 100644
--- a/apps/web/public/r/spotlight-card.json
+++ b/apps/web/public/r/spotlight-card.json
@@ -11,10 +11,10 @@
{
"path": "components/ui/spotlight-card.tsx",
"type": "registry:ui",
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface SpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n spotlightColor?: string\n borderColor?: string\n borderWidth?: number\n borderRadius?: number\n glowIntensity?: number\n}\n\nfunction SpotlightCard({\n children,\n className,\n spotlightColor = \"rgba(120, 119, 198, 0.3)\",\n borderColor,\n borderWidth = 1,\n borderRadius = 16,\n glowIntensity = 0.15,\n ...props\n}: SpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n const [opacity, setOpacity] = React.useState(0)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n const handleMouseEnter = React.useCallback(() => {\n setIsHovered(true)\n setOpacity(1)\n }, [])\n\n const handleMouseLeave = React.useCallback(() => {\n setIsHovered(false)\n setOpacity(0)\n }, [])\n\n return (\n \n )\n}\n\nfunction SpotlightCardContent({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\nfunction SpotlightCardHeader({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\nfunction SpotlightCardTitle({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children} \n}\n\nfunction SpotlightCardDescription({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\ninterface MultiSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n colors?: string[]\n borderRadius?: number\n}\n\nfunction MultiSpotlightCard({\n children,\n className,\n colors = [\"rgba(120, 119, 198, 0.4)\", \"rgba(255, 77, 77, 0.3)\", \"rgba(77, 255, 174, 0.3)\"],\n borderRadius = 16,\n ...props\n}: MultiSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden bg-neutral-950\", \"border border-neutral-800\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n {colors.map((color, index) => (\n
\n ))}\n
{children}
\n
\n )\n}\n\ninterface BeamSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n beamColor?: string\n beamWidth?: number\n borderRadius?: number\n}\n\nfunction BeamSpotlightCard({\n children,\n className,\n beamColor = \"rgba(120, 119, 198, 0.5)\",\n beamWidth = 200,\n borderRadius = 16,\n ...props\n}: BeamSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden bg-neutral-950\", \"border border-neutral-800\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n
\n
\n
\n
{children}
\n
\n )\n}\n\ninterface GradientFollowCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n gradientColors?: [string, string, string]\n borderRadius?: number\n}\n\nfunction GradientFollowCard({\n children,\n className,\n gradientColors = [\"#7877c6\", \"#5eead4\", \"#f472b6\"],\n borderRadius = 16,\n ...props\n}: GradientFollowCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 50, y: 50 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n const x = ((e.clientX - rect.left) / rect.width) * 100\n const y = ((e.clientY - rect.top) / rect.height) * 100\n setPosition({ x, y })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n
\n
\n
\n
{children}
\n
\n )\n}\n\ninterface TiltSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n maxTilt?: number\n perspective?: number\n scale?: number\n borderRadius?: number\n glareOpacity?: number\n}\n\nfunction TiltSpotlightCard({\n children,\n className,\n maxTilt = 10,\n perspective = 1000,\n scale = 1.02,\n borderRadius = 16,\n glareOpacity = 0.2,\n ...props\n}: TiltSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [transform, setTransform] = React.useState({ rotateX: 0, rotateY: 0, scale: 1 })\n const [glarePosition, setGlarePosition] = React.useState({ x: 50, y: 50 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n const centerX = rect.width / 2\n const centerY = rect.height / 2\n const mouseX = e.clientX - rect.left\n const mouseY = e.clientY - rect.top\n const rotateY = ((mouseX - centerX) / centerX) * maxTilt\n const rotateX = -((mouseY - centerY) / centerY) * maxTilt\n setTransform({ rotateX, rotateY, scale })\n setGlarePosition({ x: (mouseX / rect.width) * 100, y: (mouseY / rect.height) * 100 })\n },\n [maxTilt, scale]\n )\n\n const handleMouseLeave = React.useCallback(() => {\n setTransform({ rotateX: 0, rotateY: 0, scale: 1 })\n setIsHovered(false)\n }, [])\n\n return (\n setIsHovered(true)}\n onMouseLeave={handleMouseLeave}\n className={cn(\"relative overflow-hidden bg-neutral-950\", \"border border-neutral-800\", \"transition-[border-color] duration-500\", isHovered && \"border-neutral-700\", className)}\n style={{\n borderRadius: `${borderRadius}px`,\n perspective: `${perspective}px`,\n transform: `perspective(${perspective}px) rotateX(${transform.rotateX}deg) rotateY(${transform.rotateY}deg) scale(${transform.scale})`,\n transition: isHovered ? \"transform 0.1s ease-out\" : \"transform 0.5s ease-out\",\n }}\n {...props}\n >\n
\n
{children}
\n
\n )\n}\n\nexport {\n SpotlightCard,\n SpotlightCardContent,\n SpotlightCardHeader,\n SpotlightCardTitle,\n SpotlightCardDescription,\n MultiSpotlightCard,\n BeamSpotlightCard,\n GradientFollowCard,\n TiltSpotlightCard,\n}\n"
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface SpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n spotlightColor?: string\n borderColor?: string\n borderWidth?: number\n borderRadius?: number\n glowIntensity?: number\n}\n\nfunction SpotlightCard({\n children,\n className,\n spotlightColor = \"rgba(120, 119, 198, 0.3)\",\n borderColor,\n borderWidth = 1,\n borderRadius = 16,\n glowIntensity = 0.15,\n ...props\n}: SpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n const [opacity, setOpacity] = React.useState(0)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n const handleMouseEnter = React.useCallback(() => {\n setIsHovered(true)\n setOpacity(1)\n }, [])\n\n const handleMouseLeave = React.useCallback(() => {\n setIsHovered(false)\n setOpacity(0)\n }, [])\n\n return (\n \n )\n}\n\nfunction SpotlightCardContent({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\nfunction SpotlightCardHeader({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\nfunction SpotlightCardTitle({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children} \n}\n\nfunction SpotlightCardDescription({ children, className, ...props }: React.HTMLAttributes & { children: React.ReactNode }) {\n return {children}
\n}\n\ninterface MultiSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n colors?: string[]\n borderRadius?: number\n}\n\nfunction MultiSpotlightCard({\n children,\n className,\n colors = [\"rgba(120, 119, 198, 0.4)\", \"rgba(255, 77, 77, 0.3)\", \"rgba(77, 255, 174, 0.3)\"],\n borderRadius = 16,\n ...props\n}: MultiSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden\", \"bg-white dark:bg-neutral-950\", \"border border-neutral-200 dark:border-neutral-800\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n {colors.map((color, index) => (\n
\n ))}\n
{children}
\n
\n )\n}\n\ninterface BeamSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n beamColor?: string\n beamWidth?: number\n borderRadius?: number\n}\n\nfunction BeamSpotlightCard({\n children,\n className,\n beamColor = \"rgba(120, 119, 198, 0.5)\",\n beamWidth = 200,\n borderRadius = 16,\n ...props\n}: BeamSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 0, y: 0 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden\", \"bg-white dark:bg-neutral-950\", \"border border-neutral-200 dark:border-neutral-800\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n
\n
\n
\n
{children}
\n
\n )\n}\n\ninterface GradientFollowCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n gradientColors?: [string, string, string]\n borderRadius?: number\n}\n\nfunction GradientFollowCard({\n children,\n className,\n gradientColors = [\"#7877c6\", \"#5eead4\", \"#f472b6\"],\n borderRadius = 16,\n ...props\n}: GradientFollowCardProps) {\n const containerRef = React.useRef(null)\n const [position, setPosition] = React.useState({ x: 50, y: 50 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n const x = ((e.clientX - rect.left) / rect.width) * 100\n const y = ((e.clientY - rect.top) / rect.height) * 100\n setPosition({ x, y })\n },\n []\n )\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className={cn(\"relative overflow-hidden\", \"transition-all duration-500\", className)}\n style={{ borderRadius: `${borderRadius}px` }}\n {...props}\n >\n
\n
\n
\n
{children}
\n
\n )\n}\n\ninterface TiltSpotlightCardProps extends React.HTMLAttributes {\n children: React.ReactNode\n maxTilt?: number\n perspective?: number\n scale?: number\n borderRadius?: number\n glareOpacity?: number\n}\n\nfunction TiltSpotlightCard({\n children,\n className,\n maxTilt = 10,\n perspective = 1000,\n scale = 1.02,\n borderRadius = 16,\n glareOpacity = 0.2,\n ...props\n}: TiltSpotlightCardProps) {\n const containerRef = React.useRef(null)\n const [transform, setTransform] = React.useState({ rotateX: 0, rotateY: 0, scale: 1 })\n const [glarePosition, setGlarePosition] = React.useState({ x: 50, y: 50 })\n const [isHovered, setIsHovered] = React.useState(false)\n\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n if (!containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n const centerX = rect.width / 2\n const centerY = rect.height / 2\n const mouseX = e.clientX - rect.left\n const mouseY = e.clientY - rect.top\n const rotateY = ((mouseX - centerX) / centerX) * maxTilt\n const rotateX = -((mouseY - centerY) / centerY) * maxTilt\n setTransform({ rotateX, rotateY, scale })\n setGlarePosition({ x: (mouseX / rect.width) * 100, y: (mouseY / rect.height) * 100 })\n },\n [maxTilt, scale]\n )\n\n const handleMouseLeave = React.useCallback(() => {\n setTransform({ rotateX: 0, rotateY: 0, scale: 1 })\n setIsHovered(false)\n }, [])\n\n return (\n setIsHovered(true)}\n onMouseLeave={handleMouseLeave}\n className={cn(\"relative overflow-hidden\", \"bg-white dark:bg-neutral-950\", \"border border-neutral-200 dark:border-neutral-800\", \"transition-[border-color] duration-500\", isHovered && \"border-neutral-300 dark:border-neutral-700\", className)}\n style={{\n borderRadius: `${borderRadius}px`,\n perspective: `${perspective}px`,\n transform: `perspective(${perspective}px) rotateX(${transform.rotateX}deg) rotateY(${transform.rotateY}deg) scale(${transform.scale})`,\n transition: isHovered ? \"transform 0.1s ease-out\" : \"transform 0.5s ease-out\",\n }}\n {...props}\n >\n
\n
{children}
\n
\n )\n}\n\nexport {\n SpotlightCard,\n SpotlightCardContent,\n SpotlightCardHeader,\n SpotlightCardTitle,\n SpotlightCardDescription,\n MultiSpotlightCard,\n BeamSpotlightCard,\n GradientFollowCard,\n TiltSpotlightCard,\n}\n"
}
],
"tailwind": {},
"cssVars": {},
"meta": {}
-}
+}
\ No newline at end of file
diff --git a/docs/CREATING_COMPONENTS.md b/docs/CREATING_COMPONENTS.md
new file mode 100644
index 0000000..8318ac8
--- /dev/null
+++ b/docs/CREATING_COMPONENTS.md
@@ -0,0 +1,396 @@
+# Creating New Components for Componentry
+
+This guide documents the complete process for adding new components to the Componentry UI library.
+
+## 📁 Project Structure Overview
+
+```
+componentry/
+├── packages/ui/ # Core UI package
+│ ├── components.json # shadcn/ui configuration
+│ └── src/
+│ └── components/ # Component source files (.tsx)
+│ ├── circuit-board.tsx
+│ ├── showcase-card.tsx
+│ └── ...
+│
+├── apps/web/ # Documentation website
+│ ├── public/r/ # Registry JSON files (for installation)
+│ │ ├── circuit-board.json
+│ │ ├── showcase-card.json
+│ │ └── ...
+│ │
+│ ├── config/
+│ │ └── docs.ts # Sidebar navigation config
+│ │
+│ ├── components/ # Shared docs components
+│ │ ├── component-layout.tsx # Layout wrapper for component pages
+│ │ ├── install-command.tsx # Package manager command generator
+│ │ └── code-block.tsx # Syntax-highlighted code display
+│ │
+│ └── app/docs/components/ # Component documentation pages
+│ ├── circuit-board/page.tsx
+│ ├── showcase-card/page.tsx
+│ └── ...
+│
+└── docs/ # Internal documentation (this file)
+```
+
+---
+
+## 🚀 Step-by-Step: Creating a New Component
+
+### Step 1: Create the Component Source File
+
+**Location:** `packages/ui/src/components/{component-name}.tsx`
+
+**Key Requirements:**
+
+1. **"use client" directive** - Add at the top for client-side components
+2. **Import utilities** from `@workspace/ui/lib/utils` for `cn()` helper
+3. **TypeScript interfaces** - Define props with JSDoc comments
+4. **Export all variants** - Named exports at the bottom
+
+**Example Structure:**
+
+```tsx
+"use client"
+
+import * as React from "react"
+import { motion } from "framer-motion" // If using animations
+import { cn } from "@workspace/ui/lib/utils"
+
+// Define TypeScript interfaces with JSDoc comments
+interface MyComponentProps {
+ /** Description of this prop */
+ title: string
+ /** Optional prop with default */
+ variant?: "default" | "compact"
+ className?: string
+ children?: React.ReactNode
+}
+
+// Main component function
+function MyComponent({
+ title,
+ variant = "default",
+ className,
+ children,
+}: MyComponentProps) {
+ return (
+
+ {/* Component implementation */}
+
+ )
+}
+
+// Export all components and types
+export {
+ MyComponent,
+ type MyComponentProps,
+}
+```
+
+**Reference Files:**
+- `packages/ui/src/components/showcase-card.tsx` - Complex component with variants
+- `packages/ui/src/components/circuit-board.tsx` - SVG-based interactive component
+- `packages/ui/src/components/spotlight-card.tsx` - Multiple sub-components
+
+---
+
+### Step 2: Create the Registry JSON File
+
+**Location:** `apps/web/public/r/{component-name}.json`
+
+This file enables users to install your component via the shadcn CLI.
+
+**Structure:**
+
+```json
+{
+ "name": "my-component",
+ "type": "registry:ui",
+ "dependencies": ["framer-motion"],
+ "registryDependencies": [],
+ "description": "Brief description of the component.",
+ "files": [
+ {
+ "path": "components/ui/my-component.tsx",
+ "content": "// Full component source code here...",
+ "type": "registry:ui"
+ }
+ ]
+}
+```
+
+**Important Notes:**
+
+1. **dependencies** - List npm packages required (e.g., `framer-motion`, `lucide-react`)
+2. **registryDependencies** - List other components from this registry if dependent
+3. **content** - The full component source, with `@workspace/ui/lib/utils` replaced with `@/lib/utils`
+
+**Generating the JSON file programmatically:**
+
+```bash
+node -e "
+const fs = require('fs');
+const content = fs.readFileSync('packages/ui/src/components/my-component.tsx', 'utf8');
+const registry = {
+ name: 'my-component',
+ type: 'registry:ui',
+ dependencies: ['framer-motion'],
+ registryDependencies: [],
+ description: 'Description here.',
+ files: [{
+ path: 'components/ui/my-component.tsx',
+ content: content.replace(/@workspace\/ui\/lib\/utils/g, '@/lib/utils'),
+ type: 'registry:ui'
+ }]
+};
+fs.writeFileSync('apps/web/public/r/my-component.json', JSON.stringify(registry, null, 2));
+"
+```
+
+---
+
+### Step 3: Create the Documentation Page
+
+**Location:** `apps/web/app/docs/components/{component-name}/page.tsx`
+
+**Required Structure:**
+
+```tsx
+import type React from "react"
+import type { Metadata } from "next"
+import { MyComponent } from "@workspace/ui/components/my-component"
+import { InstallCommand } from "@/components/install-command"
+import { CodeBlock } from "@/components/code-block"
+import { ComponentLayout, Section } from "@/components/component-layout"
+
+// SEO Metadata
+export const metadata: Metadata = {
+ title: "My Component",
+ description: "Description for SEO. Free React component by Harsh Jadhav.",
+ alternates: {
+ canonical: "https://componentry.fun/docs/components/my-component",
+ },
+}
+
+// Code examples as string constants
+const basicCode = `import { MyComponent } from "@/components/ui/my-component"
+
+ `
+
+export default function MyComponentPage(): React.JSX.Element {
+ return (
+
+ {/* Install Section */}
+
+
+ {/* Examples Section */}
+
+
+ {/* Props Section */}
+
+
+
+
title
+
+ The main title text (required)
+
+
+ {/* More props... */}
+
+
+
+ )
+}
+```
+
+**Reference Files:**
+- `apps/web/app/docs/components/showcase-card/page.tsx`
+- `apps/web/app/docs/components/circuit-board/page.tsx`
+- `apps/web/app/docs/components/spotlight-card/page.tsx`
+
+---
+
+### Step 4: Add to Sidebar Navigation
+
+**Location:** `apps/web/config/docs.ts`
+
+Add your component to the appropriate section:
+
+```typescript
+export const docsConfig = {
+ nav: [
+ {
+ title: "Components",
+ items: [
+ // Existing components...
+ {
+ title: "My Component",
+ href: "/docs/components/my-component",
+ },
+ ],
+ },
+ ],
+}
+```
+
+---
+
+## 📝 Documentation Page Sections
+
+### Required Sections
+
+| Section | Purpose |
+|---------|---------|
+| **Install** | `` with component name |
+| **Examples** | Live demos with code snippets |
+| **Props** | Table of all component props |
+
+### Optional Sections
+
+| Section | Purpose |
+|---------|---------|
+| **Features** | Key capabilities list |
+| **Variants** | Different component versions |
+| **Usage Notes** | Best practices, accessibility |
+
+---
+
+## ⚠️ Important Considerations
+
+### Server vs Client Components
+
+Next.js App Router uses Server Components by default. Be aware:
+
+- ❌ **Cannot pass functions** as props from Server to Client components
+- ✅ Remove `onClick` handlers from documentation examples, or
+- ✅ Create a wrapper Client Component for interactive demos
+
+### Code Example Strings
+
+When creating code example strings:
+
+```tsx
+// ✅ Good - escaped properly for template literals
+const code = ` `
+
+// ❌ Bad - unescaped characters can break
+const code = ` `
+```
+
+### Image URLs
+
+For demo images, use reliable sources:
+- Unsplash: `https://images.unsplash.com/photo-{id}?w=800&q=80`
+- Placeholder: `https://picsum.photos/800/600`
+
+---
+
+## 🎨 Component Design Guidelines
+
+### Micro-interactions
+
+Premium components should include:
+- Hover effects (scale, shadow, glow)
+- Smooth transitions (use `transition-all duration-300`)
+- Spring physics for natural feel (Framer Motion: `{ damping: 25, stiffness: 150 }`)
+
+### Theme Awareness
+
+Support both light and dark modes:
+
+```tsx
+const [isDark, setIsDark] = React.useState(true)
+
+React.useEffect(() => {
+ const checkTheme = () => {
+ const isDarkMode = document.documentElement.classList.contains("dark")
+ setIsDark(isDarkMode)
+ }
+ checkTheme()
+
+ const observer = new MutationObserver(checkTheme)
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["class"]
+ })
+
+ return () => observer.disconnect()
+}, [])
+```
+
+### Responsive Design
+
+Always include responsive breakpoints:
+
+```tsx
+className="text-sm sm:text-base lg:text-lg"
+className="p-4 sm:p-6 lg:p-8"
+className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
+```
+
+---
+
+## ✅ Pre-Launch Checklist
+
+Before considering a component complete:
+
+- [ ] Component source file created in `packages/ui/src/components/`
+- [ ] Registry JSON file created in `apps/web/public/r/`
+- [ ] Documentation page created in `apps/web/app/docs/components/`
+- [ ] Added to sidebar navigation in `apps/web/config/docs.ts`
+- [ ] Tested with `pnpm run dev`
+- [ ] Verified installation command works
+- [ ] All examples render without errors
+- [ ] Responsive design tested (mobile, tablet, desktop)
+- [ ] Light/dark mode tested
+- [ ] Props table complete with descriptions
+
+---
+
+## 🔗 Quick Reference
+
+| Task | File Location |
+|------|---------------|
+| Component source | `packages/ui/src/components/{name}.tsx` |
+| Registry JSON | `apps/web/public/r/{name}.json` |
+| Docs page | `apps/web/app/docs/components/{name}/page.tsx` |
+| Sidebar nav | `apps/web/config/docs.ts` |
+| Layout wrapper | `apps/web/components/component-layout.tsx` |
+| Install command | `apps/web/components/install-command.tsx` |
+| Code block | `apps/web/components/code-block.tsx` |
+| Utils (cn) | `packages/ui/src/lib/utils.ts` |
+
+---
+
+## 📚 Example Components to Study
+
+| Component | Complexity | Key Features |
+|-----------|------------|--------------|
+| `border-beam` | Simple | CSS animations, minimal props |
+| `showcase-card` | Medium | Multiple variants, 3D effects |
+| `circuit-board` | Complex | SVG rendering, connection logic |
+| `spotlight-card` | Complex | Multiple exports, cursor tracking |
+| `flight-status-card` | Advanced | Many sub-states, real data |
+
+---
+
+*Last updated: January 2026*
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..7ba2ec3
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,52 @@
+# Componentry Internal Documentation
+
+This directory contains internal documentation for maintaining and extending the Componentry UI library.
+
+## 📄 Available Guides
+
+| Document | Description |
+|----------|-------------|
+| [CREATING_COMPONENTS.md](./CREATING_COMPONENTS.md) | Complete guide for adding new components |
+
+## 🏗️ Architecture Overview
+
+```
+componentry/
+├── packages/ui/ # Core component library (publishable)
+├── apps/web/ # Documentation website (Next.js)
+└── docs/ # Internal documentation (this folder)
+```
+
+### Key Concepts
+
+1. **Monorepo Structure** - Uses pnpm workspaces with Turborepo
+2. **Component Registry** - shadcn/ui compatible JSON files for easy installation
+3. **Documentation Site** - Next.js App Router with MDX-free component pages
+
+## 🚀 Quick Start for Contributors
+
+```bash
+# Install dependencies
+pnpm install
+
+# Start development server
+pnpm run dev
+
+# Build for production
+pnpm run build
+```
+
+## 📦 Adding a New Component (Quick Steps)
+
+1. **Create component:** `packages/ui/src/components/{name}.tsx`
+2. **Create registry JSON:** `apps/web/public/r/{name}.json`
+3. **Create docs page:** `apps/web/app/docs/components/{name}/page.tsx`
+4. **Add to sidebar:** `apps/web/config/docs.ts`
+
+See [CREATING_COMPONENTS.md](./CREATING_COMPONENTS.md) for detailed instructions.
+
+## 🔗 Useful Links
+
+- **Live Site:** https://componentry.fun
+- **Registry URL:** https://componentry.fun/r/{component}.json
+- **Install Example:** `pnpm dlx shadcn@latest add "https://componentry.fun/r/showcase-card.json"`
diff --git a/package.json b/package.json
index 9edf3a9..08d0ff1 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,18 @@
{
- "name": "component-playground",
+ "name": "componentry",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
- "format": "prettier --write \"**/*.{ts,tsx,md}\""
+ "format": "prettier --write \"**/*.{ts,tsx,md}\"",
+ "screenshot": "node scripts/screenshot.js"
},
"devDependencies": {
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
+ "playwright": "^1.57.0",
"prettier": "^3.7.4",
"turbo": "^2.6.3",
"typescript": "5.7.3"
diff --git a/packages/ui/package.json b/packages/ui/package.json
index d96128b..706001e 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -10,6 +10,8 @@
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "framer-motion": "^12.23.26",
"lucide-react": "^0.475.0",
"next-themes": "^0.4.6",
"react": "^19.1.1",
diff --git a/packages/ui/src/components/border-beam.tsx b/packages/ui/src/components/border-beam.tsx
new file mode 100644
index 0000000..723f821
--- /dev/null
+++ b/packages/ui/src/components/border-beam.tsx
@@ -0,0 +1,60 @@
+"use client"
+
+import { motion } from "framer-motion"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface BorderBeamProps {
+ className?: string
+ size?: number
+ duration?: number
+ borderWidth?: number
+ anchor?: number
+ colorFrom?: string
+ colorTo?: string
+ delay?: number
+}
+
+export function BorderBeam({
+ className,
+ size = 200,
+ duration = 15,
+ anchor = 90,
+ borderWidth = 1.5,
+ colorFrom = "#ffaa40",
+ colorTo = "#9c40ff",
+ delay = 0,
+}: BorderBeamProps) {
+ return (
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/circuit-board.tsx b/packages/ui/src/components/circuit-board.tsx
new file mode 100644
index 0000000..4b46eb1
--- /dev/null
+++ b/packages/ui/src/components/circuit-board.tsx
@@ -0,0 +1,678 @@
+"use client"
+
+import * as React from "react"
+import { motion } from "framer-motion"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface CircuitNode {
+ id: string
+ x: number
+ y: number
+ label?: string
+ icon?: React.ReactNode
+ status?: "active" | "inactive" | "processing" | "error"
+ size?: "sm" | "md" | "lg"
+}
+
+interface CircuitConnection {
+ from: string
+ to: string
+ animated?: boolean
+ bidirectional?: boolean
+ color?: string
+ pulseColor?: string
+}
+
+interface CircuitBoardProps extends React.HTMLAttributes {
+ nodes: CircuitNode[]
+ connections: CircuitConnection[]
+ width?: number
+ height?: number
+ gridSize?: number
+ showGrid?: boolean
+ gridColor?: string
+ traceColor?: string
+ pulseColor?: string
+ nodeColor?: string
+ pulseSpeed?: number
+ traceWidth?: number
+ /** Force a specific theme variant. Defaults to auto-detect from system. */
+ variant?: "light" | "dark" | "auto"
+}
+
+function CircuitBoard({
+ nodes,
+ connections,
+ width = 600,
+ height = 400,
+ gridSize = 20,
+ showGrid = true,
+ gridColor,
+ traceColor,
+ pulseColor,
+ nodeColor,
+ pulseSpeed = 2,
+ traceWidth = 2,
+ variant = "auto",
+ className,
+ ...props
+}: CircuitBoardProps) {
+ // Theme-aware color defaults
+ const [isDark, setIsDark] = React.useState(true)
+
+ React.useEffect(() => {
+ if (variant !== "auto") {
+ setIsDark(variant === "dark")
+ return
+ }
+
+ // Check for dark class on html/body
+ const checkTheme = () => {
+ const isDarkMode = document.documentElement.classList.contains("dark") ||
+ document.body.classList.contains("dark")
+ setIsDark(isDarkMode)
+ }
+
+ checkTheme()
+
+ // Listen for changes
+ const observer = new MutationObserver(checkTheme)
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
+ observer.observe(document.body, { attributes: true, attributeFilter: ["class"] })
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+ mediaQuery.addEventListener("change", checkTheme)
+
+ return () => {
+ observer.disconnect()
+ mediaQuery.removeEventListener("change", checkTheme)
+ }
+ }, [variant])
+
+ // Compute theme-aware colors
+ const computedGridColor = gridColor || (isDark ? "rgba(163, 163, 163, 0.08)" : "rgba(64, 64, 64, 0.12)")
+ const computedTraceColor = traceColor || (isDark ? "rgba(163, 163, 163, 0.25)" : "rgba(64, 64, 64, 0.35)")
+ const computedPulseColor = pulseColor || (isDark ? "rgba(163, 163, 163, 0.6)" : "rgba(64, 64, 64, 0.7)")
+ const computedNodeColor = nodeColor || (isDark ? "rgba(163, 163, 163, 0.5)" : "rgba(64, 64, 64, 0.6)")
+ const nodeMap = React.useMemo(() => {
+ return new Map(nodes.map((node) => [node.id, node]))
+ }, [nodes])
+
+ const getNodeSize = React.useCallback((size?: CircuitNode["size"]) => {
+ switch (size) {
+ case "sm":
+ return 24
+ case "lg":
+ return 48
+ default:
+ return 36
+ }
+ }, [])
+
+ const calculatePath = React.useCallback(
+ (from: CircuitNode, to: CircuitNode): string => {
+ const fromSize = getNodeSize(from.size) / 2 + 4
+ const toSize = getNodeSize(to.size) / 2 + 4
+
+ const dx = to.x - from.x
+ const dy = to.y - from.y
+
+ // Calculate start and end points offset from node centers
+ let startX = from.x
+ let startY = from.y
+ let endX = to.x
+ let endY = to.y
+
+ // Create circuit-like paths with right angles
+ if (Math.abs(dx) > Math.abs(dy)) {
+ // Horizontal first, then vertical
+ startX = from.x + (dx > 0 ? fromSize : -fromSize)
+ endX = to.x + (dx > 0 ? -toSize : toSize)
+ const midX = from.x + dx / 2
+ return `M ${startX} ${startY} H ${midX} V ${endY} H ${endX}`
+ } else {
+ // Vertical first, then horizontal
+ startY = from.y + (dy > 0 ? fromSize : -fromSize)
+ endY = to.y + (dy > 0 ? -toSize : toSize)
+ const midY = from.y + dy / 2
+ return `M ${startX} ${startY} V ${midY} H ${endX} V ${endY}`
+ }
+ },
+ [getNodeSize]
+ )
+
+ const getStatusColor = (status?: CircuitNode["status"]) => {
+ if (isDark) {
+ switch (status) {
+ case "active":
+ return "rgba(163, 163, 163, 0.7)"
+ case "processing":
+ return "rgba(163, 163, 163, 0.5)"
+ case "error":
+ return "rgba(120, 113, 108, 0.6)"
+ default:
+ return computedNodeColor
+ }
+ } else {
+ switch (status) {
+ case "active":
+ return "rgba(64, 64, 64, 0.8)"
+ case "processing":
+ return "rgba(64, 64, 64, 0.6)"
+ case "error":
+ return "rgba(180, 83, 83, 0.7)"
+ default:
+ return computedNodeColor
+ }
+ }
+ }
+
+ return (
+
+
+
+ {/* Glow filter for the pulse effect */}
+
+
+
+
+
+
+
+
+ {/* Grid pattern */}
+ {showGrid && (
+
+
+
+ )}
+
+ {/* Animated gradient for electricity effect */}
+ {connections.map((conn, i) => (
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Grid background */}
+ {showGrid && (
+
+ )}
+
+ {/* Connection traces */}
+ {connections.map((conn, i) => {
+ const fromNode = nodeMap.get(conn.from)
+ const toNode = nodeMap.get(conn.to)
+ if (!fromNode || !toNode) return null
+
+ const path = calculatePath(fromNode, toNode)
+ const pathLength = 500 // Approximate path length for animation
+
+ return (
+
+ {/* Base trace */}
+
+
+ {/* Animated electricity pulse */}
+ {conn.animated !== false && (
+
+ )}
+
+ {/* Bidirectional pulse */}
+ {conn.bidirectional && (
+
+ )}
+
+
+
+ )
+ })}
+
+
+ {/* Nodes */}
+ {nodes.map((node, i) => {
+ const size = getNodeSize(node.size)
+ const statusColor = getStatusColor(node.status)
+
+ return (
+
+ {/* Node background with pulse */}
+
+
+ {/* Node border */}
+
+
+ {/* Inner glow for active nodes */}
+ {node.status === "active" && (
+
+ )}
+
+ {/* Node content */}
+
+ {node.icon && (
+
{node.icon}
+ )}
+
+
+ {/* Label */}
+ {node.label && (
+
+ {node.label}
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
+// Pre-built circuit patterns
+interface CircuitPatternProps extends Omit {
+ pattern: "data-flow" | "network" | "processor" | "tree"
+}
+
+function CircuitPattern({ pattern, ...props }: CircuitPatternProps) {
+ const patterns = {
+ "data-flow": {
+ nodes: [
+ { id: "input", x: 50, y: 200, label: "Input", status: "active" as const },
+ { id: "process1", x: 200, y: 100, label: "Process", status: "processing" as const },
+ { id: "process2", x: 200, y: 300, label: "Validate", status: "active" as const },
+ { id: "merge", x: 400, y: 200, label: "Merge", status: "active" as const },
+ { id: "output", x: 550, y: 200, label: "Output", status: "active" as const },
+ ],
+ connections: [
+ { from: "input", to: "process1", animated: true },
+ { from: "input", to: "process2", animated: true },
+ { from: "process1", to: "merge", animated: true },
+ { from: "process2", to: "merge", animated: true },
+ { from: "merge", to: "output", animated: true },
+ ],
+ },
+ network: {
+ nodes: [
+ { id: "server", x: 300, y: 80, label: "Server", status: "active" as const, size: "lg" as const },
+ { id: "client1", x: 100, y: 200, label: "Client 1", status: "active" as const },
+ { id: "client2", x: 300, y: 250, label: "Client 2", status: "processing" as const },
+ { id: "client3", x: 500, y: 200, label: "Client 3", status: "active" as const },
+ { id: "db", x: 300, y: 350, label: "Database", status: "active" as const },
+ ],
+ connections: [
+ { from: "server", to: "client1", bidirectional: true },
+ { from: "server", to: "client2", bidirectional: true },
+ { from: "server", to: "client3", bidirectional: true },
+ { from: "server", to: "db", bidirectional: true },
+ ],
+ },
+ processor: {
+ nodes: [
+ { id: "alu", x: 300, y: 200, label: "ALU", status: "processing" as const, size: "lg" as const },
+ { id: "reg1", x: 150, y: 100, label: "R1", status: "active" as const, size: "sm" as const },
+ { id: "reg2", x: 150, y: 200, label: "R2", status: "active" as const, size: "sm" as const },
+ { id: "reg3", x: 150, y: 300, label: "R3", status: "active" as const, size: "sm" as const },
+ { id: "cache", x: 450, y: 200, label: "Cache", status: "active" as const },
+ { id: "out", x: 550, y: 200, label: "Out", status: "active" as const, size: "sm" as const },
+ ],
+ connections: [
+ { from: "reg1", to: "alu", animated: true },
+ { from: "reg2", to: "alu", animated: true },
+ { from: "reg3", to: "alu", animated: true },
+ { from: "alu", to: "cache", animated: true },
+ { from: "cache", to: "out", animated: true },
+ ],
+ },
+ tree: {
+ nodes: [
+ { id: "root", x: 300, y: 50, label: "Root", status: "active" as const },
+ { id: "l1", x: 150, y: 150, label: "L1", status: "active" as const },
+ { id: "r1", x: 450, y: 150, label: "R1", status: "processing" as const },
+ { id: "l1l", x: 80, y: 280, label: "L1L", status: "active" as const, size: "sm" as const },
+ { id: "l1r", x: 220, y: 280, label: "L1R", status: "active" as const, size: "sm" as const },
+ { id: "r1l", x: 380, y: 280, label: "R1L", status: "error" as const, size: "sm" as const },
+ { id: "r1r", x: 520, y: 280, label: "R1R", status: "active" as const, size: "sm" as const },
+ ],
+ connections: [
+ { from: "root", to: "l1", animated: true },
+ { from: "root", to: "r1", animated: true },
+ { from: "l1", to: "l1l", animated: true },
+ { from: "l1", to: "l1r", animated: true },
+ { from: "r1", to: "r1l", animated: true },
+ { from: "r1", to: "r1r", animated: true },
+ ],
+ },
+ }
+
+ const selectedPattern = patterns[pattern]
+ return
+}
+
+// Interactive circuit node for building custom circuits
+interface CircuitNodeComponentProps {
+ status?: "active" | "inactive" | "processing" | "error"
+ size?: "sm" | "md" | "lg"
+ glowColor?: string
+ children?: React.ReactNode
+ className?: string
+ onClick?: () => void
+}
+
+function CircuitNode({
+ status = "inactive",
+ size = "md",
+ glowColor,
+ children,
+ className,
+ onClick,
+}: CircuitNodeComponentProps) {
+ const [isDark, setIsDark] = React.useState(true)
+
+ React.useEffect(() => {
+ const checkTheme = () => {
+ const isDarkMode = document.documentElement.classList.contains("dark") ||
+ document.body.classList.contains("dark")
+ setIsDark(isDarkMode)
+ }
+
+ checkTheme()
+
+ const observer = new MutationObserver(checkTheme)
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
+ observer.observe(document.body, { attributes: true, attributeFilter: ["class"] })
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+ mediaQuery.addEventListener("change", checkTheme)
+
+ return () => {
+ observer.disconnect()
+ mediaQuery.removeEventListener("change", checkTheme)
+ }
+ }, [])
+
+ const sizeClasses = {
+ sm: "w-8 h-8",
+ md: "w-12 h-12",
+ lg: "w-16 h-16",
+ }
+
+ const statusColors = isDark
+ ? {
+ active: "rgba(163, 163, 163, 0.7)",
+ inactive: "rgba(115, 115, 115, 0.4)",
+ processing: "rgba(163, 163, 163, 0.5)",
+ error: "rgba(120, 113, 108, 0.6)",
+ }
+ : {
+ active: "rgba(64, 64, 64, 0.8)",
+ inactive: "rgba(100, 100, 100, 0.5)",
+ processing: "rgba(64, 64, 64, 0.6)",
+ error: "rgba(180, 83, 83, 0.7)",
+ }
+
+ const color = glowColor || statusColors[status]
+
+ return (
+
+ {/* Pulse animation for processing state */}
+ {status === "processing" && (
+
+ )}
+
+ {/* Active glow */}
+ {status === "active" && (
+
+ )}
+
+ {/* Error pulse */}
+ {status === "error" && (
+
+ )}
+
+
+ {children}
+
+
+ )
+}
+
+// Animated trace line component for custom layouts
+interface CircuitTraceProps {
+ path: string
+ animated?: boolean
+ color?: string
+ pulseColor?: string
+ width?: number
+ pulseSpeed?: number
+}
+
+function CircuitTrace({
+ path,
+ animated = true,
+ color,
+ pulseColor,
+ width = 2,
+ pulseSpeed = 2,
+}: CircuitTraceProps) {
+ const [isDark, setIsDark] = React.useState(true)
+
+ React.useEffect(() => {
+ const checkTheme = () => {
+ const isDarkMode = document.documentElement.classList.contains("dark") ||
+ document.body.classList.contains("dark")
+ setIsDark(isDarkMode)
+ }
+
+ checkTheme()
+
+ const observer = new MutationObserver(checkTheme)
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
+ observer.observe(document.body, { attributes: true, attributeFilter: ["class"] })
+
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
+ mediaQuery.addEventListener("change", checkTheme)
+
+ return () => {
+ observer.disconnect()
+ mediaQuery.removeEventListener("change", checkTheme)
+ }
+ }, [])
+
+ const computedColor = color || (isDark ? "rgba(163, 163, 163, 0.25)" : "rgba(64, 64, 64, 0.35)")
+ const computedPulseColor = pulseColor || (isDark ? "rgba(163, 163, 163, 0.6)" : "rgba(64, 64, 64, 0.7)")
+ const pathLength = 500
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {/* Base trace */}
+
+
+ {/* Animated pulse */}
+ {animated && (
+
+ )}
+
+ )
+}
+
+export {
+ CircuitBoard,
+ CircuitPattern,
+ CircuitNode,
+ CircuitTrace,
+ type CircuitNode as CircuitNodeType,
+ type CircuitConnection,
+ type CircuitBoardProps,
+}
diff --git a/packages/ui/src/components/command-menu.tsx b/packages/ui/src/components/command-menu.tsx
new file mode 100644
index 0000000..1718492
--- /dev/null
+++ b/packages/ui/src/components/command-menu.tsx
@@ -0,0 +1,223 @@
+"use client"
+
+import * as React from "react"
+import * as ReactDOM from "react-dom"
+import { Command } from "cmdk"
+import { Search, ArrowRight, X } from "lucide-react"
+import { motion, AnimatePresence } from "framer-motion"
+
+import { cn } from "@workspace/ui/lib/utils"
+
+export interface CommandMenuItem {
+ id: string
+ title: string
+ group?: string
+ icon?: React.ReactNode
+ onSelect?: () => void
+}
+
+export interface CommandMenuGroup {
+ title: string
+ items: CommandMenuItem[]
+}
+
+export interface CommandMenuProps {
+ groups: CommandMenuGroup[]
+ placeholder?: string
+ emptyMessage?: string
+ brandName?: string
+ triggerClassName?: string
+ triggerLabel?: string
+ shortcutKey?: string
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}
+
+function CommandMenu({
+ groups,
+ placeholder = "Search...",
+ emptyMessage = "No results found",
+ brandName = "Command Menu",
+ triggerClassName,
+ triggerLabel = "Search...",
+ shortcutKey = "K",
+ open: controlledOpen,
+ onOpenChange,
+}: CommandMenuProps) {
+ const [internalOpen, setInternalOpen] = React.useState(false)
+ const [query, setQuery] = React.useState("")
+ const inputRef = React.useRef(null)
+
+ const isControlled = controlledOpen !== undefined
+ const open = isControlled ? controlledOpen : internalOpen
+ const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen
+
+ React.useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key.toLowerCase() === shortcutKey.toLowerCase() && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault()
+ setOpen(!open)
+ }
+ if (e.key === "Escape") {
+ setOpen(false)
+ }
+ }
+
+ document.addEventListener("keydown", down)
+ return () => document.removeEventListener("keydown", down)
+ }, [open, setOpen, shortcutKey])
+
+ React.useEffect(() => {
+ if (open) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 0)
+ } else {
+ setQuery("")
+ }
+ }, [open])
+
+ const handleSelect = React.useCallback((item: CommandMenuItem) => {
+ setOpen(false)
+ item.onSelect?.()
+ }, [setOpen])
+
+ return (
+ <>
+ setOpen(true)}
+ className={cn(
+ "group inline-flex items-center gap-2 whitespace-nowrap transition-all duration-200",
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
+ "disabled:pointer-events-none disabled:opacity-50",
+ "border border-input/50 hover:border-input hover:bg-accent/50",
+ "px-3 py-2 relative h-9 w-full justify-start rounded-lg bg-muted/30",
+ "text-sm font-normal text-muted-foreground sm:pr-12 md:w-40 lg:w-56",
+ triggerClassName
+ )}
+ >
+
+ {triggerLabel}
+ Search
+
+ ⌘ {shortcutKey}
+
+
+
+ {typeof document !== "undefined" && ReactDOM.createPortal(
+
+ {open && (
+ <>
+ setOpen(false)}
+ />
+
+
+
+
+
+
+
+ {query && (
+
setQuery("")}
+ className="rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
+ >
+ Clear
+
+ )}
+
+ ESC
+
+
+
+
+
+
+
+
+ {emptyMessage}
+ Try searching for something else
+
+
+ {groups.map((group) => (
+
+ {group.items.map((item) => (
+ handleSelect(item)}
+ className="group/item relative flex cursor-pointer select-none items-center gap-3 rounded-xl px-3 py-2.5 text-sm outline-none transition-colors hover:bg-accent/70 hover:text-accent-foreground aria-[selected='true']:bg-accent aria-[selected='true']:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50"
+ >
+
+ {item.icon}
+
+
+ {item.title}
+ {group.title}
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+ ↑↓
+ Navigate
+
+
+ ↵
+ Select
+
+
+
{brandName}
+
+
+
+ >
+ )}
+ ,
+ document.body
+ )}
+ >
+ )
+}
+
+CommandMenu.displayName = "CommandMenu"
+
+export { CommandMenu }
diff --git a/packages/ui/src/components/dither-gradient.tsx b/packages/ui/src/components/dither-gradient.tsx
new file mode 100644
index 0000000..bb8c571
--- /dev/null
+++ b/packages/ui/src/components/dither-gradient.tsx
@@ -0,0 +1,129 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface DitherGradientProps {
+ className?: string
+ colorFrom?: string
+ colorTo?: string
+ colorMid?: string
+ intensity?: number
+ speed?: number
+ angle?: number
+}
+
+export function DitherGradient({
+ className,
+ colorFrom = "#4f46e5",
+ colorTo = "#ec4899",
+ colorMid = "#a855f7",
+ intensity = 0.15,
+ speed = 3,
+ angle = 45,
+}: DitherGradientProps) {
+ const canvasRef = useRef(null)
+ const animationRef = useRef(0)
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext("2d")
+ if (!ctx) return
+
+ const resize = () => {
+ const rect = canvas.getBoundingClientRect()
+ canvas.width = rect.width
+ canvas.height = rect.height
+ }
+
+ resize()
+ window.addEventListener("resize", resize)
+
+ let time = 0
+ const bayerMatrix = [
+ [0, 8, 2, 10],
+ [12, 4, 14, 6],
+ [3, 11, 1, 9],
+ [15, 7, 13, 5],
+ ]
+
+ const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+ if (!result) return { r: 0, g: 0, b: 0 }
+ return {
+ r: parseInt(result[1]!, 16),
+ g: parseInt(result[2]!, 16),
+ b: parseInt(result[3]!, 16),
+ }
+ }
+
+ const smoothstep = (t: number) => t * t * (3 - 2 * t)
+
+ const animate = () => {
+ const { width, height } = canvas
+ const imageData = ctx.createImageData(width, height)
+ const data = imageData.data
+
+ const from = hexToRgb(colorFrom)
+ const mid = hexToRgb(colorMid)
+ const to = hexToRgb(colorTo)
+ const rad = (angle * Math.PI) / 180
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const normalizedX = x / width
+ const normalizedY = y / height
+
+ const gradientPos =
+ (normalizedX * Math.cos(rad) + normalizedY * Math.sin(rad)) * 0.8 +
+ 0.1 +
+ Math.sin(time * speed * 0.0008) * 0.1
+
+ const clampedPos = Math.max(0, Math.min(1, gradientPos))
+
+ let r: number, g: number, b: number
+ if (clampedPos < 0.5) {
+ const t = smoothstep(clampedPos * 2)
+ r = from.r + (mid.r - from.r) * t
+ g = from.g + (mid.g - from.g) * t
+ b = from.b + (mid.b - from.b) * t
+ } else {
+ const t = smoothstep((clampedPos - 0.5) * 2)
+ r = mid.r + (to.r - mid.r) * t
+ g = mid.g + (to.g - mid.g) * t
+ b = mid.b + (to.b - mid.b) * t
+ }
+
+ const threshold = (bayerMatrix[y % 4]![x % 4]! / 16 - 0.5) * intensity * 180
+ const noise = (Math.random() - 0.5) * intensity * 60
+
+ const idx = (y * width + x) * 4
+ data[idx] = Math.min(255, Math.max(0, r + threshold + noise))
+ data[idx + 1] = Math.min(255, Math.max(0, g + threshold + noise))
+ data[idx + 2] = Math.min(255, Math.max(0, b + threshold + noise))
+ data[idx + 3] = 255
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0)
+ time += 16
+ animationRef.current = requestAnimationFrame(animate)
+ }
+
+ animate()
+
+ return () => {
+ window.removeEventListener("resize", resize)
+ cancelAnimationFrame(animationRef.current)
+ }
+ }, [colorFrom, colorTo, colorMid, intensity, speed, angle])
+
+ return (
+
+ )
+}
diff --git a/packages/ui/src/components/flight-status-card.tsx b/packages/ui/src/components/flight-status-card.tsx
index 2eec3c8..9ed5303 100644
--- a/packages/ui/src/components/flight-status-card.tsx
+++ b/packages/ui/src/components/flight-status-card.tsx
@@ -2,6 +2,7 @@
import * as React from "react"
import { cn } from "@workspace/ui/lib/utils"
+import { motion } from "framer-motion"
// Dot matrix patterns for each character (5x7 grid)
const DOT_MATRIX: Record = {
@@ -248,6 +249,7 @@ interface DotMatrixCharProps {
activeColor?: string
inactiveColor?: string
className?: string
+ delay?: number
}
function DotMatrixChar({
@@ -257,21 +259,24 @@ function DotMatrixChar({
activeColor = "#b4f54e",
inactiveColor = "rgba(180, 245, 78, 0.1)",
className,
+ delay = 0,
}: DotMatrixCharProps) {
const matrix = DOT_MATRIX[char.toUpperCase()] ?? DOT_MATRIX["A"]!
const width = 5 * dotSize + 4 * gap
const height = 7 * dotSize + 6 * gap
return (
-
{matrix!.map((row, rowIndex) =>
row.map((cell, colIndex) => (
-
))
)}
-
+
)
}
@@ -314,7 +325,10 @@ function DotMatrixText({
className,
}: DotMatrixTextProps) {
return (
-
+
{text.split("").map((char, index) => (
))}
@@ -331,11 +346,19 @@ function DotMatrixText({
function HalftonePattern({ className }: { className?: string }) {
return (
-
-
+
@@ -363,7 +399,7 @@ function HalftonePattern({ className }: { className?: string }) {
fill="url(#halftone)"
mask="url(#halftone-mask)"
/>
-
+
)
}
@@ -429,7 +465,10 @@ function FlightStatusCard({
className,
}: FlightStatusCardProps) {
return (
-
-
+
{departureCity}
-
-
+
+
{departureTime}
-
+
{/* Arrow */}
{/* Arrival */}
@@ -492,17 +544,32 @@ function FlightStatusCard({
gap={2}
charGap={6}
/>
-
+
{arrivalCity}
-
-
+
+
{arrivalTime}
-
+
{/* ETA Panel */}
-
+
{eta}
@@ -513,17 +580,19 @@ function FlightStatusCard({
{nextEvent} {nextEventTime}
-
+
{/* Progress bar */}
{/* Progress fill with gradient and glow */}
-
+
+
{/* Glow effect behind progress */}
-
{/* Remaining time */}
-
+
{remainingTime}
-
+
-
+
)
}
@@ -580,7 +662,10 @@ function FlightStatusCardLight({
className,
}: FlightStatusCardProps) {
return (
-
-
+
{departureCity}
-
-
+
+
{departureTime}
-
+
{/* Arrow */}
{/* Arrival */}
@@ -645,17 +743,32 @@ function FlightStatusCardLight({
activeColor="#2d7a2d"
inactiveColor="rgba(45, 122, 45, 0.15)"
/>
-
+
{arrivalCity}
-
-
+
+
{arrivalTime}
-
+
{/* ETA Panel */}
-
+
{eta}
@@ -666,17 +779,19 @@ function FlightStatusCardLight({
{nextEvent} {nextEventTime}
-
+
{/* Progress bar */}
{/* Progress fill with gradient */}
-
+
+
{/* Remaining time */}
-
+
{remainingTime}
-
+
-
+
)
}
diff --git a/packages/ui/src/components/interactive-hover-button.tsx b/packages/ui/src/components/interactive-hover-button.tsx
new file mode 100644
index 0000000..af3ed79
--- /dev/null
+++ b/packages/ui/src/components/interactive-hover-button.tsx
@@ -0,0 +1,39 @@
+"use client"
+
+import React from "react"
+import { ArrowRight } from "lucide-react"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface InteractiveHoverButtonProps
+ extends React.ButtonHTMLAttributes {
+ text?: string
+}
+
+const InteractiveHoverButton = React.forwardRef<
+ HTMLButtonElement,
+ InteractiveHoverButtonProps
+>(({ text = "Button", className, ...props }, ref) => {
+ return (
+
+
+ {text}
+
+
+
+
+ )
+})
+
+InteractiveHoverButton.displayName = "InteractiveHoverButton"
+
+export { InteractiveHoverButton }
diff --git a/packages/ui/src/components/liquid-blob.tsx b/packages/ui/src/components/liquid-blob.tsx
new file mode 100644
index 0000000..615c5e7
--- /dev/null
+++ b/packages/ui/src/components/liquid-blob.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import { motion, useMotionValue, useSpring } from "framer-motion"
+import { useEffect, useRef } from "react"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface LiquidBlobProps {
+ className?: string
+ color?: string
+ secondaryColor?: string
+ size?: number
+ blur?: number
+ speed?: number
+ opacity?: number
+ interactive?: boolean
+}
+
+export function LiquidBlob({
+ className,
+ color = "#8b5cf6",
+ secondaryColor = "#ec4899",
+ size = 300,
+ blur = 60,
+ speed = 8,
+ opacity = 0.7,
+ interactive = true,
+}: LiquidBlobProps) {
+ const containerRef = useRef(null)
+ const mouseX = useMotionValue(0)
+ const mouseY = useMotionValue(0)
+
+ const springConfig = { damping: 25, stiffness: 150 }
+ const smoothX = useSpring(mouseX, springConfig)
+ const smoothY = useSpring(mouseY, springConfig)
+
+ useEffect(() => {
+ if (!interactive) return
+
+ const container = containerRef.current
+ if (!container) return
+
+ const handleMouseMove = (e: MouseEvent) => {
+ const rect = container.getBoundingClientRect()
+ const x = e.clientX - rect.left - rect.width / 2
+ const y = e.clientY - rect.top - rect.height / 2
+ mouseX.set(x * 0.15)
+ mouseY.set(y * 0.15)
+ }
+
+ const handleMouseLeave = () => {
+ mouseX.set(0)
+ mouseY.set(0)
+ }
+
+ container.addEventListener("mousemove", handleMouseMove)
+ container.addEventListener("mouseleave", handleMouseLeave)
+
+ return () => {
+ container.removeEventListener("mousemove", handleMouseMove)
+ container.removeEventListener("mouseleave", handleMouseLeave)
+ }
+ }, [interactive, mouseX, mouseY])
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/magnetic-dock.tsx b/packages/ui/src/components/magnetic-dock.tsx
new file mode 100644
index 0000000..78b6580
--- /dev/null
+++ b/packages/ui/src/components/magnetic-dock.tsx
@@ -0,0 +1,437 @@
+"use client"
+
+import * as React from "react"
+import { motion, useMotionValue, useSpring, useTransform, AnimatePresence, type MotionValue } from "framer-motion"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface MagneticDockProps {
+ /** Array of dock items */
+ items: DockItemData[]
+ /** Size of icons in pixels */
+ iconSize?: number
+ /** Maximum scale on hover */
+ maxScale?: number
+ /** Distance of magnetic effect in pixels */
+ magneticDistance?: number
+ /** Show labels on hover */
+ showLabels?: boolean
+ /** Dock position */
+ position?: "bottom" | "top" | "left" | "right"
+ /** Background style */
+ variant?: "glass" | "solid" | "transparent"
+ /** Custom class name */
+ className?: string
+}
+
+interface DockItemData {
+ /** Unique identifier */
+ id: string
+ /** Display label */
+ label: string
+ /** Icon component or image URL */
+ icon: React.ReactNode
+ /** Click handler */
+ onClick?: () => void
+ /** Whether item is active */
+ isActive?: boolean
+ /** Badge count */
+ badge?: number
+}
+
+interface DockItemProps {
+ item: DockItemData
+ mouseX: MotionValue
+ iconSize: number
+ maxScale: number
+ magneticDistance: number
+ showLabels: boolean
+ isVertical: boolean
+}
+
+function DockItem({
+ item,
+ mouseX,
+ iconSize,
+ maxScale,
+ magneticDistance,
+ showLabels,
+ isVertical,
+}: DockItemProps) {
+ const ref = React.useRef(null)
+ const [isHovered, setIsHovered] = React.useState(false)
+
+ // Calculate distance from mouse to center of item
+ const distance = useTransform(mouseX, (val: number) => {
+ if (!ref.current) return magneticDistance + 1
+ const rect = ref.current.getBoundingClientRect()
+ const center = isVertical
+ ? rect.top + rect.height / 2
+ : rect.left + rect.width / 2
+ return val - center
+ })
+
+ // Scale based on distance - closer = larger
+ const scale = useTransform(distance, [-magneticDistance, 0, magneticDistance], [1, maxScale, 1])
+
+ // Apply spring physics for smooth animation
+ const springConfig = { damping: 20, stiffness: 300, mass: 0.5 }
+ const smoothScale = useSpring(scale, springConfig)
+
+ // Calculate the size based on scale
+ const size = useTransform(smoothScale, (s) => s * iconSize)
+
+ // Floating effect
+ const y = useTransform(smoothScale, (s) => (s - 1) * -10)
+ const smoothY = useSpring(y, springConfig)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className={cn(
+ "relative flex items-center justify-center",
+ "rounded-2xl transition-colors duration-200",
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 dark:focus-visible:ring-white/50",
+ item.isActive && "bg-neutral-200/50 dark:bg-white/10"
+ )}
+ style={{
+ width: size,
+ height: size,
+ y: isVertical ? 0 : smoothY,
+ x: isVertical ? smoothY : 0,
+ }}
+ whileTap={{ scale: 0.9 }}
+ >
+ {/* Icon Container */}
+
+ {/* Icon */}
+
+ {item.icon}
+
+
+ {/* Shine effect */}
+
+
+
+ {/* Badge */}
+
+ {item.badge !== undefined && item.badge > 0 && (
+
+ {item.badge > 99 ? "99+" : item.badge}
+
+ )}
+
+
+ {/* Active Indicator */}
+
+ {item.isActive && (
+
+ )}
+
+
+ {/* Tooltip */}
+
+ {showLabels && isHovered && (
+
+ {item.label}
+ {/* Tooltip arrow */}
+
+
+ )}
+
+
+ {/* Hover glow */}
+
+
+ )
+}
+
+function MagneticDock({
+ items,
+ iconSize = 56,
+ maxScale = 1.5,
+ magneticDistance = 150,
+ showLabels = true,
+ position = "bottom",
+ variant = "glass",
+ className,
+}: MagneticDockProps) {
+ const mousePosition = useMotionValue(Infinity)
+ const isVertical = position === "left" || position === "right"
+
+ const handleMouseMove = React.useCallback(
+ (e: React.MouseEvent) => {
+ if (isVertical) {
+ mousePosition.set(e.clientY)
+ } else {
+ mousePosition.set(e.clientX)
+ }
+ },
+ [mousePosition, isVertical]
+ )
+
+ const handleMouseLeave = () => {
+ mousePosition.set(Infinity)
+ }
+
+ const variantStyles = {
+ glass: cn(
+ "bg-white/80 dark:bg-neutral-900/80",
+ "backdrop-blur-xl backdrop-saturate-150",
+ "border border-neutral-200 dark:border-neutral-700"
+ ),
+ solid: cn(
+ "bg-neutral-100 dark:bg-neutral-900",
+ "border border-neutral-300 dark:border-neutral-700"
+ ),
+ transparent: "bg-transparent border-0",
+ }
+
+ const positionStyles = {
+ bottom: "flex-row",
+ top: "flex-row",
+ left: "flex-col",
+ right: "flex-col",
+ }
+
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ )
+}
+
+// Preset icons for common use cases
+function DockIconHome({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
+
+function DockIconSearch({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
+
+function DockIconFolder({ className }: { className?: string }) {
+ return (
+
+
+
+ )
+}
+
+function DockIconMail({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
+
+function DockIconMusic({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+ )
+}
+
+function DockIconSettings({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
+
+function DockIconTrash({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
+
+export {
+ MagneticDock,
+ DockIconHome,
+ DockIconSearch,
+ DockIconFolder,
+ DockIconMail,
+ DockIconMusic,
+ DockIconSettings,
+ DockIconTrash,
+ type MagneticDockProps,
+ type DockItemData,
+}
diff --git a/packages/ui/src/components/noise-texture.tsx b/packages/ui/src/components/noise-texture.tsx
new file mode 100644
index 0000000..aaa0b02
--- /dev/null
+++ b/packages/ui/src/components/noise-texture.tsx
@@ -0,0 +1,98 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface NoiseTextureProps {
+ className?: string
+ opacity?: number
+ speed?: number
+ grain?: "fine" | "medium" | "coarse"
+ blend?: "overlay" | "soft-light" | "multiply" | "screen" | "normal"
+ animate?: boolean
+}
+
+export function NoiseTexture({
+ className,
+ opacity = 0.15,
+ speed = 10,
+ grain = "medium",
+ blend = "overlay",
+ animate = true,
+}: NoiseTextureProps) {
+ const canvasRef = useRef(null)
+ const animationRef = useRef(0)
+
+ useEffect(() => {
+ const canvas = canvasRef.current
+ if (!canvas) return
+
+ const ctx = canvas.getContext("2d")
+ if (!ctx) return
+
+ const grainSizes: Record = {
+ fine: 1,
+ medium: 2,
+ coarse: 4,
+ }
+ const grainSize = grainSizes[grain] ?? 2
+
+ const resize = () => {
+ const rect = canvas.getBoundingClientRect()
+ canvas.width = Math.ceil(rect.width / grainSize)
+ canvas.height = Math.ceil(rect.height / grainSize)
+ }
+
+ resize()
+ window.addEventListener("resize", resize)
+
+ const renderNoise = () => {
+ const { width, height } = canvas
+ if (width === 0 || height === 0) return
+
+ const imageData = ctx.createImageData(width, height)
+ const data = imageData.data
+
+ for (let i = 0; i < data.length; i += 4) {
+ const value = Math.random() * 255
+ data[i] = value
+ data[i + 1] = value
+ data[i + 2] = value
+ data[i + 3] = 255
+ }
+
+ ctx.putImageData(imageData, 0, 0)
+ }
+
+ renderNoise()
+
+ if (animate) {
+ const animateNoise = () => {
+ renderNoise()
+ animationRef.current = setTimeout(() => {
+ requestAnimationFrame(animateNoise)
+ }, 1000 / speed)
+ }
+ animateNoise()
+ }
+
+ return () => {
+ window.removeEventListener("resize", resize)
+ if (animationRef.current) {
+ clearTimeout(animationRef.current as NodeJS.Timeout)
+ }
+ }
+ }, [grain, speed, animate])
+
+ return (
+
+ )
+}
diff --git a/packages/ui/src/components/pulsating-button.tsx b/packages/ui/src/components/pulsating-button.tsx
new file mode 100644
index 0000000..fe4b27c
--- /dev/null
+++ b/packages/ui/src/components/pulsating-button.tsx
@@ -0,0 +1,51 @@
+"use client"
+
+import React from "react"
+
+import { cn } from "@workspace/ui/lib/utils"
+
+interface PulsatingButtonProps
+ extends React.ButtonHTMLAttributes {
+ pulseColor?: string
+ duration?: string
+}
+
+const PulsatingButton = React.forwardRef<
+ HTMLButtonElement,
+ PulsatingButtonProps
+>(
+ (
+ {
+ className,
+ children,
+ pulseColor = "#0096ff",
+ duration = "1.5s",
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+ {children}
+
+
+ )
+ },
+)
+
+PulsatingButton.displayName = "PulsatingButton"
+
+export { PulsatingButton }
diff --git a/packages/ui/src/components/shimmer-button.tsx b/packages/ui/src/components/shimmer-button.tsx
new file mode 100644
index 0000000..9d1bdfc
--- /dev/null
+++ b/packages/ui/src/components/shimmer-button.tsx
@@ -0,0 +1,91 @@
+"use client"
+
+import React from "react"
+
+import { cn } from "@workspace/ui/lib/utils"
+
+export interface ShimmerButtonProps
+ extends React.ButtonHTMLAttributes {
+ shimmerColor?: string
+ shimmerSize?: string
+ borderRadius?: string
+ shimmerDuration?: string
+ background?: string
+ className?: string
+ children?: React.ReactNode
+}
+
+const ShimmerButton = React.forwardRef(
+ (
+ {
+ shimmerColor = "#ffffff",
+ shimmerSize = "0.05em",
+ shimmerDuration = "3s",
+ borderRadius = "100px",
+ background = "rgba(0, 0, 0, 1)",
+ className,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+ {/* spark container */}
+
+ {/* spark */}
+
+ {/* spark before */}
+
+
+
+ {children}
+
+ {/* Highlight */}
+
+
+ {/* backdrop */}
+
+
+ )
+ },
+)
+
+ShimmerButton.displayName = "ShimmerButton"
+
+export { ShimmerButton }
diff --git a/packages/ui/src/components/showcase-card.tsx b/packages/ui/src/components/showcase-card.tsx
new file mode 100644
index 0000000..82a010f
--- /dev/null
+++ b/packages/ui/src/components/showcase-card.tsx
@@ -0,0 +1,538 @@
+"use client"
+
+import * as React from "react"
+import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"
+import { cn } from "@workspace/ui/lib/utils"
+
+interface ShowcaseCardProps {
+ /** Top tagline text */
+ tagline?: string
+ /** Main heading text */
+ heading: string
+ /** Description text below heading */
+ description?: string
+ /** Image URL for the hero section */
+ imageUrl: string
+ /** Alt text for the image */
+ imageAlt?: string
+ /** CTA button text */
+ ctaText?: string
+ /** CTA button click handler */
+ onCtaClick?: () => void
+ /** Brand name or logo text */
+ brandName?: string
+ /** Array of service tags */
+ services?: string[]
+ /** Custom class name */
+ className?: string
+ /** Enable 3D tilt effect on hover */
+ enableTilt?: boolean
+ /** Maximum tilt angle in degrees */
+ maxTilt?: number
+ /** Enable parallax effect on image */
+ enableParallax?: boolean
+}
+
+function ShowcaseCard({
+ tagline,
+ heading,
+ description,
+ imageUrl,
+ imageAlt = "Showcase image",
+ ctaText,
+ onCtaClick,
+ brandName,
+ services = [],
+ className,
+ enableTilt = true,
+ maxTilt = 8,
+ enableParallax = true,
+}: ShowcaseCardProps) {
+ const cardRef = React.useRef(null)
+ const [isHovered, setIsHovered] = React.useState(false)
+
+ // Mouse position for tilt effect
+ const mouseX = useMotionValue(0)
+ const mouseY = useMotionValue(0)
+
+ // Smooth spring animation for tilt
+ const springConfig = { damping: 25, stiffness: 150 }
+ const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [maxTilt, -maxTilt]), springConfig)
+ const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-maxTilt, maxTilt]), springConfig)
+
+ // Parallax transform for image
+ const parallaxX = useSpring(useTransform(mouseX, [-0.5, 0.5], [-15, 15]), springConfig)
+ const parallaxY = useSpring(useTransform(mouseY, [-0.5, 0.5], [-15, 15]), springConfig)
+
+ // Glow effect position
+ const glowX = useSpring(useTransform(mouseX, [-0.5, 0.5], [0, 100]), springConfig)
+ const glowY = useSpring(useTransform(mouseY, [-0.5, 0.5], [0, 100]), springConfig)
+
+ const handleMouseMove = React.useCallback(
+ (e: React.MouseEvent) => {
+ if (!cardRef.current || !enableTilt) return
+
+ const rect = cardRef.current.getBoundingClientRect()
+ const x = (e.clientX - rect.left) / rect.width - 0.5
+ const y = (e.clientY - rect.top) / rect.height - 0.5
+
+ mouseX.set(x)
+ mouseY.set(y)
+ },
+ [mouseX, mouseY, enableTilt]
+ )
+
+ const handleMouseEnter = () => {
+ setIsHovered(true)
+ }
+
+ const handleMouseLeave = () => {
+ setIsHovered(false)
+ mouseX.set(0)
+ mouseY.set(0)
+ }
+
+ return (
+
+ {/* Subtle glow overlay on hover */}
+
+
+ {/* Image Section */}
+
+ {/* Tagline */}
+ {tagline && (
+
+
+ {tagline}
+
+
+ )}
+
+ {/* Hero Image with Parallax */}
+
+
+
+
+ {/* Gradient overlay for text readability */}
+
+
+
+ {/* Content Section */}
+
+ {/* Heading */}
+
+ {heading}
+
+
+ {/* Description */}
+ {description && (
+
+ {description}
+
+ )}
+
+ {/* CTA Button */}
+ {ctaText && (
+
+ {/* Button hover shine effect */}
+
+ {ctaText}
+
+ )}
+
+
+ {/* Footer Section */}
+ {(brandName || services.length > 0) && (
+
+
+ {/* Brand Name */}
+ {brandName && (
+
+ {brandName}
+
+ )}
+
+ {/* Services */}
+ {services.length > 0 && (
+
+ {services.map((service, index) => (
+
+
+ {service}
+
+ {index < services.length - 1 && (
+
+ ✦
+
+ )}
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Border glow effect */}
+
+
+ )
+}
+
+// Compact variant for grids
+interface ShowcaseCardCompactProps {
+ heading: string
+ description?: string
+ imageUrl: string
+ imageAlt?: string
+ className?: string
+ onClick?: () => void
+}
+
+function ShowcaseCardCompact({
+ heading,
+ description,
+ imageUrl,
+ imageAlt = "Showcase image",
+ className,
+ onClick,
+}: ShowcaseCardCompactProps) {
+ const [isHovered, setIsHovered] = React.useState(false)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ whileHover={{ scale: 1.02, y: -4 }}
+ whileTap={{ scale: 0.98 }}
+ transition={{ duration: 0.3 }}
+ >
+ {/* Image */}
+
+
+ {/* Content */}
+
+
{heading}
+ {description && (
+
{description}
+ )}
+
+
+ {/* Hover indicator */}
+
+
+
+
+
+
+
+ )
+}
+
+// Horizontal variant for featured sections
+interface ShowcaseCardHorizontalProps extends Omit {
+ imagePosition?: "left" | "right"
+}
+
+function ShowcaseCardHorizontal({
+ tagline,
+ heading,
+ description,
+ imageUrl,
+ imageAlt = "Showcase image",
+ ctaText,
+ onCtaClick,
+ brandName,
+ services = [],
+ className,
+ imagePosition = "left",
+ enableParallax = true,
+}: ShowcaseCardHorizontalProps) {
+ const [isHovered, setIsHovered] = React.useState(false)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ initial={{ opacity: 0, y: 40 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.6 }}
+ whileHover={{ scale: 1.01 }}
+ >
+ {/* Image Section */}
+
+ {tagline && (
+
+ {tagline}
+
+ )}
+
+
+
+
+
+ {/* Content Section */}
+
+
+ {heading}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {ctaText && (
+
+ {ctaText}
+
+ )}
+
+ {(brandName || services.length > 0) && (
+
+ {brandName && (
+
{brandName}
+ )}
+ {services.length > 0 && (
+
+ {services.map((service, index) => (
+
+ {service}
+ {index < services.length - 1 && (
+ ✦
+ )}
+
+ ))}
+
+ )}
+
+ )}
+
+
+ )
+}
+
+// Grid container for showcase cards
+interface ShowcaseGridProps {
+ children: React.ReactNode
+ columns?: 1 | 2 | 3 | 4
+ gap?: "sm" | "md" | "lg"
+ className?: string
+}
+
+function ShowcaseGrid({
+ children,
+ columns = 3,
+ gap = "md",
+ className,
+}: ShowcaseGridProps) {
+ const gapClasses = {
+ sm: "gap-4",
+ md: "gap-6",
+ lg: "gap-8",
+ }
+
+ const columnClasses = {
+ 1: "grid-cols-1",
+ 2: "grid-cols-1 sm:grid-cols-2",
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export {
+ ShowcaseCard,
+ ShowcaseCardCompact,
+ ShowcaseCardHorizontal,
+ ShowcaseGrid,
+ type ShowcaseCardProps,
+ type ShowcaseCardCompactProps,
+ type ShowcaseCardHorizontalProps,
+ type ShowcaseGridProps,
+}
diff --git a/packages/ui/src/components/spotlight-card.tsx b/packages/ui/src/components/spotlight-card.tsx
index 900b758..e5e0922 100644
--- a/packages/ui/src/components/spotlight-card.tsx
+++ b/packages/ui/src/components/spotlight-card.tsx
@@ -57,7 +57,8 @@ function SpotlightCard({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(
- "relative overflow-hidden bg-gradient-to-b from-neutral-950 to-neutral-900",
+ "relative overflow-hidden bg-gradient-to-b",
+ "from-neutral-50 to-white",
"dark:from-neutral-950 dark:to-neutral-900",
"transition-all duration-500",
className
@@ -169,7 +170,8 @@ function SpotlightCardTitle({
return (
{children}
@@ -241,8 +243,9 @@ function MultiSpotlightCard({
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn(
- "relative overflow-hidden bg-neutral-950",
- "border border-neutral-800",
+ "relative overflow-hidden",
+ "bg-white dark:bg-neutral-950",
+ "border border-neutral-200 dark:border-neutral-800",
"transition-all duration-500",
className
)}
@@ -313,8 +316,9 @@ function BeamSpotlightCard({
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn(
- "relative overflow-hidden bg-neutral-950",
- "border border-neutral-800",
+ "relative overflow-hidden",
+ "bg-white dark:bg-neutral-950",
+ "border border-neutral-200 dark:border-neutral-800",
"transition-all duration-500",
className
)}
@@ -440,7 +444,7 @@ function GradientFollowCard({
{/* Base background */}
@@ -525,10 +529,11 @@ function TiltSpotlightCard({
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={handleMouseLeave}
className={cn(
- "relative overflow-hidden bg-neutral-950",
- "border border-neutral-800",
+ "relative overflow-hidden",
+ "bg-white dark:bg-neutral-950",
+ "border border-neutral-200 dark:border-neutral-800",
"transition-[border-color] duration-500",
- isHovered && "border-neutral-700",
+ isHovered && "border-neutral-300 dark:border-neutral-700",
className
)}
style={{
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index a5c6ce3..cf79a4e 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -115,6 +115,48 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
+
+ --animate-border-beam: border-beam calc(var(--duration)*1s) infinite linear;
+ --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite alternate;
+ --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
+
+ @keyframes border-beam {
+ 100% {
+ offset-distance: 100%;
+ }
+ }
+
+ @keyframes shimmer-slide {
+ to {
+ transform: translate(calc(100cqw - 100%), 0);
+ }
+ }
+
+ @keyframes spin-around {
+ 0% {
+ transform: translateZ(0) rotate(0);
+ }
+ 15%, 35% {
+ transform: translateZ(0) rotate(90deg);
+ }
+ 65%, 85% {
+ transform: translateZ(0) rotate(270deg);
+ }
+ 100% {
+ transform: translateZ(0) rotate(360deg);
+ }
+ }
+
+ --animate-pulse: pulse var(--duration) ease-out infinite;
+
+ @keyframes pulse {
+ 0% {
+ box-shadow: 0 0 0 0 var(--pulse-color);
+ }
+ 100% {
+ box-shadow: 0 0 0 8px rgba(0, 0, 0, 0);
+ }
+ }
}
@layer base {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ef6d838..47279e2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@workspace/typescript-config':
specifier: workspace:*
version: link:packages/typescript-config
+ playwright:
+ specifier: ^1.57.0
+ version: 1.57.0
prettier:
specifier: ^3.7.4
version: 3.7.4
@@ -26,12 +29,21 @@ importers:
apps/web:
dependencies:
+ '@vercel/analytics':
+ specifier: ^1.6.1
+ version: 1.6.1(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)
'@workspace/ui':
specifier: workspace:*
version: link:../../packages/ui
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ framer-motion:
+ specifier: ^12.23.26
+ version: 12.23.26(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ lenis:
+ specifier: ^1.3.17
+ version: 1.3.17(react@19.2.1)
lucide-react:
specifier: ^0.475.0
version: 0.475.0(react@19.2.1)
@@ -131,6 +143,12 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ cmdk:
+ specifier: ^1.1.1
+ version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ framer-motion:
+ specifier: ^12.23.26
+ version: 12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
lucide-react:
specifier: ^0.475.0
version: 0.475.0(react@19.1.1)
@@ -1071,6 +1089,32 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ '@vercel/analytics@1.6.1':
+ resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
+ peerDependencies:
+ '@remix-run/react': ^2
+ '@sveltejs/kit': ^1 || ^2
+ next: '>= 13'
+ react: ^18 || ^19 || ^19.0.0-rc
+ svelte: '>= 4'
+ vue: ^3
+ vue-router: ^4
+ peerDependenciesMeta:
+ '@remix-run/react':
+ optional: true
+ '@sveltejs/kit':
+ optional: true
+ next:
+ optional: true
+ react:
+ optional: true
+ svelte:
+ optional: true
+ vue:
+ optional: true
+ vue-router:
+ optional: true
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1620,6 +1664,20 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
+ framer-motion@12.23.26:
+ resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
+ 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
+
fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
@@ -1627,6 +1685,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -2001,6 +2064,20 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ lenis@1.3.17:
+ resolution: {integrity: sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==}
+ peerDependencies:
+ '@nuxt/kit': '>=3.0.0'
+ react: '>=17.0.0'
+ vue: '>=3.0.0'
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+ react:
+ optional: true
+ vue:
+ optional: true
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -2253,6 +2330,12 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ motion-dom@12.23.23:
+ resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
+
+ motion-utils@12.23.6:
+ resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2435,6 +2518,16 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
+ playwright-core@1.57.0:
+ resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.57.0:
+ resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -3412,12 +3505,40 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-context@1.1.2(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
react: 19.2.1
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.9)(react@19.1.1)
+ aria-hidden: 1.2.6
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ react-remove-scroll: 2.7.2(@types/react@19.1.9)(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -3440,6 +3561,19 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -3453,12 +3587,29 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.1)':
dependencies:
react: 19.2.1
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
@@ -3470,6 +3621,13 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-id@1.1.1(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
@@ -3477,6 +3635,16 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@@ -3487,6 +3655,16 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
@@ -3497,6 +3675,15 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1)
@@ -3506,6 +3693,15 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.4(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+ '@types/react-dom': 19.1.7(@types/react@19.1.9)
+
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.1)
@@ -3529,6 +3725,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-slot@1.2.4(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
@@ -3536,12 +3739,26 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
react: 19.2.1
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.1)
@@ -3550,6 +3767,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
@@ -3557,6 +3781,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
@@ -3564,6 +3795,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.9)(react@19.1.1)':
+ dependencies:
+ react: 19.1.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.1)':
dependencies:
react: 19.2.1
@@ -3945,6 +4182,11 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@vercel/analytics@1.6.1(next@16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)':
+ optionalDependencies:
+ next: 16.0.10(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ react: 19.2.1
+
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -4183,6 +4425,18 @@ snapshots:
clsx@2.1.1: {}
+ cmdk@1.1.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.9)(react@19.1.1)
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+
cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
@@ -4678,6 +4932,24 @@ snapshots:
dependencies:
is-callable: 1.2.7
+ framer-motion@12.23.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ motion-dom: 12.23.23
+ motion-utils: 12.23.6
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
+ framer-motion@12.23.26(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
+ dependencies:
+ motion-dom: 12.23.23
+ motion-utils: 12.23.6
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.1
+ react-dom: 19.2.1(react@19.2.1)
+
fs-extra@10.1.0:
dependencies:
graceful-fs: 4.2.11
@@ -4686,6 +4958,9 @@ snapshots:
fs.realpath@1.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -5111,6 +5386,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ lenis@1.3.17(react@19.2.1):
+ optionalDependencies:
+ react: 19.2.1
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -5323,6 +5602,12 @@ snapshots:
mkdirp@3.0.1: {}
+ motion-dom@12.23.23:
+ dependencies:
+ motion-utils: 12.23.6
+
+ motion-utils@12.23.6: {}
+
ms@2.1.3: {}
mute-stream@0.0.8: {}
@@ -5545,6 +5830,14 @@ snapshots:
picomatch@2.3.1: {}
+ playwright-core@1.57.0: {}
+
+ playwright@1.57.0:
+ dependencies:
+ playwright-core: 1.57.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
possible-typed-array-names@1.1.0: {}
postcss@8.4.31:
@@ -5609,6 +5902,14 @@ snapshots:
react-is@16.13.1: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.1.9)(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ react-style-singleton: 2.2.3(@types/react@19.1.9)(react@19.1.1)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.1):
dependencies:
react: 19.2.1
@@ -5617,6 +5918,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ react-remove-scroll@2.7.2(@types/react@19.1.9)(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ react-remove-scroll-bar: 2.3.8(@types/react@19.1.9)(react@19.1.1)
+ react-style-singleton: 2.2.3(@types/react@19.1.9)(react@19.1.1)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.1.9)(react@19.1.1)
+ use-sidecar: 1.1.3(@types/react@19.1.9)(react@19.1.1)
+ optionalDependencies:
+ '@types/react': 19.1.9
+
react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.1):
dependencies:
react: 19.2.1
@@ -5628,6 +5940,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ react-style-singleton@2.2.3(@types/react@19.1.9)(react@19.1.1):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.1.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
get-nonce: 1.0.1
@@ -6195,6 +6515,13 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-callback-ref@1.3.3(@types/react@19.1.9)(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
react: 19.2.1
@@ -6202,6 +6529,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
+ use-sidecar@1.1.3(@types/react@19.1.9)(react@19.1.1):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.1.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.9
+
use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.1):
dependencies:
detect-node-es: 1.1.0
diff --git a/scripts/screenshot.js b/scripts/screenshot.js
new file mode 100644
index 0000000..e1f2515
--- /dev/null
+++ b/scripts/screenshot.js
@@ -0,0 +1,32 @@
+const { chromium } = require("playwright");
+
+async function takeScreenshot() {
+ const browser = await chromium.launch();
+ const context = await browser.newContext({
+ viewport: { width: 1400, height: 800 }, // Wider to show side elements (xl breakpoint)
+ deviceScaleFactor: 3, // 3x for crisp quality
+ colorScheme: "dark",
+ });
+
+ const page = await context.newPage();
+
+ // Wait for fonts and animations to settle
+ await page.goto("http://localhost:3000/", {
+ waitUntil: "networkidle",
+ });
+
+ // Extra wait for fonts and animations to finish
+ await page.waitForTimeout(10000);
+
+ await page.screenshot({
+ path: "apps/web/public/home.png",
+ type: "png",
+ });
+
+ console.log("Screenshot saved to apps/web/public/home.png");
+ console.log("Resolution: 3600x1890 (1200x630 @3x)");
+
+ await browser.close();
+}
+
+takeScreenshot().catch(console.error);