diff --git a/apps/web/app/[locale]/(landing)/components/footer.tsx b/apps/web/app/[locale]/(landing)/components/footer.tsx
new file mode 100644
index 0000000..248dcb1
--- /dev/null
+++ b/apps/web/app/[locale]/(landing)/components/footer.tsx
@@ -0,0 +1,163 @@
+"use client";
+
+import Link from "next/link";
+import type { ReactNode } from "react";
+import { cn } from "@workspace/ui/lib/utils";
+import { Logo } from "@workspace/ui/components/landing/logo";
+import {
+ GithubIcon,
+ MessageSquareIcon,
+ BugIcon,
+ GitPullRequestIcon,
+} from "lucide-react";
+import { ModeSwitch } from "@workspace/core/components/common/mode-switch";
+import { AnimatedContainer } from "@workspace/ui/components/landing/animated-container";
+import { TextHoverEffect } from "@workspace/ui/components/landing/text-hover-effect";
+import { BorderBeam } from "@workspace/ui/components/landing/border-beam";
+
+type FooterLink = {
+ title: string;
+ href: string;
+ isExternal: boolean;
+ icon?: ReactNode;
+};
+
+type FooterSection = {
+ label: string;
+ links: FooterLink[];
+};
+
+const footerLinks: FooterSection[] = [
+ {
+ label: "Product",
+ links: [
+ { title: "Pricing", href: "/", isExternal: false },
+ { title: "Features", href: "/", isExternal: false },
+ { title: "Download", href: "/download", isExternal: false },
+ { title: "Showcase", href: "/", isExternal: false },
+ ],
+ },
+ {
+ label: "Resources",
+ links: [
+ { title: "Documentation", href: "/docs", isExternal: false },
+ { title: "Quick Start", href: "/docs/quick-start", isExternal: false },
+ {
+ title: "Architecture",
+ href: "/docs/architecture/overview",
+ isExternal: false,
+ },
+ {
+ title: "Changelog",
+ href: "https://github.com/odest/tntstack/blob/master/CHANGELOG.md",
+ isExternal: true,
+ },
+ ],
+ },
+ {
+ label: "Legal",
+ links: [
+ {
+ title: "MIT License",
+ href: "https://github.com/odest/tntstack/blob/master/LICENSE",
+ isExternal: true,
+ },
+ { title: "Privacy Policy", href: "/", isExternal: false },
+ { title: "Terms of Service", href: "/", isExternal: false },
+ { title: "Security", href: "/", isExternal: false },
+ ],
+ },
+ {
+ label: "Community",
+ links: [
+ {
+ title: "GitHub",
+ href: "https://github.com/odest/tntstack",
+ isExternal: true,
+ icon: ,
+ },
+ {
+ title: "Contribute",
+ href: "https://github.com/odest/tntstack/blob/master/CONTRIBUTING.md",
+ isExternal: true,
+ icon: ,
+ },
+ {
+ title: "Discussions",
+ href: "https://github.com/odest/tntstack/discussions",
+ isExternal: true,
+ icon: ,
+ },
+ {
+ title: "Report an Issue",
+ href: "https://github.com/odest/tntstack/issues",
+ isExternal: true,
+ icon: ,
+ },
+ ],
+ },
+];
+
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/apps/web/app/[locale]/(landing)/components/hero-section.tsx b/apps/web/app/[locale]/(landing)/components/hero-section.tsx
index 3aa2e45..16b41dc 100644
--- a/apps/web/app/[locale]/(landing)/components/hero-section.tsx
+++ b/apps/web/app/[locale]/(landing)/components/hero-section.tsx
@@ -7,6 +7,7 @@ import { Button } from "@workspace/ui/components/button";
import { AnimatedGroup } from "@workspace/ui/components/landing/animated-group";
import { LogoCloud } from "@workspace/ui/components/landing/logo-cloud";
import { TextEffect } from "@workspace/ui/components/landing/text-effect";
+import { BorderBeam } from "@workspace/ui/components/landing/border-beam";
import { transitionVariants } from "@/lib/animations";
export default function HeroSection() {
@@ -110,6 +111,7 @@ export default function HeroSection() {
className="hidden size-full dark:block"
width="3276"
height="4095"
+ priority
/>
@@ -218,19 +220,24 @@ export default function HeroSection() {
diff --git a/apps/web/app/[locale]/(landing)/layout.tsx b/apps/web/app/[locale]/(landing)/layout.tsx
index 830aae0..656da8c 100644
--- a/apps/web/app/[locale]/(landing)/layout.tsx
+++ b/apps/web/app/[locale]/(landing)/layout.tsx
@@ -1,5 +1,6 @@
import { ThemeProvider } from "@workspace/core/providers/theme-provider";
import { HeroHeader } from "./components/header";
+import { Footer } from "./components/footer";
interface LandingLayoutProps {
children: React.ReactNode;
@@ -16,6 +17,7 @@ export default function LandingLayout({ children }: LandingLayoutProps) {
>
{children}
+
);
}
diff --git a/packages/core/src/components/common/mode-switch.tsx b/packages/core/src/components/common/mode-switch.tsx
new file mode 100644
index 0000000..82c3a54
--- /dev/null
+++ b/packages/core/src/components/common/mode-switch.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { LayoutGroup, motion } from "motion/react";
+import { Monitor, Sun, Moon } from "lucide-react";
+import { cn } from "@workspace/ui/lib/utils";
+import { useMounted } from "@workspace/core/hooks/use-mounted";
+import { useThemeTransition } from "@workspace/core/hooks/use-theme-transition";
+
+type Mode = "system" | "light" | "dark";
+
+const modes: { value: Mode; icon: typeof Monitor }[] = [
+ { value: "system", icon: Monitor },
+ { value: "light", icon: Sun },
+ { value: "dark", icon: Moon },
+];
+
+export function ModeSwitch({ className }: { className?: string }) {
+ const mounted = useMounted();
+ const { theme, handleThemeChange } = useThemeTransition();
+
+ const selected = (theme as Mode) ?? "system";
+
+ if (!mounted) {
+ return <>>;
+ }
+
+ return (
+
+
+ {modes.map((mode) => {
+ const Icon = mode.icon;
+ const isActive = selected === mode.value;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/packages/ui/src/components/landing/animated-container.tsx b/packages/ui/src/components/landing/animated-container.tsx
new file mode 100644
index 0000000..fe96ca1
--- /dev/null
+++ b/packages/ui/src/components/landing/animated-container.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { motion, useReducedMotion } from "motion/react";
+import type { ReactNode } from "react";
+
+export function AnimatedContainer({
+ className,
+ delay = 0.1,
+ children,
+}: {
+ delay?: number;
+ className?: string;
+ children: ReactNode;
+}) {
+ const shouldReduceMotion = useReducedMotion();
+
+ if (shouldReduceMotion) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/ui/src/components/landing/border-beam.tsx b/packages/ui/src/components/landing/border-beam.tsx
new file mode 100644
index 0000000..fbc0c28
--- /dev/null
+++ b/packages/ui/src/components/landing/border-beam.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import { motion, MotionStyle, Transition } from "motion/react";
+
+import { cn } from "@workspace/ui/lib/utils";
+
+interface BorderBeamProps {
+ /**
+ * The size of the border beam.
+ */
+ size?: number;
+ /**
+ * The duration of the border beam.
+ */
+ duration?: number;
+ /**
+ * The delay of the border beam.
+ */
+ delay?: number;
+ /**
+ * The color of the border beam from.
+ */
+ colorFrom?: string;
+ /**
+ * The color of the border beam to.
+ */
+ colorTo?: string;
+ /**
+ * The motion transition of the border beam.
+ */
+ transition?: Transition;
+ /**
+ * The class name of the border beam.
+ */
+ className?: string;
+ /**
+ * The style of the border beam.
+ */
+ style?: React.CSSProperties;
+ /**
+ * Whether to reverse the animation direction.
+ */
+ reverse?: boolean;
+ /**
+ * The initial offset position (0-100).
+ */
+ initialOffset?: number;
+ /**
+ * The border width of the beam.
+ */
+ borderWidth?: number;
+}
+
+export const BorderBeam = ({
+ className,
+ size = 50,
+ delay = 0,
+ duration = 6,
+ colorFrom = "#ffaa40",
+ colorTo = "#9c40ff",
+ transition,
+ style,
+ reverse = false,
+ initialOffset = 0,
+ borderWidth = 1,
+}: BorderBeamProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/ui/src/components/landing/text-hover-effect.tsx b/packages/ui/src/components/landing/text-hover-effect.tsx
new file mode 100644
index 0000000..d5b3fee
--- /dev/null
+++ b/packages/ui/src/components/landing/text-hover-effect.tsx
@@ -0,0 +1,134 @@
+"use client";
+import { useRef, useEffect, useState } from "react";
+import { motion } from "motion/react";
+
+export const TextHoverEffect = ({
+ text,
+ duration,
+}: {
+ text: string;
+ duration?: number;
+ automatic?: boolean;
+}) => {
+ const svgRef = useRef(null);
+ const [cursor, setCursor] = useState({ x: 0, y: 0 });
+ const [hovered, setHovered] = useState(false);
+ const [maskPosition, setMaskPosition] = useState({ cx: "50%", cy: "50%" });
+
+ useEffect(() => {
+ if (svgRef.current && cursor.x !== null && cursor.y !== null) {
+ const svgRect = svgRef.current.getBoundingClientRect();
+ const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
+ const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
+ setMaskPosition({
+ cx: `${cxPercentage}%`,
+ cy: `${cyPercentage}%`,
+ });
+ }
+ }, [cursor]);
+
+ return (
+
+ );
+};