diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index e69de29..dc16add 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -0,0 +1,16 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends("next/core-web-vitals"),
+];
+
+export default eslintConfig;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 4f65814..2af00ae 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,8 +1,9 @@
import React from 'react';
+import '../styles/globals.css';
export const metadata = {
- title: 'SupportHub',
- description: 'Enterprise Support Platform',
+ title: 'SupportHub | Customer Support Platform',
+ description: 'Enterprise AI-powered customer support and ticketing platform for modern teams.',
};
export default function RootLayout({
@@ -11,8 +12,11 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
{children}
+
+
+ {children}
+
);
}
+
diff --git a/src/components/index.ts b/src/components/index.ts
index e69de29..6de577b 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -0,0 +1,3 @@
+export * from './ui';
+export * from './marketing';
+export * from './layout';
diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx
new file mode 100644
index 0000000..9b8cf29
--- /dev/null
+++ b/src/components/layout/footer.tsx
@@ -0,0 +1,170 @@
+import * as React from 'react';
+import { Headphones, Github, Twitter, Linkedin } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Container } from '@/components/marketing/container';
+import { Badge } from '@/components/ui/badge';
+import { IconButton } from '@/components/ui/icon-button';
+
+export interface FooterLink {
+ label: string;
+ href: string;
+ external?: boolean;
+}
+
+export interface FooterColumn {
+ title: string;
+ links: FooterLink[];
+}
+
+export interface FooterProps extends React.HTMLAttributes {
+ brandName?: string;
+ description?: string;
+ columns?: FooterColumn[];
+ copyright?: string;
+}
+
+const defaultColumns: FooterColumn[] = [
+ {
+ title: 'Product',
+ links: [
+ { label: 'AI Support Co-Pilot', href: '#co-pilot' },
+ { label: 'Omnichannel Ticketing', href: '#ticketing' },
+ { label: 'Knowledge Base', href: '#knowledge-base' },
+ { label: 'Analytics & SLA', href: '#analytics' },
+ { label: 'Integrations', href: '#integrations' },
+ ],
+ },
+ {
+ title: 'Solutions',
+ links: [
+ { label: 'Enterprise SaaS', href: '#enterprise' },
+ { label: 'E-Commerce', href: '#ecommerce' },
+ { label: 'FinTech', href: '#fintech' },
+ { label: 'Startups', href: '#startups' },
+ ],
+ },
+ {
+ title: 'Resources',
+ links: [
+ { label: 'Documentation', href: '#docs' },
+ { label: 'API Reference', href: '#api' },
+ { label: 'Guides & Case Studies', href: '#guides' },
+ { label: 'Community', href: '#community' },
+ { label: 'Status Page', href: '#status' },
+ ],
+ },
+ {
+ title: 'Company',
+ links: [
+ { label: 'About Us', href: '#about' },
+ { label: 'Careers', href: '#careers' },
+ { label: 'Blog', href: '#blog' },
+ { label: 'Press Kit', href: '#press' },
+ { label: 'Contact Sales', href: '#contact' },
+ ],
+ },
+ {
+ title: 'Legal',
+ links: [
+ { label: 'Privacy Policy', href: '#privacy' },
+ { label: 'Terms of Service', href: '#terms' },
+ { label: 'Security & SOC 2', href: '#security' },
+ { label: 'GDPR Compliance', href: '#gdpr' },
+ ],
+ },
+];
+
+const Footer = React.forwardRef(
+ (
+ {
+ className,
+ brandName = 'SupportHub',
+ description = 'The enterprise customer support platform built for modern high-growth SaaS teams.',
+ columns = defaultColumns,
+ copyright = `© ${new Date().getFullYear()} SupportHub, Inc. All rights reserved.`,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ );
+ }
+);
+
+Footer.displayName = 'Footer';
+
+export { Footer };
diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts
new file mode 100644
index 0000000..5def158
--- /dev/null
+++ b/src/components/layout/index.ts
@@ -0,0 +1,4 @@
+export * from './navbar';
+export * from './mobile-menu';
+export * from './footer';
+export * from './page-container';
diff --git a/src/components/layout/mobile-menu.tsx b/src/components/layout/mobile-menu.tsx
new file mode 100644
index 0000000..ffb25a2
--- /dev/null
+++ b/src/components/layout/mobile-menu.tsx
@@ -0,0 +1,97 @@
+import * as React from 'react';
+import { X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Badge } from '@/components/ui/badge';
+import { IconButton } from '@/components/ui/icon-button';
+
+export interface NavLinkItem {
+ label: string;
+ href: string;
+ badge?: string;
+ external?: boolean;
+}
+
+export interface MobileMenuProps {
+ open: boolean;
+ onClose: () => void;
+ brand: React.ReactNode;
+ navLinks: NavLinkItem[];
+ actions?: React.ReactNode;
+}
+
+const MobileMenu: React.FC = ({
+ open,
+ onClose,
+ brand,
+ navLinks,
+ actions,
+}) => {
+ React.useEffect(() => {
+ if (open) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, [open]);
+
+ if (!open) return null;
+
+ return (
+
+ {/* Backdrop Overlay */}
+
+
+ {/* Slide-over Drawer Panel */}
+
+
+
+ {/* Footer Actions */}
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+ );
+};
+
+export { MobileMenu };
diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx
new file mode 100644
index 0000000..f263f6c
--- /dev/null
+++ b/src/components/layout/navbar.tsx
@@ -0,0 +1,144 @@
+import * as React from 'react';
+import { Menu, Headphones } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Container } from '@/components/marketing/container';
+import { Button } from '@/components/ui/button';
+import { IconButton } from '@/components/ui/icon-button';
+import { Badge } from '@/components/ui/badge';
+import { MobileMenu, NavLinkItem } from './mobile-menu';
+
+export interface NavbarProps extends React.HTMLAttributes {
+ brandName?: string;
+ brandLogo?: React.ReactNode;
+ navLinks?: NavLinkItem[];
+ actions?: React.ReactNode;
+ sticky?: boolean;
+}
+
+const defaultNavLinks: NavLinkItem[] = [
+ { label: 'Features', href: '#features' },
+ { label: 'Solutions', href: '#solutions' },
+ { label: 'Pricing', href: '#pricing' },
+ { label: 'Resources', href: '#resources' },
+];
+
+const Navbar = React.forwardRef(
+ (
+ {
+ className,
+ brandName = 'SupportHub',
+ brandLogo,
+ navLinks = defaultNavLinks,
+ actions,
+ sticky = true,
+ ...props
+ },
+ ref
+ ) => {
+ const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
+ const [isScrolled, setIsScrolled] = React.useState(false);
+
+ React.useEffect(() => {
+ const handleScroll = () => {
+ if (window.scrollY > 20) {
+ setIsScrolled(true);
+ } else {
+ setIsScrolled(false);
+ }
+ };
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ const logoNode = brandLogo || (
+
+
+
+
+
+ {brandName}
+
+
+ );
+
+ const actionNode = actions || (
+
+ );
+
+ return (
+
+
+
+ {/* Brand Logo */}
+
+
+ {/* Desktop Actions */}
+
{actionNode}
+
+ {/* Mobile Menu Trigger */}
+
+ setMobileMenuOpen(true)}
+ >
+
+
+
+
+
+
+ {/* Mobile Slide-over Menu */}
+ setMobileMenuOpen(false)}
+ brand={logoNode}
+ navLinks={navLinks}
+ actions={actionNode}
+ />
+
+ );
+ }
+);
+
+Navbar.displayName = 'Navbar';
+
+export { Navbar };
diff --git a/src/components/layout/page-container.tsx b/src/components/layout/page-container.tsx
new file mode 100644
index 0000000..0f86d6a
--- /dev/null
+++ b/src/components/layout/page-container.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { ToastProvider } from '@/components/ui/toast';
+import { Navbar, NavbarProps } from './navbar';
+import { Footer, FooterProps } from './footer';
+
+export interface PageContainerProps extends React.HTMLAttributes {
+ showNavbar?: boolean;
+ showFooter?: boolean;
+ navbarProps?: NavbarProps;
+ footerProps?: FooterProps;
+}
+
+const PageContainer = React.forwardRef(
+ (
+ {
+ className,
+ showNavbar = true,
+ showFooter = true,
+ navbarProps,
+ footerProps,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+
+ {showNavbar && }
+ {children}
+ {showFooter && }
+
+
+ );
+ }
+);
+
+PageContainer.displayName = 'PageContainer';
+
+export { PageContainer };
diff --git a/src/components/marketing/browser-mockup.tsx b/src/components/marketing/browser-mockup.tsx
new file mode 100644
index 0000000..1fb7ec5
--- /dev/null
+++ b/src/components/marketing/browser-mockup.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import { Lock } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export interface BrowserMockupProps extends React.HTMLAttributes {
+ url?: string;
+ children: React.ReactNode;
+}
+
+const BrowserMockup = React.forwardRef(
+ ({ className, url = 'https://app.supporthub.io', children, ...props }, ref) => {
+ return (
+
+ {/* Browser Header Bar */}
+
+ {/* Window Traffic Lights */}
+
+
+
+
+
+
+ {/* Address Bar */}
+
+
+ {url}
+
+
+ {/* Spacer */}
+
+
+
+ {/* Viewport Content */}
+
+ {children}
+
+
+ );
+ }
+);
+
+BrowserMockup.displayName = 'BrowserMockup';
+
+export { BrowserMockup };
diff --git a/src/components/marketing/container.tsx b/src/components/marketing/container.tsx
new file mode 100644
index 0000000..e314a38
--- /dev/null
+++ b/src/components/marketing/container.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface ContainerProps extends React.HTMLAttributes {
+ size?: 'narrow' | 'default' | 'wide' | 'full';
+ as?: React.ElementType;
+}
+
+const sizeClasses = {
+ narrow: 'max-w-4xl',
+ default: 'max-w-7xl',
+ wide: 'max-w-7xl lg:max-w-[90rem]',
+ full: 'max-w-none',
+};
+
+const Container = React.forwardRef(
+ ({ className, size = 'default', as: Component = 'div', children, ...props }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+Container.displayName = 'Container';
+
+export { Container };
diff --git a/src/components/marketing/cta-section.tsx b/src/components/marketing/cta-section.tsx
new file mode 100644
index 0000000..ea36b31
--- /dev/null
+++ b/src/components/marketing/cta-section.tsx
@@ -0,0 +1,70 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { Container } from './container';
+import { Eyebrow } from './eyebrow';
+
+export interface CTASectionProps extends Omit, 'title'> {
+ title: React.ReactNode;
+ description?: React.ReactNode;
+ primaryAction?: React.ReactNode;
+ secondaryAction?: React.ReactNode;
+ eyebrow?: string;
+ variant?: 'default' | 'card' | 'dark';
+}
+
+const CTASection = React.forwardRef(
+ (
+ {
+ className,
+ title,
+ description,
+ primaryAction,
+ secondaryAction,
+ eyebrow,
+ variant = 'card',
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+
+
+ {/* Background Glow */}
+
+
+ {eyebrow &&
{eyebrow}}
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {(primaryAction || secondaryAction) && (
+
+ {primaryAction}
+ {secondaryAction}
+
+ )}
+
+
+
+ );
+ }
+);
+
+CTASection.displayName = 'CTASection';
+
+export { CTASection };
diff --git a/src/components/marketing/eyebrow.tsx b/src/components/marketing/eyebrow.tsx
new file mode 100644
index 0000000..ad4fb17
--- /dev/null
+++ b/src/components/marketing/eyebrow.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface EyebrowProps extends React.HTMLAttributes {
+ icon?: React.ReactNode;
+ variant?: 'default' | 'primary' | 'outline';
+}
+
+const Eyebrow = React.forwardRef(
+ ({ className, icon, variant = 'primary', children, ...props }, ref) => {
+ const variantClasses = {
+ default: 'bg-muted text-muted-foreground border-border/60',
+ primary: 'bg-primary/10 text-primary border-primary/20 hover:bg-primary/15',
+ outline: 'bg-background text-foreground border-border shadow-subtle',
+ };
+
+ return (
+
+ {icon && {icon}}
+ {children}
+
+ );
+ }
+);
+
+Eyebrow.displayName = 'Eyebrow';
+
+export { Eyebrow };
diff --git a/src/components/marketing/feature-card.tsx b/src/components/marketing/feature-card.tsx
new file mode 100644
index 0000000..b74c2be
--- /dev/null
+++ b/src/components/marketing/feature-card.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+import { ArrowRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+
+export interface FeatureCardProps extends React.HTMLAttributes {
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+ badge?: string;
+ linkText?: string;
+ onLinkClick?: () => void;
+ variant?: 'default' | 'bordered' | 'flat' | 'gradient';
+}
+
+const FeatureCard = React.forwardRef(
+ (
+ {
+ className,
+ icon,
+ title,
+ description,
+ badge,
+ linkText,
+ onLinkClick,
+ variant = 'default',
+ ...props
+ },
+ ref
+ ) => {
+ const variantClasses = {
+ default: 'bg-card border-border hover:border-primary/30 hover:shadow-float',
+ bordered: 'bg-background border-2 border-border hover:border-primary/50',
+ flat: 'bg-muted/40 border-transparent shadow-none hover:bg-muted/70',
+ gradient: 'bg-gradient-to-b from-card to-accent/20 border-border/80 hover:border-primary/40',
+ };
+
+ return (
+
+
+
+
+ {icon}
+
+ {badge &&
{badge}}
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {linkText && (
+
+ )}
+
+
+ );
+ }
+);
+
+FeatureCard.displayName = 'FeatureCard';
+
+export { FeatureCard };
diff --git a/src/components/marketing/gradient-text.tsx b/src/components/marketing/gradient-text.tsx
new file mode 100644
index 0000000..6cabdbb
--- /dev/null
+++ b/src/components/marketing/gradient-text.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface GradientTextProps extends React.HTMLAttributes {
+ gradient?: 'primary' | 'brand' | 'emerald' | 'amber' | 'subtle';
+ as?: React.ElementType;
+}
+
+const gradientClasses = {
+ primary: 'bg-gradient-to-r from-indigo-600 via-indigo-500 to-purple-600 dark:from-indigo-400 dark:via-indigo-300 dark:to-purple-400',
+ brand: 'bg-gradient-to-r from-primary via-indigo-500 to-violet-600 dark:from-indigo-400 dark:to-violet-400',
+ emerald: 'bg-gradient-to-r from-emerald-600 to-teal-500 dark:from-emerald-400 dark:to-teal-300',
+ amber: 'bg-gradient-to-r from-amber-600 to-orange-500 dark:from-amber-400 dark:to-orange-300',
+ subtle: 'bg-gradient-to-r from-foreground via-foreground/90 to-foreground/70',
+};
+
+const GradientText = React.forwardRef(
+ ({ className, gradient = 'primary', as: Component = 'span', children, ...props }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+GradientText.displayName = 'GradientText';
+
+export { GradientText };
diff --git a/src/components/marketing/index.ts b/src/components/marketing/index.ts
new file mode 100644
index 0000000..6c81b95
--- /dev/null
+++ b/src/components/marketing/index.ts
@@ -0,0 +1,14 @@
+export * from './section';
+export * from './section-header';
+export * from './container';
+export * from './gradient-text';
+export * from './eyebrow';
+export * from './feature-card';
+export * from './step-card';
+export * from './metric-card';
+export * from './logo-cloud';
+export * from './testimonial-card';
+export * from './cta-section';
+export * from './product-mockup';
+export * from './browser-mockup';
+export * from './screenshot-frame';
diff --git a/src/components/marketing/logo-cloud.tsx b/src/components/marketing/logo-cloud.tsx
new file mode 100644
index 0000000..46d2b48
--- /dev/null
+++ b/src/components/marketing/logo-cloud.tsx
@@ -0,0 +1,57 @@
+/* eslint-disable @next/next/no-img-element */
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface LogoItem {
+ name: string;
+ logoUrl?: string;
+ svg?: React.ReactNode;
+}
+
+export interface LogoCloudProps extends React.HTMLAttributes {
+ title?: string;
+ logos: LogoItem[];
+}
+
+const LogoCloud = React.forwardRef(
+ ({ className, title, logos, ...props }, ref) => {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {logos.map((logo, index) => (
+
+ {logo.svg ? (
+ logo.svg
+ ) : logo.logoUrl ? (
+

+ ) : (
+
{logo.name}
+ )}
+
+ ))}
+
+
+ );
+ }
+);
+
+LogoCloud.displayName = 'LogoCloud';
+
+export { LogoCloud };
diff --git a/src/components/marketing/metric-card.tsx b/src/components/marketing/metric-card.tsx
new file mode 100644
index 0000000..fc8e573
--- /dev/null
+++ b/src/components/marketing/metric-card.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react';
+import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+
+export interface MetricCardProps extends React.HTMLAttributes {
+ value: string;
+ label: string;
+ trend?: string;
+ trendDirection?: 'up' | 'down' | 'neutral';
+ subtext?: string;
+ icon?: React.ReactNode;
+}
+
+const MetricCard = React.forwardRef(
+ (
+ {
+ className,
+ value,
+ label,
+ trend,
+ trendDirection = 'up',
+ subtext,
+ icon,
+ ...props
+ },
+ ref
+ ) => {
+ const trendIcons = {
+ up: ,
+ down: ,
+ neutral: ,
+ };
+
+ const trendVariants = {
+ up: 'success' as const,
+ down: 'destructive' as const,
+ neutral: 'secondary' as const,
+ };
+
+ return (
+
+
+
+
+ {label}
+
+ {icon &&
{icon}
}
+
+
+
+
+ {value}
+
+ {trend && (
+
+ {trendIcons[trendDirection]}
+ {trend}
+
+ )}
+
+
+ {subtext && (
+ {subtext}
+ )}
+
+
+ );
+ }
+);
+
+MetricCard.displayName = 'MetricCard';
+
+export { MetricCard };
diff --git a/src/components/marketing/product-mockup.tsx b/src/components/marketing/product-mockup.tsx
new file mode 100644
index 0000000..569182d
--- /dev/null
+++ b/src/components/marketing/product-mockup.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { BrowserMockup } from './browser-mockup';
+
+export interface ProductMockupProps extends React.HTMLAttributes {
+ url?: string;
+ glow?: boolean;
+}
+
+const ProductMockup = React.forwardRef(
+ ({ className, url, glow = true, children, ...props }, ref) => {
+ return (
+
+ {/* Subtle Ambient Radial Glow behind the mockup */}
+ {glow && (
+
+ )}
+
{children}
+
+ );
+ }
+);
+
+ProductMockup.displayName = 'ProductMockup';
+
+export { ProductMockup };
diff --git a/src/components/marketing/screenshot-frame.tsx b/src/components/marketing/screenshot-frame.tsx
new file mode 100644
index 0000000..b2870b6
--- /dev/null
+++ b/src/components/marketing/screenshot-frame.tsx
@@ -0,0 +1,78 @@
+/* eslint-disable @next/next/no-img-element */
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { Skeleton } from '@/components/ui/skeleton';
+
+export interface ScreenshotFrameProps extends React.HTMLAttributes {
+ src?: string;
+ alt?: string;
+ caption?: string;
+ aspectRatio?: '16/9' | '4/3' | '1/1' | 'auto';
+}
+
+const ScreenshotFrame = React.forwardRef(
+ (
+ {
+ className,
+ src,
+ alt = 'App Screenshot',
+ caption,
+ aspectRatio = '16/9',
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [loaded, setLoaded] = React.useState(false);
+
+ const aspectClasses = {
+ '16/9': 'aspect-video',
+ '4/3': 'aspect-[4/3]',
+ '1/1': 'aspect-square',
+ auto: 'aspect-auto',
+ };
+
+ return (
+
+
+ {src ? (
+ <>
+ {!loaded && (
+
+ )}
+

setLoaded(true)}
+ className={cn(
+ 'h-full w-full object-cover transition-opacity duration-300',
+ loaded ? 'opacity-100' : 'opacity-0'
+ )}
+ />
+ >
+ ) : (
+ children
+ )}
+
+ {caption && (
+
+ {caption}
+
+ )}
+
+ );
+ }
+);
+
+ScreenshotFrame.displayName = 'ScreenshotFrame';
+
+export { ScreenshotFrame };
diff --git a/src/components/marketing/section-header.tsx b/src/components/marketing/section-header.tsx
new file mode 100644
index 0000000..b0ad353
--- /dev/null
+++ b/src/components/marketing/section-header.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+import { Eyebrow } from './eyebrow';
+
+export interface SectionHeaderProps extends Omit, 'title'> {
+ eyebrow?: React.ReactNode;
+ title: React.ReactNode;
+ description?: React.ReactNode;
+ align?: 'left' | 'center';
+ actions?: React.ReactNode;
+}
+
+const SectionHeader = React.forwardRef(
+ (
+ {
+ className,
+ eyebrow,
+ title,
+ description,
+ align = 'center',
+ actions,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ {eyebrow && (
+ typeof eyebrow === 'string' ? (
+
{eyebrow}
+ ) : (
+ eyebrow
+ )
+ )}
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+ {actions &&
{actions}
}
+
+ );
+ }
+);
+
+SectionHeader.displayName = 'SectionHeader';
+
+export { SectionHeader };
diff --git a/src/components/marketing/section.tsx b/src/components/marketing/section.tsx
new file mode 100644
index 0000000..0c06c82
--- /dev/null
+++ b/src/components/marketing/section.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface SectionProps extends React.HTMLAttributes {
+ spacing?: 'sm' | 'md' | 'lg' | 'none';
+ background?: 'default' | 'muted' | 'dark' | 'gradient';
+}
+
+const spacingClasses = {
+ none: 'py-0',
+ sm: 'py-12 sm:py-16',
+ md: 'py-16 sm:py-24',
+ lg: 'py-20 sm:py-32',
+};
+
+const backgroundClasses = {
+ default: 'bg-background text-foreground',
+ muted: 'bg-muted/50 text-foreground border-y border-border/50',
+ dark: 'bg-slate-950 text-slate-50 dark:bg-slate-950 dark:text-slate-50',
+ gradient: 'bg-gradient-to-b from-background via-accent/30 to-background text-foreground',
+};
+
+const Section = React.forwardRef(
+ (
+ {
+ className,
+ spacing = 'md',
+ background = 'default',
+ children,
+ id,
+ ...props
+ },
+ ref
+ ) => {
+ return (
+
+ );
+ }
+);
+
+Section.displayName = 'Section';
+
+export { Section };
diff --git a/src/components/marketing/step-card.tsx b/src/components/marketing/step-card.tsx
new file mode 100644
index 0000000..a39b436
--- /dev/null
+++ b/src/components/marketing/step-card.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface StepCardProps extends React.HTMLAttributes {
+ stepNumber: number | string;
+ title: string;
+ description: string;
+ icon?: React.ReactNode;
+ isLast?: boolean;
+}
+
+const StepCard = React.forwardRef(
+ (
+ {
+ className,
+ stepNumber,
+ title,
+ description,
+ icon,
+ isLast = false,
+ ...props
+ },
+ ref
+ ) => {
+ const formattedStep =
+ typeof stepNumber === 'number' && stepNumber < 10
+ ? `0${stepNumber}`
+ : stepNumber;
+
+ return (
+
+
+
+ {formattedStep}
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+
{title}
+
{description}
+
+ );
+ }
+);
+
+StepCard.displayName = 'StepCard';
+
+export { StepCard };
diff --git a/src/components/marketing/testimonial-card.tsx b/src/components/marketing/testimonial-card.tsx
new file mode 100644
index 0000000..0f2fe75
--- /dev/null
+++ b/src/components/marketing/testimonial-card.tsx
@@ -0,0 +1,80 @@
+import * as React from 'react';
+import { Star, Quote } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Card, CardContent } from '@/components/ui/card';
+import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
+
+export interface TestimonialCardProps extends React.HTMLAttributes {
+ quote: string;
+ authorName: string;
+ authorTitle: string;
+ authorCompany?: string;
+ avatarUrl?: string;
+ rating?: number;
+}
+
+const TestimonialCard = React.forwardRef(
+ (
+ {
+ className,
+ quote,
+ authorName,
+ authorTitle,
+ authorCompany,
+ avatarUrl,
+ rating = 5,
+ ...props
+ },
+ ref
+ ) => {
+ const initials = authorName
+ .split(' ')
+ .map((n) => n[0])
+ .join('')
+ .substring(0, 2);
+
+ return (
+
+
+
+
+
+ {Array.from({ length: rating }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+ “{quote}”
+
+
+
+
+
+
+ {initials}
+
+
+
{authorName}
+
+ {authorTitle}
+ {authorCompany && • {authorCompany}}
+
+
+
+
+
+ );
+ }
+);
+
+TestimonialCard.displayName = 'TestimonialCard';
+
+export { TestimonialCard };
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..fa52667
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -0,0 +1,142 @@
+import * as React from 'react';
+import { ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface AccordionContextValue {
+ openItems: string[];
+ toggleItem: (value: string) => void;
+}
+
+const AccordionContext = React.createContext(null);
+
+export interface AccordionProps extends React.HTMLAttributes {
+ type?: 'single' | 'multiple';
+ defaultValue?: string | string[];
+}
+
+const Accordion = React.forwardRef(
+ ({ type = 'single', defaultValue, children, className, ...props }, ref) => {
+ const [openItems, setOpenItems] = React.useState(() => {
+ if (!defaultValue) return [];
+ return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
+ });
+
+ const toggleItem = React.useCallback(
+ (value: string) => {
+ setOpenItems((prev) => {
+ if (type === 'single') {
+ return prev.includes(value) ? [] : [value];
+ } else {
+ return prev.includes(value)
+ ? prev.filter((v) => v !== value)
+ : [...prev, value];
+ }
+ });
+ },
+ [type]
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+Accordion.displayName = 'Accordion';
+
+interface AccordionItemContextValue {
+ value: string;
+}
+
+const AccordionItemContext = React.createContext(null);
+
+export interface AccordionItemProps extends React.HTMLAttributes {
+ value: string;
+}
+
+const AccordionItem = React.forwardRef(
+ ({ value, children, className, ...props }, ref) => (
+
+
+ {children}
+
+
+ )
+);
+AccordionItem.displayName = 'AccordionItem';
+
+export interface AccordionTriggerProps
+ extends React.ButtonHTMLAttributes {}
+
+const AccordionTrigger = React.forwardRef(
+ ({ children, className, ...props }, ref) => {
+ const accContext = React.useContext(AccordionContext);
+ const itemContext = React.useContext(AccordionItemContext);
+
+ if (!accContext || !itemContext) {
+ throw new Error('AccordionTrigger must be used inside Accordion and AccordionItem');
+ }
+
+ const isOpen = accContext.openItems.includes(itemContext.value);
+
+ return (
+
+ );
+ }
+);
+AccordionTrigger.displayName = 'AccordionTrigger';
+
+export interface AccordionContentProps
+ extends React.HTMLAttributes {}
+
+const AccordionContent = React.forwardRef(
+ ({ children, className, ...props }, ref) => {
+ const accContext = React.useContext(AccordionContext);
+ const itemContext = React.useContext(AccordionItemContext);
+
+ if (!accContext || !itemContext) {
+ throw new Error('AccordionContent must be used inside Accordion and AccordionItem');
+ }
+
+ const isOpen = accContext.openItems.includes(itemContext.value);
+
+ if (!isOpen) return null;
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+AccordionContent.displayName = 'AccordionContent';
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..91dbba7
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react';
+import { AlertCircle, CheckCircle2, AlertTriangle, Info } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export interface AlertProps extends React.HTMLAttributes {
+ variant?: 'default' | 'info' | 'success' | 'warning' | 'destructive';
+ icon?: React.ReactNode;
+}
+
+const variantMap = {
+ default: 'bg-background text-foreground border-border',
+ info: 'bg-sky-50 dark:bg-sky-950/30 text-sky-900 dark:text-sky-200 border-sky-200 dark:border-sky-800',
+ success: 'bg-emerald-50 dark:bg-emerald-950/30 text-emerald-900 dark:text-emerald-200 border-emerald-200 dark:border-emerald-800',
+ warning: 'bg-amber-50 dark:bg-amber-950/30 text-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800',
+ destructive: 'bg-rose-50 dark:bg-rose-950/30 text-rose-900 dark:text-rose-200 border-rose-200 dark:border-rose-800',
+};
+
+const defaultIcons = {
+ default: ,
+ info: ,
+ success: ,
+ warning: ,
+ destructive: ,
+};
+
+const Alert = React.forwardRef(
+ ({ className, variant = 'default', icon, children, ...props }, ref) => {
+ const displayIcon = icon !== undefined ? icon : defaultIcons[variant];
+
+ return (
+
+ {displayIcon &&
{displayIcon}
}
+
{children}
+
+ );
+ }
+);
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..9b66b98
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,98 @@
+/* eslint-disable @next/next/no-img-element */
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface AvatarProps extends React.HTMLAttributes {
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ status?: 'online' | 'offline' | 'busy' | 'away';
+}
+
+const sizeMap = {
+ sm: 'h-8 w-8 text-xs',
+ md: 'h-10 w-10 text-sm',
+ lg: 'h-12 w-12 text-base',
+ xl: 'h-16 w-16 text-lg',
+};
+
+const Avatar = React.forwardRef(
+ ({ className, size = 'md', status, children, ...props }, ref) => {
+ const statusColors = {
+ online: 'bg-emerald-500',
+ offline: 'bg-slate-400',
+ busy: 'bg-rose-500',
+ away: 'bg-amber-500',
+ };
+
+ return (
+
+
+ {children}
+
+ {status && (
+
+ )}
+
+ );
+ }
+);
+Avatar.displayName = 'Avatar';
+
+export interface AvatarImageProps
+ extends React.ImgHTMLAttributes {
+ onLoadingStatusChange?: (status: 'loading' | 'loaded' | 'error') => void;
+}
+
+const AvatarImage = React.forwardRef(
+ ({ className, src, alt = '', ...props }, ref) => {
+ const [hasError, setHasError] = React.useState(false);
+
+ if (!src || hasError) return null;
+
+ return (
+
setHasError(true)}
+ className={cn('aspect-square h-full w-full object-cover', className)}
+ {...props}
+ />
+ );
+ }
+);
+AvatarImage.displayName = 'AvatarImage';
+
+export interface AvatarFallbackProps
+ extends React.HTMLAttributes {}
+
+const AvatarFallback = React.forwardRef(
+ ({ className, children, ...props }, ref) => (
+
+ {children}
+
+ )
+);
+AvatarFallback.displayName = 'AvatarFallback';
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..5a99485
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,88 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface BadgeProps extends React.HTMLAttributes {
+ variant?:
+ | 'default'
+ | 'primary'
+ | 'secondary'
+ | 'outline'
+ | 'success'
+ | 'warning'
+ | 'destructive'
+ | 'info';
+ size?: 'sm' | 'md';
+ dot?: boolean;
+}
+
+const Badge = React.forwardRef(
+ (
+ {
+ className,
+ variant = 'default',
+ size = 'md',
+ dot = false,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const variants = {
+ default:
+ 'border-transparent bg-primary/10 text-primary hover:bg-primary/20',
+ primary:
+ 'border-transparent bg-primary/10 text-primary hover:bg-primary/20',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ outline: 'border-border text-foreground bg-background',
+ success:
+ 'border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20',
+ warning:
+ 'border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20',
+ destructive:
+ 'border-transparent bg-destructive/10 text-destructive hover:bg-destructive/20',
+ info: 'border-transparent bg-sky-500/10 text-sky-600 dark:text-sky-400 hover:bg-sky-500/20',
+ };
+
+ const dotColors = {
+ default: 'bg-primary',
+ primary: 'bg-primary',
+ secondary: 'bg-secondary-foreground',
+ outline: 'bg-foreground',
+ success: 'bg-emerald-500',
+ warning: 'bg-amber-500',
+ destructive: 'bg-destructive',
+ info: 'bg-sky-500',
+ };
+
+ const sizes = {
+ sm: 'px-2 py-0.5 text-xs font-medium',
+ md: 'px-2.5 py-1 text-xs font-semibold',
+ };
+
+ return (
+
+ {dot && (
+
+ )}
+ {children}
+
+ );
+ }
+);
+
+Badge.displayName = 'Badge';
+
+export { Badge };
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..f2e14a6
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,84 @@
+import * as React from 'react';
+import { Loader2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link';
+ size?: 'sm' | 'md' | 'lg' | 'icon';
+ isLoading?: boolean;
+ leftIcon?: React.ReactNode;
+ rightIcon?: React.ReactNode;
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ (
+ {
+ className,
+ variant = 'primary',
+ size = 'md',
+ isLoading = false,
+ leftIcon,
+ rightIcon,
+ disabled,
+ children,
+ asChild = false,
+ type = 'button',
+ ...props
+ },
+ ref
+ ) => {
+ const baseStyles =
+ 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-[0.98] select-none';
+
+ const variants = {
+ primary:
+ 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md border border-transparent',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 border border-transparent',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground text-foreground shadow-subtle',
+ ghost: 'hover:bg-accent hover:text-accent-foreground text-foreground',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
+ link: 'text-primary underline-offset-4 hover:underline p-0 h-auto',
+ };
+
+ const sizes = {
+ sm: 'h-8 px-3 text-xs gap-1.5',
+ md: 'h-10 px-4 text-sm gap-2',
+ lg: 'h-12 px-6 text-base gap-2.5',
+ icon: 'h-10 w-10 p-0 text-sm justify-center',
+ };
+
+ const combinedClassName = cn(baseStyles, variants[variant], sizes[size], className);
+
+ if (asChild && React.isValidElement(children)) {
+ return React.cloneElement(children as React.ReactElement<{ className?: string }>, {
+ className: cn(combinedClassName, (children.props as { className?: string }).className),
+ });
+ }
+
+ return (
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
+
+export { Button };
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..596c531
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface CardProps extends React.HTMLAttributes {
+ hoverable?: boolean;
+}
+
+const Card = React.forwardRef(
+ ({ className, hoverable = false, ...props }, ref) => (
+
+ )
+);
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+};
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..76ee2c2
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,189 @@
+import * as React from 'react';
+import { X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface DialogContextValue {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+const DialogContext = React.createContext(null);
+
+export interface DialogProps {
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ defaultOpen?: boolean;
+ children: React.ReactNode;
+}
+
+const Dialog: React.FC = ({
+ open: controlledOpen,
+ onOpenChange,
+ defaultOpen = false,
+ children,
+}) => {
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
+ const isOpen = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
+
+ const handleOpenChange = React.useCallback(
+ (newOpen: boolean) => {
+ if (controlledOpen === undefined) {
+ setUncontrolledOpen(newOpen);
+ }
+ onOpenChange?.(newOpen);
+ },
+ [controlledOpen, onOpenChange]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export interface DialogTriggerProps {
+ asChild?: boolean;
+ children: React.ReactElement;
+}
+
+const DialogTrigger: React.FC = ({ children }) => {
+ const context = React.useContext(DialogContext);
+ if (!context) throw new Error('DialogTrigger must be used within Dialog');
+
+ return React.cloneElement(children, {
+ onClick: (e: React.MouseEvent) => {
+ children.props.onClick?.(e);
+ context.onOpenChange(true);
+ },
+ });
+};
+
+export interface DialogContentProps extends React.HTMLAttributes {
+ showClose?: boolean;
+}
+
+const DialogContent = React.forwardRef(
+ ({ className, children, showClose = true, ...props }, ref) => {
+ const context = React.useContext(DialogContext);
+ if (!context) throw new Error('DialogContent must be used within Dialog');
+
+ React.useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && context.open) {
+ context.onOpenChange(false);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [context]);
+
+ if (!context.open) return null;
+
+ return (
+
+ {/* Backdrop */}
+
context.onOpenChange(false)}
+ />
+ {/* Modal Panel */}
+
+ {children}
+ {showClose && (
+
+ )}
+
+
+ );
+ }
+);
+DialogContent.displayName = 'DialogContent';
+
+const DialogHeader: React.FC
> = ({
+ className,
+ ...props
+}) => (
+
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogTitle = React.forwardRef<
+ HTMLHeadingElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = 'DialogTitle';
+
+const DialogDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = 'DialogDescription';
+
+const DialogFooter: React.FC> = ({
+ className,
+ ...props
+}) => (
+
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogClose: React.FC<{ children: React.ReactElement }> = ({ children }) => {
+ const context = React.useContext(DialogContext);
+ if (!context) throw new Error('DialogClose must be used within Dialog');
+
+ return React.cloneElement(children, {
+ onClick: (e: React.MouseEvent) => {
+ children.props.onClick?.(e);
+ context.onOpenChange(false);
+ },
+ });
+};
+
+export {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+ DialogClose,
+};
diff --git a/src/components/ui/dropdown.tsx b/src/components/ui/dropdown.tsx
new file mode 100644
index 0000000..4d9f81d
--- /dev/null
+++ b/src/components/ui/dropdown.tsx
@@ -0,0 +1,159 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+interface DropdownContextValue {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}
+
+const DropdownContext = React.createContext(null);
+
+export interface DropdownProps {
+ children: React.ReactNode;
+}
+
+const Dropdown: React.FC = ({ children }) => {
+ const [open, setOpen] = React.useState(false);
+ const dropdownRef = React.useRef(null);
+
+ React.useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+
+ if (open) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [open]);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export interface DropdownTriggerProps {
+ children: React.ReactElement;
+}
+
+const DropdownTrigger: React.FC = ({ children }) => {
+ const context = React.useContext(DropdownContext);
+ if (!context) throw new Error('DropdownTrigger must be used within Dropdown');
+
+ return React.cloneElement(children, {
+ onClick: (e: React.MouseEvent) => {
+ children.props.onClick?.(e);
+ context.setOpen(!context.open);
+ },
+ 'aria-expanded': context.open,
+ });
+};
+
+export interface DropdownContentProps extends React.HTMLAttributes {
+ align?: 'left' | 'right' | 'center';
+}
+
+const DropdownContent = React.forwardRef(
+ ({ className, align = 'left', children, ...props }, ref) => {
+ const context = React.useContext(DropdownContext);
+ if (!context) throw new Error('DropdownContent must be used within Dropdown');
+
+ if (!context.open) return null;
+
+ const alignClasses = {
+ left: 'left-0 origin-top-left',
+ right: 'right-0 origin-top-right',
+ center: 'left-1/2 -translate-x-1/2 origin-top',
+ };
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+DropdownContent.displayName = 'DropdownContent';
+
+export interface DropdownItemProps extends React.HTMLAttributes {
+ disabled?: boolean;
+ destructive?: boolean;
+ icon?: React.ReactNode;
+}
+
+const DropdownItem = React.forwardRef(
+ ({ className, disabled, destructive, icon, children, onClick, ...props }, ref) => {
+ const context = React.useContext(DropdownContext);
+
+ const handleClick = (e: React.MouseEvent) => {
+ if (disabled) return;
+ onClick?.(e);
+ context?.setOpen(false);
+ };
+
+ return (
+
+ {icon && {icon}}
+ {children}
+
+ );
+ }
+);
+DropdownItem.displayName = 'DropdownItem';
+
+const DropdownLabel: React.FC> = ({
+ className,
+ ...props
+}) => (
+
+);
+DropdownLabel.displayName = 'DropdownLabel';
+
+const DropdownSeparator: React.FC> = ({
+ className,
+ ...props
+}) => (
+
+);
+DropdownSeparator.displayName = 'DropdownSeparator';
+
+export {
+ Dropdown,
+ DropdownTrigger,
+ DropdownContent,
+ DropdownItem,
+ DropdownLabel,
+ DropdownSeparator,
+};
diff --git a/src/components/ui/icon-button.tsx b/src/components/ui/icon-button.tsx
new file mode 100644
index 0000000..8e4ace6
--- /dev/null
+++ b/src/components/ui/icon-button.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface IconButtonProps extends React.ButtonHTMLAttributes {
+ 'aria-label': string;
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
+ size?: 'sm' | 'md' | 'lg';
+ isLoading?: boolean;
+ asChild?: boolean;
+}
+
+const IconButton = React.forwardRef(
+ (
+ {
+ className,
+ variant = 'ghost',
+ size = 'md',
+ isLoading = false,
+ disabled,
+ children,
+ asChild = false,
+ 'aria-label': ariaLabel,
+ type = 'button',
+ ...props
+ },
+ ref
+ ) => {
+ const baseStyles =
+ 'inline-flex items-center justify-center rounded-lg transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 shrink-0';
+
+ const variants = {
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground text-foreground shadow-subtle',
+ ghost: 'hover:bg-accent hover:text-accent-foreground text-muted-foreground hover:text-foreground',
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ };
+
+ const sizes = {
+ sm: 'h-8 w-8 text-xs',
+ md: 'h-10 w-10 text-sm',
+ lg: 'h-12 w-12 text-base',
+ };
+
+ const combinedClassName = cn(baseStyles, variants[variant], sizes[size], className);
+
+ if (asChild && React.isValidElement(children)) {
+ return React.cloneElement(children as React.ReactElement<{ className?: string; 'aria-label'?: string }>, {
+ className: cn(combinedClassName, (children.props as { className?: string }).className),
+ 'aria-label': ariaLabel,
+ });
+ }
+
+ return (
+
+ );
+ }
+);
+
+IconButton.displayName = 'IconButton';
+
+export { IconButton };
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
new file mode 100644
index 0000000..c3c9873
--- /dev/null
+++ b/src/components/ui/index.ts
@@ -0,0 +1,18 @@
+export * from './button';
+export * from './icon-button';
+export * from './input';
+export * from './textarea';
+export * from './badge';
+export * from './card';
+export * from './avatar';
+export * from './separator';
+export * from './tabs';
+export * from './accordion';
+export * from './tooltip';
+export * from './dialog';
+export * from './dropdown';
+export * from './popover';
+export * from './toast';
+export * from './alert';
+export * from './progress';
+export * from './skeleton';
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..70d9e0f
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,95 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ helperText?: string;
+ error?: string;
+ leftIcon?: React.ReactNode;
+ rightIcon?: React.ReactNode;
+ containerClassName?: string;
+}
+
+const Input = React.forwardRef(
+ (
+ {
+ className,
+ type = 'text',
+ label,
+ helperText,
+ error,
+ leftIcon,
+ rightIcon,
+ containerClassName,
+ id,
+ disabled,
+ required,
+ ...props
+ },
+ ref
+ ) => {
+ const generatedId = React.useId();
+ const inputId = id || generatedId;
+ const helperId = `${inputId}-helper`;
+ const errorId = `${inputId}-error`;
+
+ return (
+
+ {label && (
+
+ )}
+
+ {leftIcon && (
+
+ {leftIcon}
+
+ )}
+
+ {rightIcon && (
+
+ {rightIcon}
+
+ )}
+
+ {error ? (
+
+ {error}
+
+ ) : helperText ? (
+
+ {helperText}
+
+ ) : null}
+
+ );
+ }
+);
+
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
new file mode 100644
index 0000000..31f4951
--- /dev/null
+++ b/src/components/ui/popover.tsx
@@ -0,0 +1,94 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+interface PopoverContextValue {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}
+
+const PopoverContext = React.createContext(null);
+
+export interface PopoverProps {
+ children: React.ReactNode;
+}
+
+const Popover: React.FC = ({ children }) => {
+ const [open, setOpen] = React.useState(false);
+ const popoverRef = React.useRef(null);
+
+ React.useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+
+ if (open) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [open]);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export interface PopoverTriggerProps {
+ children: React.ReactElement;
+}
+
+const PopoverTrigger: React.FC = ({ children }) => {
+ const context = React.useContext(PopoverContext);
+ if (!context) throw new Error('PopoverTrigger must be used within Popover');
+
+ return React.cloneElement(children, {
+ onClick: (e: React.MouseEvent) => {
+ children.props.onClick?.(e);
+ context.setOpen(!context.open);
+ },
+ 'aria-expanded': context.open,
+ });
+};
+
+export interface PopoverContentProps extends React.HTMLAttributes {
+ align?: 'left' | 'right' | 'center';
+}
+
+const PopoverContent = React.forwardRef(
+ ({ className, align = 'center', children, ...props }, ref) => {
+ const context = React.useContext(PopoverContext);
+ if (!context) throw new Error('PopoverContent must be used within Popover');
+
+ if (!context.open) return null;
+
+ const alignClasses = {
+ left: 'left-0 origin-top-left',
+ right: 'right-0 origin-top-right',
+ center: 'left-1/2 -translate-x-1/2 origin-top',
+ };
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+PopoverContent.displayName = 'PopoverContent';
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
new file mode 100644
index 0000000..f6b3abe
--- /dev/null
+++ b/src/components/ui/progress.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface ProgressProps extends React.HTMLAttributes {
+ value?: number;
+ max?: number;
+ size?: 'sm' | 'md' | 'lg';
+ showLabel?: boolean;
+ indicatorClassName?: string;
+}
+
+const heightMap = {
+ sm: 'h-1.5',
+ md: 'h-2.5',
+ lg: 'h-4',
+};
+
+const Progress = React.forwardRef(
+ (
+ {
+ className,
+ value = 0,
+ max = 100,
+ size = 'md',
+ showLabel = false,
+ indicatorClassName,
+ ...props
+ },
+ ref
+ ) => {
+ const percentage = Math.min(Math.max(0, (value / max) * 100), 100);
+
+ return (
+
+ {showLabel && (
+
+ Progress
+ {Math.round(percentage)}%
+
+ )}
+
+
+ );
+ }
+);
+
+Progress.displayName = 'Progress';
+
+export { Progress };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..43b69b0
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface SeparatorProps extends React.HTMLAttributes {
+ orientation?: 'horizontal' | 'vertical';
+ label?: React.ReactNode;
+}
+
+const Separator = React.forwardRef(
+ (
+ {
+ className,
+ orientation = 'horizontal',
+ label,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isHorizontal = orientation === 'horizontal';
+ const displayLabel = label || children;
+
+ if (displayLabel && isHorizontal) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+);
+
+Separator.displayName = 'Separator';
+
+export { Separator };
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..ff86684
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface SkeletonProps extends React.HTMLAttributes {
+ variant?: 'text' | 'circular' | 'rectangular';
+}
+
+function Skeleton({
+ className,
+ variant = 'rectangular',
+ ...props
+}: SkeletonProps) {
+ const variantClasses = {
+ text: 'h-4 w-full rounded',
+ circular: 'rounded-full',
+ rectangular: 'rounded-lg',
+ };
+
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..3965f14
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,139 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+interface TabsContextValue {
+ value: string;
+ onValueChange: (value: string) => void;
+}
+
+const TabsContext = React.createContext(null);
+
+export interface TabsProps extends React.HTMLAttributes {
+ defaultValue?: string;
+ value?: string;
+ onValueChange?: (value: string) => void;
+}
+
+const Tabs = React.forwardRef(
+ (
+ {
+ defaultValue,
+ value: controlledValue,
+ onValueChange,
+ children,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const [selectedTab, setSelectedTab] = React.useState(defaultValue || '');
+
+ const currentTab = controlledValue !== undefined ? controlledValue : selectedTab;
+
+ const handleValueChange = React.useCallback(
+ (val: string) => {
+ if (controlledValue === undefined) {
+ setSelectedTab(val);
+ }
+ onValueChange?.(val);
+ },
+ [controlledValue, onValueChange]
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+Tabs.displayName = 'Tabs';
+
+const TabsList = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = 'TabsList';
+
+export interface TabsTriggerProps
+ extends React.ButtonHTMLAttributes {
+ value: string;
+}
+
+const TabsTrigger = React.forwardRef(
+ ({ className, value, children, ...props }, ref) => {
+ const context = React.useContext(TabsContext);
+ if (!context) {
+ throw new Error('TabsTrigger must be used within a Tabs component');
+ }
+
+ const isActive = context.value === value;
+
+ return (
+
+ );
+ }
+);
+TabsTrigger.displayName = 'TabsTrigger';
+
+export interface TabsContentProps
+ extends React.HTMLAttributes {
+ value: string;
+}
+
+const TabsContent = React.forwardRef(
+ ({ className, value, children, ...props }, ref) => {
+ const context = React.useContext(TabsContext);
+ if (!context) {
+ throw new Error('TabsContent must be used within a Tabs component');
+ }
+
+ if (context.value !== value) return null;
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+TabsContent.displayName = 'TabsContent';
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..3eb224f
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {
+ label?: string;
+ helperText?: string;
+ error?: string;
+ containerClassName?: string;
+}
+
+const Textarea = React.forwardRef(
+ (
+ {
+ className,
+ label,
+ helperText,
+ error,
+ containerClassName,
+ id,
+ disabled,
+ required,
+ rows = 4,
+ ...props
+ },
+ ref
+ ) => {
+ const generatedId = React.useId();
+ const textareaId = id || generatedId;
+ const helperId = `${textareaId}-helper`;
+ const errorId = `${textareaId}-error`;
+
+ return (
+
+ {label && (
+
+ )}
+
+ {error ? (
+
+ {error}
+
+ ) : helperText ? (
+
+ {helperText}
+
+ ) : null}
+
+ );
+ }
+);
+
+Textarea.displayName = 'Textarea';
+
+export { Textarea };
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
new file mode 100644
index 0000000..7d77857
--- /dev/null
+++ b/src/components/ui/toast.tsx
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import { CheckCircle2, AlertCircle, AlertTriangle, Info, X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';
+
+export interface ToastMessage {
+ id: string;
+ title?: string;
+ description?: string;
+ variant?: ToastVariant;
+ duration?: number;
+ action?: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+interface ToastContextValue {
+ toast: (options: Omit) => void;
+ dismiss: (id: string) => void;
+}
+
+const ToastContext = React.createContext(null);
+
+export const useToast = () => {
+ const context = React.useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within a ToastProvider');
+ }
+ return context;
+};
+
+export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [toasts, setToasts] = React.useState([]);
+
+ const toast = React.useCallback((options: Omit) => {
+ const id = Math.random().toString(36).substring(2, 9);
+ const newToast: ToastMessage = { id, duration: 4000, variant: 'default', ...options };
+
+ setToasts((prev) => [...prev, newToast]);
+
+ if (newToast.duration && newToast.duration > 0) {
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, newToast.duration);
+ }
+ }, []);
+
+ const dismiss = React.useCallback((id: string) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, []);
+
+ return (
+
+ {children}
+ {/* Toast viewport */}
+
+ {toasts.map((t) => (
+ dismiss(t.id)} />
+ ))}
+
+
+ );
+};
+
+const iconMap = {
+ default: Info,
+ info: Info,
+ success: CheckCircle2,
+ error: AlertCircle,
+ warning: AlertTriangle,
+};
+
+const variantClasses: Record = {
+ default: 'border-border bg-background text-foreground',
+ info: 'border-sky-500/20 bg-sky-50 dark:bg-sky-950/40 text-sky-900 dark:text-sky-200',
+ success: 'border-emerald-500/20 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-900 dark:text-emerald-200',
+ error: 'border-rose-500/20 bg-rose-50 dark:bg-rose-950/40 text-rose-900 dark:text-rose-200',
+ warning: 'border-amber-500/20 bg-amber-50 dark:bg-amber-950/40 text-amber-900 dark:text-amber-200',
+};
+
+const iconClasses: Record = {
+ default: 'text-primary',
+ info: 'text-sky-500',
+ success: 'text-emerald-500',
+ error: 'text-rose-500',
+ warning: 'text-amber-500',
+};
+
+const ToastItem: React.FC<{ toast: ToastMessage; onDismiss: () => void }> = ({
+ toast,
+ onDismiss,
+}) => {
+ const variant = toast.variant || 'default';
+ const IconComponent = iconMap[variant];
+
+ return (
+
+
+
+ {toast.title &&
{toast.title}
}
+ {toast.description && (
+
{toast.description}
+ )}
+ {toast.action && (
+
+ )}
+
+
+
+ );
+};
+
+export { ToastItem as Toast };
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..7d2dd92
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,67 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface TooltipProps {
+ content: React.ReactNode;
+ position?: 'top' | 'bottom' | 'left' | 'right';
+ children: React.ReactElement;
+ className?: string;
+ delayMs?: number;
+}
+
+const Tooltip: React.FC = ({
+ content,
+ position = 'top',
+ children,
+ className,
+ delayMs = 200,
+}) => {
+ const [isVisible, setIsVisible] = React.useState(false);
+ const timeoutRef = React.useRef(null);
+
+ const showTooltip = () => {
+ timeoutRef.current = setTimeout(() => {
+ setIsVisible(true);
+ }, delayMs);
+ };
+
+ const hideTooltip = () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ setIsVisible(false);
+ };
+
+ const positionClasses = {
+ top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
+ bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
+ left: 'right-full top-1/2 -translate-y-1/2 mr-2',
+ right: 'left-full top-1/2 -translate-y-1/2 ml-2',
+ };
+
+ return (
+
+ {children}
+ {isVisible && content && (
+
+ {content}
+
+ )}
+
+ );
+};
+
+export { Tooltip };
diff --git a/src/lib/index.ts b/src/lib/index.ts
index e69de29..04bca77 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -0,0 +1 @@
+export * from './utils';
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..91f8df2
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,9 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+/**
+ * Combines class names using clsx and merges Tailwind CSS classes using tailwind-merge.
+ */
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/src/styles/globals.css b/src/styles/globals.css
new file mode 100644
index 0000000..b98c869
--- /dev/null
+++ b/src/styles/globals.css
@@ -0,0 +1,95 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 239 84% 60%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 239 84% 96%;
+ --accent-foreground: 239 84% 40%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --success: 142.1 76.2% 36.3%;
+ --success-foreground: 355.7 100% 97.3%;
+
+ --warning: 38 92% 50%;
+ --warning-foreground: 48 96% 12%;
+
+ --info: 199 89% 48%;
+ --info-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 239 84% 60%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 224 71% 4%;
+ --foreground: 210 40% 98%;
+
+ --card: 224 71% 6%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 224 71% 6%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 239 84% 67%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 239 84% 20%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --success: 142.1 70.6% 45.3%;
+ --success-foreground: 144 100% 97.3%;
+
+ --warning: 38 92% 50%;
+ --warning-foreground: 48 96% 12%;
+
+ --info: 199 89% 58%;
+ --info-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 239 84% 67%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground antialiased selection:bg-primary/20 selection:text-primary;
+ font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
+ }
+}
diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts
new file mode 100644
index 0000000..0e0139d
--- /dev/null
+++ b/src/theme/tokens.ts
@@ -0,0 +1,93 @@
+export const tokens = {
+ colors: {
+ brand: {
+ 50: '#f0f3ff',
+ 100: '#e0e7ff',
+ 200: '#c7d2fe',
+ 300: '#a5b4fc',
+ 400: '#818cf8',
+ 500: '#6366f1',
+ 600: '#4f46e5',
+ 700: '#4338ca',
+ 800: '#3730a3',
+ 900: '#312e81',
+ 950: '#1e1b4b',
+ },
+ neutral: {
+ 50: '#f8fafc',
+ 100: '#f1f5f9',
+ 200: '#e2e8f0',
+ 300: '#cbd5e1',
+ 400: '#94a3b8',
+ 500: '#64748b',
+ 600: '#475569',
+ 700: '#334155',
+ 800: '#1e293b',
+ 900: '#0f172a',
+ 950: '#020617',
+ },
+ semantic: {
+ success: '#10b981',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ info: '#06b6d4',
+ },
+ },
+ typography: {
+ fontFamily: {
+ sans: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+ mono: 'JetBrains Mono, Menlo, Monaco, Consolas, monospace',
+ },
+ fontSize: {
+ xs: ['0.75rem', { lineHeight: '1rem' }],
+ sm: ['0.875rem', { lineHeight: '1.25rem' }],
+ base: ['1rem', { lineHeight: '1.5rem' }],
+ lg: ['1.125rem', { lineHeight: '1.75rem' }],
+ xl: ['1.25rem', { lineHeight: '1.75rem' }],
+ '2xl': ['1.5rem', { lineHeight: '2rem' }],
+ '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
+ '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
+ '5xl': ['3rem', { lineHeight: '1' }],
+ },
+ fontWeight: {
+ normal: '400',
+ medium: '500',
+ semibold: '600',
+ bold: '700',
+ },
+ },
+ spacing: {
+ containerPadding: '2rem',
+ sectionSm: '3rem',
+ sectionMd: '5rem',
+ sectionLg: '8rem',
+ },
+ borderRadius: {
+ sm: '0.25rem',
+ md: '0.375rem',
+ lg: '0.5rem',
+ xl: '0.75rem',
+ '2xl': '1rem',
+ full: '9999px',
+ },
+ shadows: {
+ subtle: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+ card: '0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.04)',
+ float: '0 4px 16px -2px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04)',
+ dropdown: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05)',
+ },
+ breakpoints: {
+ sm: '640px',
+ md: '768px',
+ lg: '1024px',
+ xl: '1280px',
+ '2xl': '1536px',
+ },
+ transitions: {
+ fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
+ normal: '250ms cubic-bezier(0.4, 0, 0.2, 1)',
+ slow: '350ms cubic-bezier(0.4, 0, 0.2, 1)',
+ },
+} as const;
+
+export type DesignTokens = typeof tokens;
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..88a30a3
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,135 @@
+import type { Config } from 'tailwindcss';
+
+const config: Config = {
+ darkMode: ['class'],
+ content: [
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px',
+ },
+ },
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ 50: '#f0f3ff',
+ 100: '#e0e7ff',
+ 500: '#6366f1',
+ 600: '#4f46e5',
+ 700: '#4338ca',
+ 900: '#312e81',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ success: {
+ DEFAULT: 'hsl(var(--success))',
+ foreground: 'hsl(var(--success-foreground))',
+ },
+ warning: {
+ DEFAULT: 'hsl(var(--warning))',
+ foreground: 'hsl(var(--warning-foreground))',
+ },
+ info: {
+ DEFAULT: 'hsl(var(--info))',
+ foreground: 'hsl(var(--info-foreground))',
+ },
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ xl: 'calc(var(--radius) + 4px)',
+ '2xl': 'calc(var(--radius) + 8px)',
+ },
+ boxShadow: {
+ subtle: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+ card: '0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.04)',
+ float: '0 4px 16px -2px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04)',
+ dropdown: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05)',
+ dialog: '0 20px 35px -10px rgba(0, 0, 0, 0.2), 0 10px 15px -5px rgba(0, 0, 0, 0.08)',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: { height: '0' },
+ to: { height: 'var(--radix-accordion-content-height, auto)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height, auto)' },
+ to: { height: '0' },
+ },
+ 'fade-in': {
+ from: { opacity: '0' },
+ to: { opacity: '1' },
+ },
+ 'fade-out': {
+ from: { opacity: '1' },
+ to: { opacity: '0' },
+ },
+ 'scale-in': {
+ from: { opacity: '0', transform: 'scale(0.95)' },
+ to: { opacity: '1', transform: 'scale(1)' },
+ },
+ 'slide-in-right': {
+ from: { transform: 'translateX(100%)' },
+ to: { transform: 'translateX(0)' },
+ },
+ 'slide-out-right': {
+ from: { transform: 'translateX(0)' },
+ to: { transform: 'translateX(100%)' },
+ },
+ pulse: {
+ '0%, 100%': { opacity: '1' },
+ '50%': { opacity: '0.4' },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ 'fade-in': 'fade-in 0.2s ease-out',
+ 'fade-out': 'fade-out 0.2s ease-in',
+ 'scale-in': 'scale-in 0.2s ease-out',
+ 'slide-in-right': 'slide-in-right 0.3s ease-out',
+ 'slide-out-right': 'slide-out-right 0.3s ease-in',
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ },
+ },
+ },
+ plugins: [],
+};
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
index ccb2ed9..9cb2ceb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,9 @@
{
"compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
"lib": [
"dom",
"dom.iterable",