;
+
+// Define a type for onClick handlers that return dropdown items
+type DropdownClickHandler = () => DropdownItems;
+
+// Define a type for the children array we use in tests
+type MockChildrenArray = [MockIconComponent, MockIconComponent, string];
+
+type ButtonProps = BaseProps & {
+ onClick?: () => DropdownItems;
+ size?: "default" | "sm" | "lg";
+ children?: React.ReactNode | MockChildrenArray;
+};
+
+type IconProps = BaseProps & {
+ size?: number | string;
+};
+
+type DropdownMenuProps = BaseProps & {
+ children?: React.ReactNode;
+};
+
+// Define mock component return types
+type MockComponent = {
+ type: string;
+ props: P & {
+ "data-testid": string;
+ [key: string]: unknown;
+ };
+};
+
+// Mock the components we need
+const mockButton = (props: ButtonProps): MockComponent => ({
+ type: "button",
+ props: {
+ ...props,
+ "data-testid": "theme-toggle-button",
+ "aria-label": "Toggle theme",
+ },
+});
+
+const mockSunIcon = (props: IconProps): MockIconComponent => ({
+ type: "svg",
+ props: {
+ ...props,
+ "data-testid": "sun-icon",
+ },
+});
+
+const mockMoonIcon = (props: IconProps): MockIconComponent => ({
+ type: "svg",
+ props: {
+ ...props,
+ "data-testid": "moon-icon",
+ },
+});
+
+// Prefix with _ to indicate it's intentionally unused (to satisfy ESLint)
+const _mockDropdownMenu = (
+ props: DropdownMenuProps
+): MockComponent => ({
+ type: "div",
+ props: {
+ ...props,
+ "data-testid": "dropdown-menu",
+ },
+});
+
+const mockDropdownMenuItem = (props: ButtonProps): DropdownItemType => ({
+ type: "button",
+ props: {
+ ...props,
+ "data-testid": `dropdown-menu-item-${String(props.children)}`,
+ onClick: () => {
+ /* Empty function to satisfy type */
+ },
+ children: String(props.children || ""),
+ },
+});
+
+// Define props for our ThemeModeToggle component
+type ThemeModeToggleProps = {
+ className?: string;
+ iconSize?: number;
+ onClick?: () => void;
+};
+
+// Mock the ThemeModeToggle component
+// This is a simplified version that mimics the behavior of the real component
+const mockThemeModeToggle = (
+ props: ThemeModeToggleProps
+): MockComponent => {
+ const { className = "", iconSize = 16, onClick } = props;
+
+ // Get theme from useTheme hook
+ const { setTheme } = mockUseTheme();
+
+ // Create the component structure
+ const toggleButton = mockButton({
+ className,
+ onClick: (() => {
+ // Toggle dropdown visibility (in real component)
+ // For test, we'll simulate this by returning dropdown items
+ const lightItem = mockDropdownMenuItem({
+ children: "Light",
+ });
+ // Override the onClick handler after creation to avoid type issues
+ lightItem.props.onClick = () => {
+ setTheme("light");
+ if (onClick) onClick();
+ };
+
+ const darkItem = mockDropdownMenuItem({
+ children: "Dark",
+ });
+ darkItem.props.onClick = () => {
+ setTheme("dark");
+ if (onClick) onClick();
+ };
+
+ const systemItem = mockDropdownMenuItem({
+ children: "System",
+ });
+ systemItem.props.onClick = () => {
+ setTheme("system");
+ if (onClick) onClick();
+ };
+
+ return [lightItem, darkItem, systemItem];
+ }) as DropdownClickHandler,
+ children: [
+ mockSunIcon({ size: iconSize }),
+ mockMoonIcon({ size: iconSize }),
+ "Toggle theme",
+ ] as MockChildrenArray,
+ });
+
+ return toggleButton;
+};
+
+describe("ThemeModeToggle", () => {
+ beforeEach(() => {
+ // Reset mocks
+ mockSetTheme.mockReset();
+ mockUseTheme.mockReset();
+
+ // Setup default mock implementations
+ mockUseTheme.mockImplementation(() => ({
+ theme: "light",
+ setTheme: mockSetTheme,
+ }));
+ });
+
+ it("renders with correct accessibility label", () => {
+ // Create the component
+ const component = mockThemeModeToggle({});
+
+ // Check that it has the correct aria-label
+ expect(component.props["aria-label"]).toBe("Toggle theme");
+ });
+
+ it("applies custom className when provided", () => {
+ // Create the component with custom class
+ const customClass = "custom-class";
+ const component = mockThemeModeToggle({ className: customClass });
+
+ // Check that it has the custom class
+ expect(component.props.className).toBe(customClass);
+ });
+
+ it("uses custom icon size when provided", () => {
+ // Create the component with custom icon size
+ const iconSize = 24;
+ const component = mockThemeModeToggle({ iconSize });
+
+ // Check that icons have the custom size
+ // Cast children to MockChildrenArray to access specific indices safely
+ const children = component.props.children as MockChildrenArray;
+ const sunIcon = children[0];
+ const moonIcon = children[1];
+
+ expect(sunIcon.props.size).toBe(iconSize);
+ expect(moonIcon.props.size).toBe(iconSize);
+ });
+
+ it("shows dropdown menu with theme options when clicked", () => {
+ // Create the component
+ const component = mockThemeModeToggle({});
+
+ // Ensure onClick exists before calling it
+ if (!component.props.onClick) {
+ throw new Error("onClick handler is not defined");
+ }
+
+ // Simulate click
+ const dropdownItems = component.props.onClick();
+
+ // Check that dropdown items are returned
+ expect(dropdownItems.length).toBe(3);
+ expect(dropdownItems[0].props.children).toBe("Light");
+ expect(dropdownItems[1].props.children).toBe("Dark");
+ expect(dropdownItems[2].props.children).toBe("System");
+ });
+
+ it("calls setTheme when a theme option is clicked", () => {
+ // Create the component
+ const component = mockThemeModeToggle({});
+
+ // Ensure onClick exists before calling it
+ if (!component.props.onClick) {
+ throw new Error("onClick handler is not defined");
+ }
+
+ // Simulate click to open dropdown
+ const dropdownItems = component.props.onClick();
+
+ // Simulate click on Dark theme option
+ dropdownItems[1].props.onClick();
+
+ // Check that setTheme was called with "dark"
+ expect(mockSetTheme).toHaveBeenCalledWith("dark");
+ });
+
+ it("calls onClick handler when theme is changed", () => {
+ // Create mock onClick handler
+ const onClickMock = vi.fn(() => {});
+
+ // Create the component with onClick handler
+ const component = mockThemeModeToggle({ onClick: onClickMock });
+
+ // Ensure onClick exists before calling it
+ if (!component.props.onClick) {
+ throw new Error("onClick handler is not defined");
+ }
+
+ // Simulate click to open dropdown
+ const dropdownItems = component.props.onClick();
+
+ // Simulate click on Dark theme option
+ dropdownItems[1].props.onClick();
+
+ // Check that onClick was called
+ expect(onClickMock).toHaveBeenCalled();
+ });
+});
diff --git a/v2/src/components/theme/__tests__/theme-provider.test.tsx b/v2/src/components/theme/__tests__/theme-provider.test.tsx
new file mode 100644
index 0000000..b6ecc73
--- /dev/null
+++ b/v2/src/components/theme/__tests__/theme-provider.test.tsx
@@ -0,0 +1,214 @@
+import type { Theme } from "@/components/theme/theme-provider";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import * as React from "react";
+
+// Create mocks for the DOM APIs we need
+const mockClassList = {
+ add: vi.fn((_className: string) => {}),
+ remove: vi.fn((_classNames: string[]) => {}),
+};
+
+const mockLocalStorage = {
+ getItem: vi.fn((_key: string) => null as null | string),
+ setItem: vi.fn((_key: string, _value: string) => {}),
+};
+
+const mockMatchMedia = vi.fn((_query: string) => ({
+ matches: false,
+}));
+
+// Define the type for context provider props
+type ContextProviderProps = {
+ value: {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+ };
+ children: React.ReactNode;
+ // Allow specific additional props that might be passed
+ attribute?: string;
+ defaultTheme?: Theme;
+ enableSystem?: boolean;
+ storageKey?: string;
+ // Index signature for other potential props with string values
+ [key: string]: string | unknown;
+};
+
+// Create a mock for the ThemeProviderContext
+const mockContextProvider = vi.fn((props: ContextProviderProps) => ({
+ type: "ContextProvider",
+ props,
+}));
+
+describe("ThemeProvider", () => {
+ // Create a simplified version of the ThemeProvider for testing
+ function createThemeProvider() {
+ // This simulates what the real ThemeProvider does
+ return {
+ initializeTheme: (
+ defaultTheme: Theme = "system",
+ storageKey: string = "portfolio-theme"
+ ) => {
+ // Get theme from localStorage or use default
+ const storedTheme = mockLocalStorage.getItem(storageKey);
+ const theme = (storedTheme as Theme | null) || defaultTheme;
+
+ // Update DOM classes
+ mockClassList.remove(["light", "dark"]);
+
+ if (theme === "system") {
+ const systemTheme = mockMatchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+ mockClassList.add(systemTheme);
+ } else {
+ mockClassList.add(theme);
+ }
+
+ return theme;
+ },
+
+ createContextValue: (theme: Theme) => {
+ return {
+ theme,
+ setTheme: (newTheme: Theme) => {
+ mockLocalStorage.setItem("portfolio-theme", newTheme);
+ },
+ };
+ },
+
+ render: (props: {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+ }) => {
+ const {
+ children,
+ defaultTheme = "system",
+ storageKey = "portfolio-theme",
+ } = props;
+
+ // Initialize theme
+ const storedTheme = mockLocalStorage.getItem(storageKey);
+ const theme = (storedTheme as Theme | null) || defaultTheme;
+
+ // Create context value
+ const contextValue = {
+ theme,
+ setTheme: (newTheme: Theme) => {
+ mockLocalStorage.setItem(storageKey, newTheme);
+ },
+ };
+
+ // Return mock context provider
+ return mockContextProvider({
+ value: contextValue,
+ children,
+ });
+ },
+ };
+ }
+
+ beforeEach(() => {
+ // Reset all mocks
+ mockClassList.add.mockReset();
+ mockClassList.remove.mockReset();
+ mockLocalStorage.getItem.mockReset();
+ mockLocalStorage.setItem.mockReset();
+ mockMatchMedia.mockReset();
+ mockContextProvider.mockReset();
+
+ // Setup default mock implementations
+ mockMatchMedia.mockImplementation(() => ({ matches: false }));
+ });
+
+ it("initializes with default theme when localStorage is empty", () => {
+ // Mock localStorage.getItem to return null
+ mockLocalStorage.getItem.mockImplementation(() => null);
+
+ // Create provider and initialize theme
+ const provider = createThemeProvider();
+ const theme = provider.initializeTheme();
+
+ // Check that classList.remove was called
+ expect(mockClassList.remove).toHaveBeenCalledWith(["light", "dark"]);
+
+ // Default should be "system", which resolves to "light" with our mock
+ expect(mockClassList.add).toHaveBeenCalledWith("light");
+
+ // Theme should be "system"
+ expect(theme).toBe("system");
+ });
+
+ it("uses theme from localStorage when available", () => {
+ // Mock localStorage.getItem to return a theme
+ mockLocalStorage.getItem.mockImplementation(() => "dark");
+
+ // Create provider and initialize theme
+ const provider = createThemeProvider();
+ const theme = provider.initializeTheme();
+
+ // Check that classList.add was called with "dark"
+ expect(mockClassList.add).toHaveBeenCalledWith("dark");
+
+ // Theme should be "dark"
+ expect(theme).toBe("dark");
+ });
+
+ it("adds appropriate class to root element based on theme", () => {
+ // Mock localStorage to return "light" theme
+ mockLocalStorage.getItem.mockImplementation(() => "light");
+
+ // Create provider and initialize theme
+ const provider = createThemeProvider();
+ provider.initializeTheme();
+
+ // Check that classList.add was called with "light"
+ expect(mockClassList.add).toHaveBeenCalledWith("light");
+ });
+
+ it("uses system preference when theme is system", () => {
+ // Mock localStorage to return "system" theme
+ mockLocalStorage.getItem.mockImplementation(() => "system");
+
+ // Mock matchMedia to return dark mode preference
+ mockMatchMedia.mockImplementation(() => ({ matches: true }));
+
+ // Create provider and initialize theme
+ const provider = createThemeProvider();
+ provider.initializeTheme();
+
+ // Check that classList.add was called with "dark"
+ expect(mockClassList.add).toHaveBeenCalledWith("dark");
+ });
+
+ it("stores theme in localStorage when setTheme is called", () => {
+ // Create provider and context value
+ const provider = createThemeProvider();
+ const contextValue = provider.createContextValue("light");
+
+ // Call setTheme
+ contextValue.setTheme("dark");
+
+ // Check that localStorage.setItem was called
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
+ "portfolio-theme",
+ "dark"
+ );
+ });
+
+ it("renders with context provider", () => {
+ // Create provider and render
+ const provider = createThemeProvider();
+ provider.render({ children: Test
});
+
+ // Check that context provider was called
+ expect(mockContextProvider).toHaveBeenCalled();
+
+ // Check that context value was passed
+ const callArgs = mockContextProvider.mock.calls[0][0];
+ expect(callArgs).toBeDefined();
+ expect(callArgs.value).toBeDefined();
+ expect(typeof callArgs.value.setTheme).toBe("function");
+ });
+});
diff --git a/v2/src/components/theme/theme-mode-toggle.tsx b/v2/src/components/theme/theme-mode-toggle.tsx
new file mode 100644
index 0000000..58101ca
--- /dev/null
+++ b/v2/src/components/theme/theme-mode-toggle.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useTheme } from "@/hooks/use-theme";
+import { cn } from "@/lib/utils";
+import { Moon, Sun } from "lucide-react";
+
+interface ThemeModeToggleProps {
+ className?: string;
+ iconSize?: number;
+ onClick?: () => void;
+}
+
+function ThemeModeToggle({
+ className,
+ iconSize = 16,
+ onClick,
+}: ThemeModeToggleProps) {
+ const { setTheme } = useTheme();
+
+ const handleSetTheme = (theme: "light" | "dark" | "system") => {
+ setTheme(theme);
+ onClick?.();
+ };
+
+ return (
+
+
+
+
+
+
+ handleSetTheme("light")}>
+ Light
+
+ handleSetTheme("dark")}>
+ Dark
+
+ handleSetTheme("system")}>
+ System
+
+
+
+ );
+}
+
+export default ThemeModeToggle;
diff --git a/v2/src/components/theme/theme-provider.tsx b/v2/src/components/theme/theme-provider.tsx
new file mode 100644
index 0000000..cdb2b1c
--- /dev/null
+++ b/v2/src/components/theme/theme-provider.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { ThemeProviderContext } from "@/store";
+import { useEffect, useState } from "react";
+
+export type Theme = "dark" | "light" | "system";
+
+export type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "portfolio-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(() =>
+ typeof window !== "undefined"
+ ? (window.localStorage.getItem(storageKey) as Theme)
+ : defaultTheme
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ if (typeof window !== "undefined") {
+ window.localStorage.setItem(storageKey, theme);
+ }
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/v2/src/components/ui/accordion.tsx b/v2/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..4a8cca4
--- /dev/null
+++ b/v2/src/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDownIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/v2/src/components/ui/alert-dialog.tsx b/v2/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..0863e40
--- /dev/null
+++ b/v2/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/v2/src/components/ui/alert.tsx b/v2/src/components/ui/alert.tsx
new file mode 100644
index 0000000..1421354
--- /dev/null
+++ b/v2/src/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/v2/src/components/ui/animated-tabs.tsx b/v2/src/components/ui/animated-tabs.tsx
new file mode 100644
index 0000000..5fb63e7
--- /dev/null
+++ b/v2/src/components/ui/animated-tabs.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { motion } from "motion/react";
+import { useState } from "react";
+
+type Tab = {
+ title: string;
+ value: string;
+ content?: string | React.ReactNode;
+};
+
+interface AnimatedTabsProps {
+ tabs: Tab[];
+ containerClassName?: string;
+ activeTabClassName?: string;
+ tabClassName?: string;
+ contentClassName?: string;
+}
+
+export const AnimatedTabs = ({
+ tabs: propTabs,
+ containerClassName,
+ activeTabClassName,
+ tabClassName,
+ contentClassName,
+}: AnimatedTabsProps) => {
+ const [active, setActive] = useState(propTabs[0]);
+ const [tabs, setTabs] = useState(propTabs);
+
+ const moveSelectedTabToTop = (idx: number) => {
+ const newTabs = [...propTabs];
+ const selectedTab = newTabs.splice(idx, 1);
+ newTabs.unshift(selectedTab[0]);
+ setTabs(newTabs);
+ setActive(newTabs[0]);
+ };
+
+ const [hovering, setHovering] = useState(false);
+
+ return (
+ <>
+
+ {propTabs.map((tab, idx) => (
+
+ ))}
+
+
+
+ >
+ );
+};
+
+export const FadeInDiv = ({
+ className,
+ tabs,
+ hovering,
+}: {
+ className?: string;
+ key?: string;
+ tabs: Tab[];
+ hovering?: boolean;
+}) => {
+ const isActive = (tab: Tab) => {
+ return tab.value === tabs[0].value;
+ };
+
+ return (
+
+ {tabs.map((tab, idx) => (
+
+ {tab.content}
+
+ ))}
+
+ );
+};
diff --git a/v2/src/components/ui/aspect-ratio.tsx b/v2/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..3df3fd0
--- /dev/null
+++ b/v2/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { AspectRatio }
diff --git a/v2/src/components/ui/avatar.tsx b/v2/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..71e428b
--- /dev/null
+++ b/v2/src/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/v2/src/components/ui/background-beams-with-collision.tsx b/v2/src/components/ui/background-beams-with-collision.tsx
new file mode 100644
index 0000000..6985120
--- /dev/null
+++ b/v2/src/components/ui/background-beams-with-collision.tsx
@@ -0,0 +1,272 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { AnimatePresence, motion } from "motion/react";
+import React, { useEffect, useRef, useState } from "react";
+
+export const BackgroundBeamsWithCollision = ({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) => {
+ const containerRef = useRef(null);
+ const parentRef = useRef(null);
+
+ const beams = [
+ {
+ initialX: 10,
+ translateX: 10,
+ duration: 7,
+ repeatDelay: 3,
+ delay: 2,
+ },
+ {
+ initialX: 600,
+ translateX: 600,
+ duration: 3,
+ repeatDelay: 3,
+ delay: 4,
+ },
+ {
+ initialX: 100,
+ translateX: 100,
+ duration: 7,
+ repeatDelay: 7,
+ className: "h-6",
+ },
+ {
+ initialX: 400,
+ translateX: 400,
+ duration: 5,
+ repeatDelay: 14,
+ delay: 4,
+ },
+ {
+ initialX: 800,
+ translateX: 800,
+ duration: 11,
+ repeatDelay: 2,
+ className: "h-20",
+ },
+ {
+ initialX: 1000,
+ translateX: 1000,
+ duration: 4,
+ repeatDelay: 2,
+ className: "h-12",
+ },
+ {
+ initialX: 1200,
+ translateX: 1200,
+ duration: 6,
+ repeatDelay: 4,
+ delay: 2,
+ className: "h-6",
+ },
+ ];
+
+ return (
+
+ {beams.map((beam) => (
+
+ ))}
+
+ {children}
+
+
+ );
+};
+
+const CollisionMechanism = React.forwardRef<
+ HTMLDivElement,
+ {
+ containerRef: React.RefObject;
+ parentRef: React.RefObject;
+ beamOptions?: {
+ initialX?: number;
+ translateX?: number;
+ initialY?: number;
+ translateY?: number;
+ rotate?: number;
+ className?: string;
+ duration?: number;
+ delay?: number;
+ repeatDelay?: number;
+ };
+ }
+>(({ parentRef, containerRef, beamOptions = {} }, _ref) => {
+ const beamRef = useRef(null);
+ const [collision, setCollision] = useState<{
+ detected: boolean;
+ coordinates: { x: number; y: number } | null;
+ }>({
+ detected: false,
+ coordinates: null,
+ });
+ const [beamKey, setBeamKey] = useState(0);
+ const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);
+
+ useEffect(() => {
+ const checkCollision = () => {
+ if (
+ beamRef.current &&
+ containerRef.current &&
+ parentRef.current &&
+ !cycleCollisionDetected
+ ) {
+ const beamRect = beamRef.current.getBoundingClientRect();
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const parentRect = parentRef.current.getBoundingClientRect();
+
+ if (beamRect.bottom >= containerRect.top) {
+ const relativeX =
+ beamRect.left - parentRect.left + beamRect.width / 2;
+ const relativeY = beamRect.bottom - parentRect.top;
+
+ setCollision({
+ detected: true,
+ coordinates: {
+ x: relativeX,
+ y: relativeY,
+ },
+ });
+ setCycleCollisionDetected(true);
+ }
+ }
+ };
+
+ let animationFrameId: number;
+
+ const checkCollisionFrame = () => {
+ checkCollision();
+ if (!cycleCollisionDetected) {
+ animationFrameId = requestAnimationFrame(checkCollisionFrame);
+ }
+ };
+
+ animationFrameId = requestAnimationFrame(checkCollisionFrame);
+
+ return () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ };
+ }, [cycleCollisionDetected, containerRef, parentRef]);
+
+ useEffect(() => {
+ if (collision.detected && collision.coordinates) {
+ setTimeout(() => {
+ setCollision({ detected: false, coordinates: null });
+ setCycleCollisionDetected(false);
+ }, 2000);
+
+ setTimeout(() => {
+ setBeamKey((prevKey) => prevKey + 1);
+ }, 2000);
+ }
+ }, [collision]);
+
+ return (
+ <>
+
+
+ {collision.detected && collision.coordinates && (
+
+ )}
+
+ >
+ );
+});
+
+CollisionMechanism.displayName = "CollisionMechanism";
+
+const Explosion = ({ ...props }: React.HTMLProps) => {
+ const spans = Array.from({ length: 20 }, (_, index) => ({
+ id: index,
+ initialX: 0,
+ initialY: 0,
+ directionX: Math.floor(Math.random() * 80 - 40),
+ directionY: Math.floor(Math.random() * -50 - 10),
+ }));
+
+ return (
+
+
+ {spans.map((span) => (
+
+ ))}
+
+ );
+};
diff --git a/v2/src/components/ui/badge.tsx b/v2/src/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/v2/src/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/v2/src/components/ui/breadcrumb.tsx b/v2/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..eb88f32
--- /dev/null
+++ b/v2/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/v2/src/components/ui/button.tsx b/v2/src/components/ui/button.tsx
new file mode 100644
index 0000000..b942f22
--- /dev/null
+++ b/v2/src/components/ui/button.tsx
@@ -0,0 +1,59 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "../../lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/v2/src/components/ui/calendar.tsx b/v2/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..b8df044
--- /dev/null
+++ b/v2/src/components/ui/calendar.tsx
@@ -0,0 +1,75 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md"
+ ),
+ day: cn(
+ buttonVariants({ variant: "ghost" }),
+ "size-8 p-0 font-normal aria-selected:opacity-100"
+ ),
+ day_range_start:
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_range_end:
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ className, ...props }) => (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+
+export { Calendar }
diff --git a/v2/src/components/ui/card.tsx b/v2/src/components/ui/card.tsx
new file mode 100644
index 0000000..d05bbc6
--- /dev/null
+++ b/v2/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/v2/src/components/ui/carousel.tsx b/v2/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..0e05a77
--- /dev/null
+++ b/v2/src/components/ui/carousel.tsx
@@ -0,0 +1,241 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) return
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) return
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/v2/src/components/ui/chart.tsx b/v2/src/components/ui/chart.tsx
new file mode 100644
index 0000000..8587c98
--- /dev/null
+++ b/v2/src/components/ui/chart.tsx
@@ -0,0 +1,353 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+}) {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+