Initial commit

This commit is contained in:
dk
2026-06-14 11:12:29 +00:00
commit 6f8f3a276d
315 changed files with 37702 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Plus } from "lucide-react";
import { cls } from "@/lib/utils";
interface AccordionProps {
items: { title: string; content: string }[];
className?: string;
}
const Accordion = ({ items, className = "" }: AccordionProps) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
return (
<div className={cls("flex flex-col gap-3 xl:gap-3.5 2xl:gap-4", className)}>
{items.map((item, index) => (
<div
key={index}
onClick={() => setActiveIndex(activeIndex === index ? null : index)}
className="p-3 xl:p-3.5 2xl:p-4 rounded card cursor-pointer select-none"
>
<div className="flex items-center justify-between gap-3 xl:gap-3.5 2xl:gap-4">
<h3 className="text-lg md:text-xl font-medium leading-snug">{item.title}</h3>
<div className="flex shrink-0 items-center justify-center size-8 md:size-9 rounded primary-button">
<Plus
className={cls(
"size-3.5 md:size-4 text-primary-cta-text transition-transform duration-300",
activeIndex === index && "rotate-45"
)}
strokeWidth={2}
/>
</div>
</div>
<AnimatePresence initial={false}>
{activeIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="overflow-hidden"
>
<p className="pt-1 text-base leading-snug">{item.content}</p>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
);
};
export default Accordion;

View File

@@ -0,0 +1,22 @@
import { cls } from "@/lib/utils";
interface ActiveBadgeProps {
text: string;
className?: string;
}
const ActiveBadge = ({ text, className }: ActiveBadgeProps) => {
return (
<div
className={cls(
"card backdrop-blur flex items-center gap-2 px-3 py-1 rounded",
className
)}
>
<span className="size-2 rounded-full bg-accent animate-pulsate" />
<p className="text-sm leading-snug font-medium text-foreground">{text}</p>
</div>
);
};
export default ActiveBadge;

View File

@@ -0,0 +1,46 @@
import { useState, useEffect } from "react";
import { cls } from "@/lib/utils";
const BARS = [
{ height: 100, hoverHeight: 40 },
{ height: 84, hoverHeight: 100 },
{ height: 62, hoverHeight: 75 },
{ height: 90, hoverHeight: 50 },
{ height: 70, hoverHeight: 90 },
{ height: 50, hoverHeight: 60 },
{ height: 75, hoverHeight: 85 },
{ height: 80, hoverHeight: 70 },
];
const AnimatedBarChart = () => {
const [active, setActive] = useState(2);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const interval = setInterval(() => setActive((p) => (p + 1) % BARS.length), 3000);
return () => clearInterval(interval);
}, []);
return (
<div
className="hidden md:block h-full w-full"
style={{ maskImage: "linear-gradient(to bottom, black 40%, transparent)" }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex items-end gap-4 h-full w-full">
{BARS.map((bar, i) => (
<div
key={i}
className="relative w-full rounded bg-background-accent transition-all duration-500"
style={{ height: `${isHovered ? bar.hoverHeight : bar.height}%` }}
>
<div className={cls("absolute inset-0 rounded primary-button transition-opacity duration-500", active === i ? "opacity-100" : "opacity-0")} />
</div>
))}
</div>
</div>
);
};
export default AnimatedBarChart;

View File

@@ -0,0 +1,31 @@
import { ArrowUpRight } from "lucide-react";
import { cls } from "@/lib/utils";
import { useButtonClick } from "@/hooks/useButtonClick";
interface ArrowButtonProps {
href?: string;
onClick?: () => void;
className?: string;
}
const ArrowButton = ({ href = "#", onClick, className }: ArrowButtonProps) => {
const handleClick = useButtonClick(href, onClick);
return (
<a
href={href}
onClick={handleClick}
className={cls(
"group/arrow flex items-center justify-center shrink-0 size-9 primary-button rounded-full cursor-pointer transition-transform duration-300 hover:scale-110",
className
)}
>
<ArrowUpRight
className="size-4 text-primary-cta-text transition-transform duration-300 group-hover/arrow:rotate-45"
strokeWidth={2}
/>
</a>
);
};
export default ArrowButton;

View File

@@ -0,0 +1,20 @@
import { cls } from "@/lib/utils";
type AuroraBackgroundProps = {
position: "fixed" | "absolute";
};
const AuroraBackground = ({ position }: AuroraBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 w-full h-full overflow-hidden bg-background pointer-events-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")}>
<div className="absolute top-0 left-1/2 -translate-y-1/2 -translate-x-[120%] w-[9vw] h-[110vh] bg-background-accent/15 -rotate-[52.5deg] rounded-[100%]" />
<div className="absolute top-[-20vh] right-[2.5vw] w-[12.5vw] h-screen bg-background-accent/15 -rotate-60 rounded-[100%]" />
<div className="absolute top-[-20vh] left-[2vw] w-[15vw] h-[150vh] bg-background-accent/20 -rotate-45 rounded-[100%]" />
<div className="absolute top-[-30vh] left-0 w-[10vw] h-[70vh] bg-background-accent/15 -rotate-45 rounded-[100%]" />
<div className="absolute bottom-[-40vh] left-0 w-[120vw] h-[50vh] bg-background-accent/10 -rotate-20 rounded-[100%]" />
<div className="absolute inset-0 backdrop-blur-3xl"></div>
</div>
);
};
export default AuroraBackground;

View File

@@ -0,0 +1,67 @@
import { useRef, useEffect, useState } from "react";
import { cls } from "@/lib/utils";
const AutoFillText = ({
children,
className = "",
paddingY = "py-10",
}: {
children: string;
className?: string;
paddingY?: string;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLHeadingElement>(null);
const [fontSize, setFontSize] = useState<number | null>(null);
const hasDescenders = /[gjpqy]/.test(children);
const lineHeight = hasDescenders ? 1.2 : 0.8;
useEffect(() => {
const container = containerRef.current;
const text = textRef.current;
if (!container || !text) return;
const calculateSize = () => {
const containerWidth = container.offsetWidth;
if (containerWidth === 0) return;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
const styles = getComputedStyle(text);
ctx.font = `${styles.fontWeight} 100px ${styles.fontFamily}`;
const textWidth = ctx.measureText(children).width;
if (textWidth > 0) {
setFontSize((containerWidth / textWidth) * 100);
}
};
calculateSize();
const observer = new ResizeObserver(calculateSize);
observer.observe(container);
return () => observer.disconnect();
}, [children]);
return (
<div ref={containerRef} className={cls("w-full min-w-0 flex-1", !hasDescenders && paddingY)}>
<h2
ref={textRef}
className={cls(
"whitespace-nowrap transition-opacity duration-150",
fontSize ? "opacity-100" : "opacity-0",
className
)}
style={{ fontSize: fontSize ? `${fontSize}px` : undefined, lineHeight }}
>
{children}
</h2>
</div>
);
};
export default AutoFillText;

View File

@@ -0,0 +1,26 @@
import { cls } from "@/lib/utils";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
interface AvatarAuthorProps {
imageSrc?: string;
videoSrc?: string;
name: string;
role: string;
className?: string;
}
const AvatarAuthor = ({ imageSrc, videoSrc, name, role, className }: AvatarAuthorProps) => (
<div className={cls("flex items-center gap-3", className)}>
<ImageOrVideo
imageSrc={imageSrc}
videoSrc={videoSrc}
className="size-10 md:size-11 2xl:size-12 rounded-full object-cover"
/>
<div className="flex flex-col min-w-0">
<span className="text-base text-foreground font-semibold leading-snug truncate">{name}</span>
<span className="text-base text-foreground/75 leading-snug truncate">{role}</span>
</div>
</div>
);
export default AvatarAuthor;

View File

@@ -0,0 +1,48 @@
import { cls } from "@/lib/utils";
interface AvatarGroupProps {
avatarsSrc: string[];
max?: number;
size?: "sm" | "md" | "lg";
label?: string;
labelClassName?: string;
className?: string;
}
const SIZES = {
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
};
const AvatarGroup = ({ avatarsSrc, max = 5, size = "md", label, labelClassName, className = "" }: AvatarGroupProps) => {
const visible = avatarsSrc.slice(0, max);
const remaining = avatarsSrc.length - visible.length;
return (
<div className={cls("flex items-center gap-3", className)}>
<div className="flex items-center">
{visible.map((src, index) => (
<div
key={index}
className={cls(
"relative shrink-0 overflow-hidden rounded-full border-2 border-background",
SIZES[size],
index > 0 && "-ml-3"
)}
>
<img src={src} alt="" className="h-full w-full object-cover" />
</div>
))}
{remaining > 0 && (
<div className={cls("flex items-center justify-center shrink-0 rounded-full border-2 border-background secondary-button font-medium text-secondary-cta-text -ml-3", SIZES[size])}>
+{remaining}
</div>
)}
</div>
{label && <span className={cls("text-base font-medium text-foreground", labelClassName)}>{label}</span>}
</div>
);
};
export default AvatarGroup;

View File

@@ -0,0 +1,81 @@
"use client";
import { useEffect, useRef } from "react";
import { cls } from "@/lib/utils";
import { animate } from "motion/react";
const spread = 40;
const proximity = 64;
const borderWidth = 1.5;
const BorderGlow = ({ className }: { className?: string }) => {
const ref = useRef<HTMLDivElement>(null);
const rafRef = useRef<number>(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onPointerMove = (e: PointerEvent) => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
const { left, top, width, height } = el.getBoundingClientRect();
const { clientX: x, clientY: y } = e;
const isActive =
x > left - proximity &&
x < left + width + proximity &&
y > top - proximity &&
y < top + height + proximity;
el.style.setProperty("--active", isActive ? "1" : "0");
if (!isActive) return;
const centerX = left + width / 2;
const centerY = top + height / 2;
const currentAngle = parseFloat(el.style.getPropertyValue("--start")) || 0;
const targetAngle = (Math.atan2(y - centerY, x - centerX) * 180) / Math.PI + 90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
animate(currentAngle, currentAngle + angleDiff, {
duration: 2,
ease: [0.16, 1, 0.3, 1],
onUpdate: (v) => el.style.setProperty("--start", String(v)),
});
});
};
document.body.addEventListener("pointermove", onPointerMove, { passive: true });
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
document.body.removeEventListener("pointermove", onPointerMove);
};
}, []);
const gradient = `radial-gradient(circle, var(--color-accent) 10%, transparent 20%),
radial-gradient(circle at 40% 40%, var(--color-background-accent) 5%, transparent 15%),
repeating-conic-gradient(from 236.84deg at 50% 50%, var(--color-accent) 0%, var(--color-background-accent) 5%, var(--color-accent) 10%)`;
return (
<div
ref={ref}
style={{ "--spread": spread, "--start": 0, "--active": 0, "--border-width": `${borderWidth}px`, "--gradient": gradient } as React.CSSProperties}
className={cls("pointer-events-none absolute inset-0 rounded-[inherit]", className)}
>
<div
className={cls(
"rounded-[inherit]",
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--border-width))]',
"after:[border:var(--border-width)_solid_transparent]",
"after:[background:var(--gradient)] after:bg-fixed",
"after:opacity-(--active) after:transition-opacity after:duration-300",
"after:[mask-clip:padding-box,border-box] after:mask-intersect",
"after:mask-[linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]"
)}
/>
</div>
);
};
export default BorderGlow;

View File

@@ -0,0 +1,87 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
import { useStyle } from "@/components/ui/useStyle";
import ButtonArrow from "@/components/ui/ButtonArrow";
import ButtonBounce from "@/components/ui/ButtonBounce";
import ButtonBubble from "@/components/ui/ButtonBubble";
import ButtonElastic from "@/components/ui/ButtonElastic";
import ButtonExpand from "@/components/ui/ButtonExpand";
import ButtonFlip from "@/components/ui/ButtonFlip";
import ButtonMagnetic from "@/components/ui/ButtonMagnetic";
import ButtonPill from "@/components/ui/ButtonPill";
import ButtonShift from "@/components/ui/ButtonShift";
import ButtonSlide from "@/components/ui/ButtonSlide";
import ButtonStagger from "@/components/ui/ButtonStagger";
interface ButtonProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const DefaultButton = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
{text}
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
const Button = (props: ButtonProps) => {
const { buttonVariant } = useStyle();
switch (buttonVariant) {
case "arrow":
return <ButtonArrow {...props} />;
case "bounce":
return <ButtonBounce {...props} />;
case "bubble":
return <ButtonBubble {...props} />;
case "elastic":
return <ButtonElastic {...props} />;
case "expand":
return <ButtonExpand {...props} />;
case "flip":
return <ButtonFlip {...props} />;
case "magnetic":
return <ButtonMagnetic {...props} />;
case "pill":
return <ButtonPill {...props} />;
case "shift":
return <ButtonShift {...props} />;
case "slide":
return <ButtonSlide {...props} />;
case "stagger":
return <ButtonStagger {...props} />;
default:
return <DefaultButton {...props} />;
}
};
export default Button;

View File

@@ -0,0 +1,50 @@
"use client";
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonArrowProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonArrow = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonArrowProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group flex items-center justify-between gap-2 h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className="truncate md:transition-transform md:duration-300 md:ease-out md:group-hover:translate-x-2">
{text}
</span>
<div className={cls("size-5 flex items-center justify-center rounded md:transition-all md:duration-300 md:ease-out md:group-hover:scale-[0.2] md:group-hover:rotate-90", variant === "primary" ? "secondary-button text-secondary-cta-text" : "primary-button text-primary-cta-text")}>
<ArrowRight className="size-3 md:transition-opacity md:duration-700 md:group-hover:opacity-0" />
</div>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonArrow;

View File

@@ -0,0 +1,54 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonBounceProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonBounce = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonBounceProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer transition-transform duration-300 ease-out md:hover:scale-90 md:hover:-rotate-3", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className="truncate overflow-hidden">
{[...text].map((char, index) => (
<span
key={index}
className="inline-block transition-transform duration-300 ease-out md:group-hover:-translate-y-[1.25em] md:group-hover:rotate-3"
style={{ textShadow: "0 1.25em currentColor", whiteSpace: char === " " ? "pre" : undefined }}
>
{char}
</span>
))}
</span>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonBounce;

View File

@@ -0,0 +1,53 @@
"use client";
import { motion } from "motion/react";
import { ArrowDownRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonBubbleProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonBubble = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonBubbleProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group relative flex items-center min-w-0 max-w-full text-sm rounded cursor-pointer", className)}
>
<div className={cls("flex items-center justify-center size-10 rounded relative scale-0 md:transition-transform md:duration-500 md:ease-out md:origin-left md:group-hover:scale-100", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text")}>
<ArrowDownRight className="size-3.5 md:transition-transform md:duration-500 md:group-hover:-rotate-45" />
</div>
<div className={cls("flex items-center justify-between flex-1 h-10 px-4 min-w-0 max-w-full rounded relative -translate-x-10 md:transition-transform md:duration-500 md:ease-out md:group-hover:translate-x-0", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text")}>
<span className="truncate">{text}</span>
</div>
<div className={cls("flex items-center justify-center size-10 rounded absolute right-0 z-20 scale-100 md:transition-transform md:duration-500 md:ease-out md:origin-right md:group-hover:scale-0", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text")}>
<ArrowDownRight className="size-3.5 md:transition-transform md:duration-500 md:group-hover:-rotate-45" />
</div>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonBubble;

View File

@@ -0,0 +1,56 @@
"use client";
import { motion, useSpring, useTransform } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonElasticProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonElastic = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonElasticProps) => {
const handleClick = useButtonClick(href, onClick);
const scale = useSpring(1, { stiffness: 300, damping: 10 });
const scaleX = useTransform(scale, [1, 1.08], [1, 1.08]);
const scaleY = useTransform(scale, [1, 1.08], [1, 0.95]);
const handleMouseEnter = () => {
if (window.innerWidth < 768) return;
scale.set(1.08);
setTimeout(() => scale.set(1), 100);
};
const button = (
<motion.a
href={href}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
style={{ scaleX, scaleY }}
className={cls("flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
{text}
</motion.a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonElastic;

View File

@@ -0,0 +1,51 @@
"use client";
import { motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonExpandProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonExpand = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonExpandProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group relative flex items-center gap-3 min-w-0 w-fit max-w-full py-0.5 pl-5 pr-0.5 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className={cls("relative z-10 flex-1 truncate md:transition-colors md:duration-500 md:ease-out", variant === "primary" ? "text-primary-cta-text md:group-hover:text-secondary-cta-text" : "text-secondary-cta-text md:group-hover:text-primary-cta-text")}>{text}</span>
<div className={cls("relative z-10 flex items-center justify-center size-8 rounded", variant === "primary" ? "text-secondary-cta-text" : "text-primary-cta-text")}>
<ArrowUpRight className="size-4" strokeWidth={1.5} />
</div>
<div className="absolute inset-0.5 z-0 overflow-hidden rounded pointer-events-none">
<div className={cls("h-full w-full rounded -translate-x-[calc(-100%+2rem)] md:transition-transform md:duration-500 md:ease-out md:group-hover:translate-x-0", variant === "primary" ? "secondary-button" : "primary-button")} />
</div>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonExpand;

View File

@@ -0,0 +1,132 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
import { useState } from "react";
interface ButtonFlipProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonFlip = ({ text, variant = "primary", href = "#", onClick, animate: shouldAnimate = true, animationDelay = 0, className = "" }: ButtonFlipProps) => {
const handleClick = useButtonClick(href, onClick);
const [isHovered, setIsHovered] = useState(false);
const maxIndex = Math.max(text.length - 1, 1);
const getCharValues = (index: number) => {
const t = index / maxIndex;
const signedIndex = index - maxIndex / 2;
const curve = Math.sin(t * 1.5 * (Math.PI / 180));
const rotCurve = Math.sin(t * 30 * (Math.PI / 180));
const rotSign = Math.max(-1, Math.min(1, signedIndex));
const delay = 0.05 + curve * 2.9;
const rotateZ = rotSign * rotCurve * 36 * -1;
const translateX = signedIndex * 0.125;
return { delay, rotateZ, translateX };
};
const button = (
<a
href={href}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cls("group flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer active:scale-[0.96]", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className="grid">
<span className="col-start-1 row-start-1 perspective-[10em] transform-3d">
{[...text].map((char, index) => {
const { delay, rotateZ, translateX } = getCharValues(index);
return (
<motion.span
key={index}
className="inline-block"
initial={false}
animate={isHovered ? {
x: `${translateX}em`,
y: "-1.25em",
rotateX: 72,
rotateZ,
scale: 0.65,
opacity: 0,
} : {
x: 0,
y: 0,
rotateX: 0,
rotateZ: 0,
scale: 1,
opacity: 1,
}}
transition={{
duration: 0.35,
delay: isHovered ? delay : 0,
ease: [0.675, 0.15, 0.1, 1],
}}
style={{ whiteSpace: char === " " ? "pre" : undefined }}
>
{char}
</motion.span>
);
})}
</span>
<span aria-hidden="true" className="col-start-1 row-start-1 perspective-[10em] transform-3d">
{[...text].map((char, index) => {
const { delay, rotateZ, translateX } = getCharValues(index);
return (
<motion.span
key={index}
className="inline-block"
initial={false}
animate={isHovered ? {
x: 0,
y: 0,
rotateX: 0,
rotateZ: 0,
scale: 1,
opacity: 1,
} : {
x: `${translateX}em`,
y: "1.25em",
rotateX: -72,
rotateZ,
scale: 0.65,
opacity: 0,
}}
transition={{
duration: 0.35,
delay: isHovered ? delay + 0.05 : 0,
ease: [0.675, 0.15, 0.1, 1],
}}
style={{ whiteSpace: char === " " ? "pre" : undefined }}
>
{char}
</motion.span>
);
})}
</span>
</span>
</a>
);
if (!shouldAnimate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonFlip;

View File

@@ -0,0 +1,69 @@
"use client";
import { useRef } from "react";
import { motion, useMotionValue, useSpring } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonMagneticProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonMagnetic = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonMagneticProps) => {
const handleClick = useButtonClick(href, onClick);
const ref = useRef<HTMLAnchorElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const springX = useSpring(x, { stiffness: 150, damping: 15 });
const springY = useSpring(y, { stiffness: 150, damping: 15 });
const handleMouseMove = (e: React.MouseEvent) => {
if (!ref.current || window.innerWidth < 768) return;
const rect = ref.current.getBoundingClientRect();
const offsetX = (e.clientX - rect.left - rect.width / 2) * 0.15;
const offsetY = (e.clientY - rect.top - rect.height / 2) * 0.15;
x.set(offsetX);
y.set(offsetY);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
const button = (
<motion.a
ref={ref}
href={href}
onClick={handleClick}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ x: springX, y: springY }}
className={cls("flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
{text}
</motion.a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonMagnetic;

View File

@@ -0,0 +1,48 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonPillProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonPill = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonPillProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls(
"flex items-center justify-center h-10 px-6 text-sm rounded-[0.5rem] cursor-pointer md:transition-[border-radius] md:duration-300 md:ease-out md:hover:rounded-[5rem]",
variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text",
className
)}
>
{text}
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonPill;

View File

@@ -0,0 +1,54 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonShiftProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonShift = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonShiftProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className="truncate overflow-hidden">
{[...text].map((char, index) => (
<span
key={index}
className="inline-block transition-transform duration-300 ease-out md:group-hover:-translate-y-[1.25em]"
style={{ textShadow: "0 1.25em currentColor", whiteSpace: char === " " ? "pre" : undefined }}
>
{char}
</span>
))}
</span>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonShift;

View File

@@ -0,0 +1,49 @@
"use client";
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonSlideProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonSlide = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonSlideProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group relative flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer overflow-clip", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<ArrowRight className="absolute left-5 size-3.5 opacity-0 -translate-x-6 md:transition-all md:duration-500 md:ease-[cubic-bezier(0.32,0.72,0,1)] md:group-hover:opacity-100 md:group-hover:translate-x-0 md:group-hover:delay-75" />
<span className="flex items-center gap-1.5 md:transition-transform md:duration-500 md:ease-[cubic-bezier(0.32,0.72,0,1)] md:group-hover:translate-x-4 md:group-hover:delay-75">
<span className="truncate">{text}</span>
<ArrowRight className="size-3.5 md:transition-all md:duration-500 md:ease-[cubic-bezier(0.32,0.72,0,1)] md:group-hover:opacity-0 md:group-hover:translate-x-6 md:group-hover:delay-50" />
</span>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonSlide;

View File

@@ -0,0 +1,54 @@
"use client";
import { motion } from "motion/react";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonStaggerProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
animationDelay?: number;
className?: string;
}
const ButtonStagger = ({ text, variant = "primary", href = "#", onClick, animate = true, animationDelay = 0, className = "" }: ButtonStaggerProps) => {
const handleClick = useButtonClick(href, onClick);
const button = (
<a
href={href}
onClick={handleClick}
className={cls("group flex items-center justify-center h-10 px-6 text-sm rounded cursor-pointer", variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text", className)}
>
<span className="truncate overflow-hidden">
{[...text].map((char, index) => (
<span
key={index}
className="inline-block transition-transform duration-400 ease-out md:group-hover:-translate-y-[1.25em]"
style={{ textShadow: "0 1.25em currentColor", transitionDelay: `${index * 0.01}s`, whiteSpace: char === " " ? "pre" : undefined }}
>
{char}
</span>
))}
</span>
</a>
);
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: animationDelay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default ButtonStagger;

View File

@@ -0,0 +1,40 @@
"use client";
import { DayPicker } from "react-day-picker";
import "react-day-picker/style.css";
import { cls } from "@/lib/utils";
interface CalendarProps {
selected?: Date;
onSelect?: (date: Date | undefined) => void;
className?: string;
}
const Calendar = ({ selected, onSelect, className = "" }: CalendarProps) => (
<DayPicker
mode="single"
selected={selected}
onSelect={onSelect}
showOutsideDays
className={cls("p-3 secondary-button rounded", className)}
classNames={{
month_caption: "flex items-center h-8 mb-2",
nav: "absolute top-0 right-0 flex items-center h-8",
caption_label: "relative z-[1] inline-flex items-center whitespace-nowrap border-0 text-base font-medium text-secondary-cta-text",
button_previous: "relative inline-flex items-center justify-center size-8 border-0 bg-transparent p-0 m-0 cursor-pointer text-secondary-cta-text hover:bg-foreground/10 rounded transition-colors duration-300",
button_next: "relative inline-flex items-center justify-center size-8 border-0 bg-transparent p-0 m-0 cursor-pointer text-secondary-cta-text hover:bg-foreground/10 rounded transition-colors duration-300",
chevron: "size-4 fill-secondary-cta-text",
weekdays: "flex",
weekday: "w-8 text-sm font-medium text-secondary-cta-text/50 text-center",
week: "flex mt-1",
day: "size-8 text-center p-0",
day_button: "size-8 rounded text-sm text-secondary-cta-text hover:bg-foreground/10 cursor-pointer transition-colors duration-300",
selected: "[&>button]:bg-foreground [&>button]:text-background [&>button]:hover:bg-foreground/90",
today: "[&>button]:font-bold",
outside: "[&>button]:opacity-30",
disabled: "[&>button]:opacity-30 [&>button]:cursor-not-allowed",
}}
/>
);
export default Calendar;

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from "react";
import { cls } from "@/lib/utils";
interface CardProps {
children: ReactNode;
className?: string;
}
const Card = ({ children, className = "" }: CardProps) => (
<div className={cls("p-6 xl:p-7 2xl:p-8 card rounded", className)}>
{children}
</div>
);
export default Card;

View File

@@ -0,0 +1,51 @@
"use client";
import { Children, type ReactNode } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
import { useCarouselControls } from "@/hooks/useCarouselControls";
interface CarouselProps {
children: ReactNode;
itemClassName?: string;
className?: string;
}
const Carousel = ({ children, itemClassName = "", className = "" }: CarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true, containScroll: "trimSnaps" });
const { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
return (
<div className={cls("flex flex-col gap-5 w-full", className)}>
<div ref={emblaRef} className="overflow-hidden w-full cursor-grab">
<div className="flex gap-4">
<div className="shrink-0 w-carousel-padding" />
{Children.map(children, (child, i) => (
<div key={i} className={cls("shrink-0", itemClassName)}>{child}</div>
))}
<div className="shrink-0 w-carousel-padding" />
</div>
</div>
<div className="flex w-full">
<div className="shrink-0 w-carousel-padding-controls" />
<div className="flex justify-between items-center w-full">
<div className="relative h-2 w-1/2 card rounded overflow-hidden">
<div className="absolute top-0 bottom-0 -left-full w-full primary-button rounded" style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }} />
</div>
<div className="flex items-center gap-3">
<button onClick={scrollPrev} disabled={prevDisabled} type="button" aria-label="Previous" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50 transition-opacity duration-300">
<ChevronLeft className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
<button onClick={scrollNext} disabled={nextDisabled} type="button" aria-label="Next" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50 transition-opacity duration-300">
<ChevronRight className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
</div>
</div>
<div className="shrink-0 w-carousel-padding-controls" />
</div>
</div>
);
};
export default Carousel;

View File

@@ -0,0 +1,46 @@
import type { LucideIcon } from "lucide-react";
import { Send } from "lucide-react";
import { cls } from "@/lib/utils";
import { resolveIcon } from "@/utils/resolve-icon";
type Exchange = { userMessage: string; aiResponse: string };
const ChatMarquee = ({ aiIcon, userIcon, exchanges, placeholder }: { aiIcon: string | LucideIcon; userIcon: string | LucideIcon; exchanges: Exchange[]; placeholder: string }) => {
const AiIcon = resolveIcon(aiIcon);
const UserIcon = resolveIcon(userIcon);
const messages = exchanges.flatMap((e) => [{ content: e.userMessage, isUser: true }, { content: e.aiResponse, isUser: false }]);
const duplicated = [...messages, ...messages];
return (
<div className="relative flex flex-col h-full w-full overflow-hidden">
<div className="flex-1 overflow-hidden mask-fade-y">
<div className="flex flex-col px-4 animate-marquee-vertical">
{duplicated.map((msg, i) => (
<div key={i} className={cls("flex items-end gap-2 mb-4 shrink-0", msg.isUser ? "flex-row-reverse" : "flex-row")}>
{msg.isUser ? (
<div className="flex items-center justify-center size-8 primary-button rounded shrink-0">
<UserIcon className="size-3 text-primary-cta-text" />
</div>
) : (
<div className="flex items-center justify-center size-8 card rounded shrink-0">
<AiIcon className="size-3" />
</div>
)}
<div className={cls("max-w-3/4 px-4 py-3 text-sm leading-snug", msg.isUser ? "primary-button rounded-2xl rounded-br-none text-primary-cta-text" : "card rounded-2xl rounded-bl-none")}>
{msg.content}
</div>
</div>
))}
</div>
</div>
<div className="flex items-center gap-2 p-2 pl-4 card rounded">
<p className="flex-1 text-sm text-foreground/75 truncate">{placeholder}</p>
<div className="flex items-center justify-center size-7 primary-button rounded">
<Send className="size-3 text-primary-cta-text" strokeWidth={1.75} />
</div>
</div>
</div>
);
};
export default ChatMarquee;

View File

@@ -0,0 +1,29 @@
import { Check } from "lucide-react";
import { cls } from "@/lib/utils";
interface CheckListProps {
items: string[];
size?: "sm" | "base" | "lg";
className?: string;
}
const textSizes = {
sm: "text-sm",
base: "text-base",
lg: "text-lg",
};
const CheckList = ({ items, size = "sm", className }: CheckListProps) => (
<div className={cls("flex flex-col gap-3", className)}>
{items.map((item) => (
<div key={item} className="flex items-center gap-3">
<div className="flex items-center justify-center shrink-0 size-6 primary-button rounded">
<Check className="size-3 text-primary-cta-text" strokeWidth={2} />
</div>
<span className={cls("leading-snug", textSizes[size])}>{item}</span>
</div>
))}
</div>
);
export default CheckList;

View File

@@ -0,0 +1,46 @@
"use client";
import { motion } from "motion/react";
import { Check } from "lucide-react";
import { cls } from "@/lib/utils";
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
label?: string;
size?: "sm" | "md" | "lg";
className?: string;
}
const SIZES = {
sm: { box: "size-4", icon: "size-2" },
md: { box: "size-5", icon: "size-2.5" },
lg: { box: "size-6", icon: "size-3" },
};
const Checkbox = ({ checked, onChange, label, size = "md", className = "" }: CheckboxProps) => (
<label className={cls("flex items-center gap-2 cursor-pointer", className)}>
<button
type="button"
role="checkbox"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={cls(
"flex items-center justify-center rounded border transition-colors duration-200 cursor-pointer",
SIZES[size].box,
checked ? "primary-button" : "secondary-button"
)}
>
<motion.div
initial={false}
animate={{ opacity: checked ? 1 : 0 }}
transition={{ duration: 0.15 }}
>
<Check className={cls(SIZES[size].icon, "text-primary-cta-text")} />
</motion.div>
</button>
{label && <span className="text-sm text-foreground">{label}</span>}
</label>
);
export default Checkbox;

View File

@@ -0,0 +1,47 @@
import { Check, Loader } from "lucide-react";
import { cls } from "@/lib/utils";
type Item = { label: string; detail: string };
const DELAYS = [
["delay-150", "delay-200", "delay-[250ms]"],
["delay-[350ms]", "delay-[400ms]", "delay-[450ms]"],
["delay-[550ms]", "delay-[600ms]", "delay-[650ms]"],
];
const ChecklistTimeline = ({ heading, subheading, items, completedLabel }: { heading: string; subheading: string; items: [Item, Item, Item]; completedLabel: string }) => (
<div className="group relative flex items-center justify-center h-full w-full overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
{[1, 0.8, 0.6].map((s) => <div key={s} className="absolute h-full aspect-square rounded-full border border-background-accent/30" style={{ transform: `scale(${s})` }} />)}
</div>
<div className="relative flex flex-col gap-3 p-4 max-w-full w-8/10 mask-fade-y">
<div className="flex items-center gap-2 p-3 card shadow rounded">
<Loader className="size-4 text-primary transition-transform duration-1000 group-hover:rotate-360" strokeWidth={1.5} />
<p className="text-xs truncate">{heading}</p>
<p className="text-xs text-foreground/75 ml-auto whitespace-nowrap">{subheading}</p>
</div>
{items.map((item, i) => (
<div key={i} className="flex items-center gap-2 px-3 py-2 card shadow rounded">
<div className="relative flex items-center justify-center size-6 card shadow rounded">
<div className="absolute size-2 primary-button rounded transition-opacity duration-300 group-hover:opacity-0" />
<div className={cls("absolute inset-0 flex items-center justify-center primary-button rounded opacity-0 scale-75 transition-all duration-300 group-hover:opacity-100 group-hover:scale-100", DELAYS[i][0])}>
<Check className="size-3 text-primary-cta-text" strokeWidth={2} />
</div>
</div>
<div className="flex-1 flex items-center justify-between gap-4 min-w-0">
<p className={cls("text-xs truncate opacity-0 transition-opacity duration-300 group-hover:opacity-100", DELAYS[i][1])}>{item.label}</p>
<p className={cls("text-xs text-foreground/75 whitespace-nowrap opacity-0 translate-y-1 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0", DELAYS[i][2])}>{item.detail}</p>
</div>
</div>
))}
<div className="relative flex items-center justify-center p-3 primary-button rounded">
<div className="absolute flex gap-2 transition-opacity duration-500 delay-900 group-hover:opacity-0">
{[0, 1, 2].map((j) => <div key={j} className="size-2 rounded bg-primary-cta-text" />)}
</div>
<p className="text-xs text-primary-cta-text truncate opacity-0 transition-opacity duration-500 delay-900 group-hover:opacity-100">{completedLabel}</p>
</div>
</div>
</div>
);
export default ChecklistTimeline;

View File

@@ -0,0 +1,148 @@
import { cls } from "@/lib/utils";
type ColumnWaveBackgroundProps = {
position: "fixed" | "absolute";
};
const ColumnWaveBackground = ({ position }: ColumnWaveBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden flex items-end justify-between pointer-events-none select-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")} aria-hidden="true">
<div className="relative flex flex-col gap-1 h-full">
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "0s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "0.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "0.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "0.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "0.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.6s" }} />
</div>
<div className="relative flex flex-col gap-1 h-full">
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.25s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.45s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.65s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "1.85s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.05s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.25s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.45s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.65s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.85s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.05s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.25s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.45s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.65s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.85s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.05s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.25s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.45s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.65s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.85s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.05s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.25s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.45s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.65s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.85s" }} />
</div>
<div className="relative flex flex-col gap-1 h-full">
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "2.9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.1s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.3s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.1s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.3s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.1s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.3s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.1s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.3s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.1s" }} />
</div>
<div className="relative flex flex-col gap-1 h-full">
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.75s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "3.95s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.15s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.35s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.55s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.75s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "4.95s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.15s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.35s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.55s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.75s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.95s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.15s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.35s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.55s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.75s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.95s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.15s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.35s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.55s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.75s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.95s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.15s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.35s" }} />
</div>
<div className="relative flex flex-col gap-1 h-full">
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "5.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "6.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "7.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.6s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "8.8s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "9s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "9.2s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "9.4s" }} />
<div className="opacity-0 h-8 w-2 bg-background-accent [box-shadow:0px_0px_50px_16px_color-mix(in_srgb,var(--color-background-accent)_12%,transparent),0px_0px_7px_1px_color-mix(in_srgb,var(--color-background-accent)_31%,transparent)] animate-[cell-wave-pulse_4s_ease_infinite]" style={{ animationDelay: "9.6s" }} />
</div>
</div>
);
};
export default ColumnWaveBackground;

View File

@@ -0,0 +1,20 @@
import { cls } from "@/lib/utils";
type CornerGlowBackgroundProps = {
position: "fixed" | "absolute";
};
const CornerGlowBackground = ({ position }: CornerGlowBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")} aria-hidden="true">
<div
className="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 w-9/10 md:w-6/10 aspect-square rounded-full opacity-20 [background:radial-gradient(circle_at_center,var(--color-background-accent)_35%,transparent_70%)]"
/>
<div
className="absolute bottom-0 left-0 -translate-x-1/2 translate-y-1/2 w-9/10 md:w-6/10 aspect-square rounded-full opacity-20 [background:radial-gradient(circle_at_center,var(--color-background-accent)_35%,transparent_70%)]"
/>
</div>
);
};
export default CornerGlowBackground;

View File

@@ -0,0 +1,54 @@
"use client";
import { useState, useRef, useEffect } from "react";
import type { ReactNode } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronDown } from "lucide-react";
import { cls } from "@/lib/utils";
interface DropdownProps {
children: ReactNode;
label: string;
className?: string;
}
const Dropdown = ({ children, label, className = "" }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div ref={ref} className={cls("relative", className)}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 h-9 px-3 rounded secondary-button text-secondary-cta-text text-sm cursor-pointer"
>
<span>{label}</span>
<ChevronDown className={cls("h-(--text-sm) w-auto transition-transform duration-300 ease-in-out", isOpen && "rotate-180")} />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
className="absolute z-50 mt-2 rounded secondary-button overflow-hidden"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.25 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default Dropdown;

View File

@@ -0,0 +1,68 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronDown } from "lucide-react";
import { cls } from "@/lib/utils";
interface DropdownMenuProps {
options: { label: string; value: string }[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
className?: string;
}
const DropdownMenu = ({ options, value, onChange, placeholder = "Select...", className = "" }: DropdownMenuProps) => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const selected = options.find((o) => o.value === value);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div ref={ref} className={cls("relative", className)}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between h-9 px-3 rounded secondary-button text-secondary-cta-text text-sm cursor-pointer"
>
<span className={selected ? "font-medium" : ""}>{selected?.label || placeholder}</span>
<ChevronDown className={cls("h-(--text-sm) w-auto transition-transform duration-300 ease-in-out", isOpen && "rotate-180")} />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
className="absolute z-50 mt-2 w-full rounded secondary-button overflow-hidden"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.25 }}
>
{options.map((option) => (
<button
key={option.value}
type="button"
onClick={() => { onChange?.(option.value); setIsOpen(false); }}
className={cls(
"w-full px-3 py-2 text-left text-sm text-secondary-cta-text hover:bg-foreground/5 transition-colors duration-500 ease-in-out cursor-pointer",
option.value === value && "bg-foreground/10"
)}
>
{option.label}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default DropdownMenu;

View File

@@ -0,0 +1,82 @@
import { cls } from "@/lib/utils";
type FloatingGradientBackgroundProps = {
position: "fixed" | "absolute";
};
const FloatingGradientBackground = ({ position }: FloatingGradientBackgroundProps) => {
return (
<div
className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none blur-[40px] mask-[linear-gradient(to_bottom,transparent,black_20%,black_80%,transparent)]")}
aria-hidden="true"
>
<div
className="absolute opacity-[0.075]"
style={{
background: "radial-gradient(circle at center, var(--color-background-accent) 0, transparent 50%)",
mixBlendMode: "hard-light",
width: "80%",
height: "80%",
top: "calc(50% - 30%)",
left: "calc(50% - 30%)",
transformOrigin: "center center",
animation: "floating-move-vertical 20s ease infinite",
}}
/>
<div
className="absolute opacity-[0.125]"
style={{
background: "radial-gradient(circle at center, var(--color-accent) 0, transparent 50%)",
mixBlendMode: "hard-light",
width: "80%",
height: "80%",
top: "calc(50% - 30%)",
left: "calc(50% - 30%)",
transformOrigin: "calc(50% - 400px)",
animation: "floating-move-in-circle 20s reverse infinite",
}}
/>
<div
className="absolute opacity-[0.125]"
style={{
background: "radial-gradient(circle at center, var(--color-primary-cta) 0, transparent 50%)",
mixBlendMode: "hard-light",
width: "60%",
height: "60%",
top: "calc(50% - 40% + 200px)",
left: "calc(50% - 40% - 500px)",
transformOrigin: "calc(50% + 400px)",
animation: "floating-move-in-circle 30s linear infinite",
}}
/>
<div
className="absolute opacity-[0.15]"
style={{
background: "radial-gradient(circle at center, var(--color-background-accent) 0, transparent 50%)",
mixBlendMode: "hard-light",
width: "60%",
height: "60%",
top: "calc(50% - 40%)",
left: "calc(50% - 40%)",
transformOrigin: "calc(50% - 200px)",
animation: "floating-move-horizontal 30s ease infinite",
}}
/>
<div
className="absolute opacity-[0.075]"
style={{
background: "radial-gradient(circle at center, var(--color-primary-cta) 0, transparent 50%)",
mixBlendMode: "hard-light",
width: "120%",
height: "120%",
top: "calc(50% - 80%)",
left: "calc(50% - 80%)",
transformOrigin: "calc(50% - 800px) calc(50% + 200px)",
animation: "floating-move-in-circle 20s ease infinite",
}}
/>
</div>
);
};
export default FloatingGradientBackground;

View File

@@ -0,0 +1,37 @@
import { cls } from "@/lib/utils";
type GradientBarsBackgroundProps = {
position: "fixed" | "absolute";
};
const GradientBarsBackground = ({ position }: GradientBarsBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none")} aria-hidden="true">
<div className="flex h-4/5 w-full justify-between mask-[linear-gradient(0deg,transparent_0%,black_100%)]">
<div className="flex h-full w-[35%] overflow-hidden mask-[linear-gradient(270deg,transparent_0%,black_100%)]">
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(90deg,var(--color-primary-cta),transparent)]" />
</div>
<div className="flex h-full w-[35%] justify-end overflow-hidden mask-[linear-gradient(90deg,transparent_0%,black_100%)]">
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
<div className="h-full flex-1 min-w-[30px] max-w-[82px] opacity-[0.075] bg-[linear-gradient(270deg,var(--color-primary-cta),transparent)]" />
</div>
</div>
</div>
);
};
export default GradientBarsBackground;

View File

@@ -0,0 +1,16 @@
import { cls } from "@/lib/utils";
type GridLinesBackgroundProps = {
position: "fixed" | "absolute";
};
const GridLinesBackground = ({ position }: GridLinesBackgroundProps) => {
return (
<div
className={cls(position, "inset-0 -z-10 overflow-hidden bg-background pointer-events-none select-none mask-[radial-gradient(circle_at_center,white_0%,transparent_90%)] bg-[linear-gradient(to_right,color-mix(in_srgb,var(--color-background-accent)_17.5%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_srgb,var(--color-background-accent)_17.5%,transparent)_1px,transparent_1px)] bg-size-[10vw_10vw]")}
aria-hidden="true"
/>
);
};
export default GridLinesBackground;

View File

@@ -0,0 +1,64 @@
import { Children, type ReactNode } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
import { useCarouselControls } from "@/hooks/useCarouselControls";
interface GridOrCarouselProps {
children: ReactNode;
}
const GridOrCarousel = ({ children }: GridOrCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true, containScroll: "trimSnaps" });
const { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
const items = Children.toArray(children);
const count = items.length;
if (count <= 3) {
return (
<div className={cls("grid grid-cols-1 gap-5 mx-auto w-content-width", count === 2 && "md:grid-cols-2", count === 3 && "md:grid-cols-3")}>
{children}
</div>
);
}
const responsiveSwitch = count === 4;
return (
<>
{responsiveSwitch && (
<div className="hidden 2xl:grid grid-cols-4 gap-5 mx-auto w-content-width">{children}</div>
)}
<div className={cls("flex flex-col gap-5 w-full overflow-hidden", responsiveSwitch && "2xl:hidden")}>
<div ref={emblaRef} className="w-full cursor-grab">
<div className="flex gap-4 items-stretch">
<div className="shrink-0 w-carousel-padding" />
{items.map((child, i) => (
<div key={i} className="shrink-0 *:h-full w-carousel-item-3 2xl:w-carousel-item-4">{child}</div>
))}
<div className="shrink-0 w-carousel-padding" />
</div>
</div>
<div className="flex w-full">
<div className="shrink-0 w-carousel-padding-controls" />
<div className="flex justify-between items-center w-full">
<div className="relative h-2 w-1/2 card rounded overflow-hidden">
<div className="absolute top-0 bottom-0 -left-full w-full primary-button rounded" style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }} />
</div>
<div className="flex items-center gap-3">
<button onClick={scrollPrev} disabled={prevDisabled} type="button" aria-label="Previous" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
<ChevronLeft className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
<button onClick={scrollNext} disabled={nextDisabled} type="button" aria-label="Next" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
<ChevronRight className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
</div>
</div>
<div className="shrink-0 w-carousel-padding-controls" />
</div>
</div>
</>
);
};
export default GridOrCarousel;

View File

@@ -0,0 +1,32 @@
"use client";
import { useStyle } from "@/components/ui/useStyle";
import CornerGlowBackground from "@/components/ui/CornerGlowBackground";
import GradientBarsBackground from "@/components/ui/GradientBarsBackground";
import HorizonGlowBackground from "@/components/ui/HorizonGlowBackground";
import LightRaysCenterBackground from "@/components/ui/LightRaysCenterBackground";
import LightRaysCornerBackground from "@/components/ui/LightRaysCornerBackground";
import RadialGradientBackground from "@/components/ui/RadialGradientBackground";
const HeroBackgroundSlot = () => {
const { heroBackground } = useStyle();
switch (heroBackground) {
case "cornerGlow":
return <CornerGlowBackground position="absolute" />;
case "gradientBars":
return <GradientBarsBackground position="absolute" />;
case "horizonGlow":
return <HorizonGlowBackground position="absolute" />;
case "lightRaysCenter":
return <LightRaysCenterBackground position="absolute" />;
case "lightRaysCorner":
return <LightRaysCornerBackground position="absolute" />;
case "radialGradient":
return <RadialGradientBackground position="absolute" />;
default:
return null;
}
};
export default HeroBackgroundSlot;

View File

@@ -0,0 +1,19 @@
import { cls } from "@/lib/utils";
type HorizonGlowBackgroundProps = {
position: "fixed" | "absolute";
};
const HorizonGlowBackground = ({ position }: HorizonGlowBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none mask-[linear-gradient(180deg,rgb(0,0,0)_0%,rgb(0,0,0)_80%,rgba(0,0,0,0)_100%)]")} aria-hidden="true">
<div className="absolute left-1/2 -translate-x-1/2 w-full h-full -bottom-[9vh] overflow-hidden z-0">
<div className="absolute left-1/2 -translate-x-1/2 w-[49vw] h-[12vh] bottom-[25vh] overflow-hidden blur-[57px] [background:radial-gradient(50%_50%_at_50%_50%,color-mix(in_srgb,var(--color-primary-cta)_25%,transparent),transparent)]" />
<div className="absolute -bottom-[61vh] -left-[33vw] -right-[33vw] h-screen rounded-[100%] [background:linear-gradient(180deg,color-mix(in_srgb,var(--color-primary-cta)_30%,transparent),transparent)]" />
<div className="absolute -bottom-[62vh] -left-[36vw] -right-[36vw] h-[105vh] rounded-[100%] bg-background [box-shadow:inset_0_2px_20px_color-mix(in_srgb,var(--color-primary-cta)_30%,transparent),0_-10px_50px_1px_color-mix(in_srgb,var(--color-primary-cta)_25%,transparent)]" />
</div>
</div>
);
};
export default HorizonGlowBackground;

View File

@@ -0,0 +1,61 @@
import { useRef, useState, useEffect } from "react";
import { motion, useMotionValue, useMotionTemplate } from "motion/react";
import { cls } from "@/lib/utils";
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomChars = () => Array.from({ length: 1500 }, () => CHARS[Math.floor(Math.random() * 62)]).join("");
interface HoverPatternProps {
children: React.ReactNode;
className?: string;
}
const HoverPattern = ({ children, className = "" }: HoverPatternProps) => {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const [chars, setChars] = useState(randomChars);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
useEffect(() => {
if (isMobile && ref.current) {
x.set(ref.current.offsetWidth / 2);
y.set(ref.current.offsetHeight / 2);
}
}, [isMobile, x, y]);
const mask = useMotionTemplate`radial-gradient(${isMobile ? 110 : 250}px at ${x}px ${y}px, white, transparent)`;
const base = "absolute inset-0 rounded-[inherit] transition-opacity duration-300";
return (
<div
ref={ref}
className={cls("group/pattern relative", className)}
onMouseMove={isMobile ? undefined : (e) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
x.set(e.clientX - rect.left);
y.set(e.clientY - rect.top);
setChars(randomChars());
}}
>
{children}
<div className="pointer-events-none absolute inset-0 rounded-[inherit]">
<div className={cls(base, isMobile ? "opacity-25" : "opacity-0 group-hover/pattern:opacity-25")} style={{ background: "linear-gradient(var(--foreground), transparent)" }} />
<motion.div className={cls(base, "bg-linear-to-r from-accent to-accent/50 backdrop-blur-xl", isMobile ? "opacity-100" : "opacity-0 group-hover/pattern:opacity-100")} style={{ maskImage: mask, WebkitMaskImage: mask }} />
<motion.div className={cls(base, "mix-blend-overlay", isMobile ? "opacity-100" : "opacity-0 group-hover/pattern:opacity-100")} style={{ maskImage: mask, WebkitMaskImage: mask }}>
<p className="absolute inset-0 h-full whitespace-pre-wrap wrap-break-word font-mono text-xs font-bold text-foreground">{chars}</p>
</motion.div>
</div>
</div>
);
};
export default HoverPattern;

View File

@@ -0,0 +1,42 @@
import type { LucideIcon } from "lucide-react";
import { resolveIcon } from "@/utils/resolve-icon";
import { cls } from "@/lib/utils";
interface IconBadgeProps {
icon: string | LucideIcon;
size?: "sm" | "base" | "lg";
className?: string;
}
const sizeStyles = {
sm: "size-10",
base: "size-12",
lg: "size-14",
};
const iconSizeStyles = {
sm: "size-3.5",
base: "size-4",
lg: "size-5",
};
const IconBadge = ({ icon, size = "base", className }: IconBadgeProps) => {
const IconComponent = resolveIcon(icon);
return (
<div
className={cls(
"flex items-center justify-center primary-button rounded shadow",
sizeStyles[size],
className
)}
>
<IconComponent
className={cls("text-primary-cta-text", iconSizeStyles[size])}
strokeWidth={1.5}
/>
</div>
);
};
export default IconBadge;

View File

@@ -0,0 +1,29 @@
import type { LucideIcon } from "lucide-react";
import { cls } from "@/lib/utils";
import { resolveIcon } from "@/utils/resolve-icon";
const IconTextMarquee = ({ centerIcon, texts }: { centerIcon: string | LucideIcon; texts: string[] }) => {
const CenterIcon = resolveIcon(centerIcon);
const items = [...texts, ...texts];
return (
<div className="relative flex flex-col h-full w-full overflow-hidden" style={{ maskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)" }}>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col gap-2 w-full opacity-60">
{Array.from({ length: 10 }).map((_, row) => (
<div key={row} className={cls("flex gap-2", row % 2 === 0 ? "animate-marquee-horizontal" : "animate-marquee-horizontal-reverse")}>
{items.map((text, i) => (
<div key={i} className="flex items-center justify-center px-4 py-2 card rounded">
<p className="text-sm leading-snug">{text}</p>
</div>
))}
</div>
))}
</div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex items-center justify-center size-16 primary-button backdrop-blur-sm rounded">
<CenterIcon className="size-6 text-primary-cta-text" strokeWidth={1.5} />
</div>
</div>
);
};
export default IconTextMarquee;

View File

@@ -0,0 +1,41 @@
import { cls } from "@/lib/utils";
interface ImageOrVideoProps {
imageSrc?: string;
videoSrc?: string;
className?: string;
}
const ImageOrVideo = ({
imageSrc,
videoSrc,
className = "",
}: ImageOrVideoProps) => {
if (videoSrc) {
return (
<video
src={videoSrc}
aria-label={videoSrc}
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
autoPlay
loop
muted
playsInline
/>
);
}
if (imageSrc) {
return (
<img
src={imageSrc}
alt={imageSrc}
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
/>
);
}
return null;
};
export default ImageOrVideo;

View File

@@ -0,0 +1,28 @@
import type { LucideIcon } from "lucide-react";
import { resolveIcon } from "@/utils/resolve-icon";
type Item = { icon: string | LucideIcon; label: string; value: string };
const InfoCardMarquee = ({ items }: { items: Item[] }) => {
const duplicated = [...items, ...items, ...items, ...items];
return (
<div className="h-full overflow-hidden mask-fade-y">
<div className="flex flex-col animate-marquee-vertical">
{duplicated.map((item, i) => (
<div key={i} className="flex items-center justify-between gap-4 p-3 mb-4 card rounded">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center size-10 secondary-button rounded">
{(() => { const Icon = resolveIcon(item.icon); return <Icon className="size-4 text-secondary-cta-text" strokeWidth={1.5} />; })()}
</div>
<p className="text-base truncate">{item.label}</p>
</div>
<p className="text-base">{item.value}</p>
</div>
))}
</div>
</div>
);
};
export default InfoCardMarquee;

View File

@@ -0,0 +1,18 @@
import { cls } from "@/lib/utils";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
className?: string;
}
const Input = ({ className = "", ...props }: InputProps) => (
<input
className={cls(
"w-full h-9 px-3 rounded secondary-button text-secondary-cta-text text-sm",
"placeholder:text-secondary-cta-text/50 focus:outline-none",
className
)}
{...props}
/>
);
export default Input;

View File

@@ -0,0 +1,17 @@
import { cls } from "@/lib/utils";
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
className?: string;
}
const Label = ({ className = "", ...props }: LabelProps) => (
<label
className={cls(
"text-sm font-medium text-foreground",
className
)}
{...props}
/>
);
export default Label;

View File

@@ -0,0 +1,58 @@
import { cls } from "@/lib/utils";
type LightRaysCenterBackgroundProps = {
position: "fixed" | "absolute";
};
const LightRaysCenterBackground = ({ position }: LightRaysCenterBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none")} aria-hidden="true">
<div className="absolute inset-0 bg-background mask-[radial-gradient(50%_50%_at_50%_0%,white_0%,transparent_100%)] bg-[linear-gradient(to_right,color-mix(in_srgb,var(--color-background-accent)_20%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_srgb,var(--color-background-accent)_10%,transparent)_1px,transparent_1px)] bg-size-[10vw_10vw]" />
<div className="absolute -top-[400px] left-1/2 -translate-x-1/2 w-[1142px] h-[129vh] overflow-hidden blur-lg mask-[radial-gradient(50%_109%,#000_0%,#000000f6_0%,transparent_96%)]">
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 1, transform: "rotate(-20deg)", animation: "ray-pulse 4s ease-in-out 0s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.6, transform: "rotate(-12deg)", animation: "ray-pulse 3.5s ease-in-out 0.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.45, transform: "scale(0.9) rotate(-5deg)", animation: "ray-pulse 5s ease-in-out 1.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-7.5px)] w-[15px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.625, transform: "rotate(-3deg)", animation: "ray-pulse 3s ease-in-out 0.3s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-20px)] w-[40px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.1, transform: "scale(0.79) rotate(0deg)", animation: "ray-pulse 4.5s ease-in-out 0.8s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.525, transform: "rotate(3deg)", animation: "ray-pulse 3.2s ease-in-out 1.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-7.5px)] w-[15px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.725, transform: "scale(0.9) rotate(5deg)", animation: "ray-pulse 4.2s ease-in-out 0.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.6, transform: "rotate(12deg)", animation: "ray-pulse 3.8s ease-in-out 1s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 1, transform: "rotate(20deg)", animation: "ray-pulse 4s ease-in-out 0.7s infinite both" } as React.CSSProperties}
/>
<div className="absolute left-[calc(50%-599px)] -top-[352px] -bottom-[46px] w-[1198px] opacity-[0.025] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-10 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-10 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
</div>
</div>
);
};
export default LightRaysCenterBackground;

View File

@@ -0,0 +1,54 @@
import { cls } from "@/lib/utils";
type LightRaysCornerBackgroundProps = {
position: "fixed" | "absolute";
};
const LightRaysCornerBackground = ({ position }: LightRaysCornerBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none")} aria-hidden="true">
<div className="absolute inset-0 bg-background mask-[radial-gradient(50%_50%_at_50%_0%,white_0%,transparent_100%)] bg-[linear-gradient(to_right,color-mix(in_srgb,var(--color-background-accent)_20%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_srgb,var(--color-background-accent)_10%,transparent)_1px,transparent_1px)] bg-size-[10vw_10vw]" />
<div className="absolute -top-[571px] -left-[373px] w-[1142px] h-[179vh] -rotate-[38deg] overflow-hidden blur-lg mask-[radial-gradient(50%_109%,#000_0%,#000000f6_0%,transparent_96%)]">
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.85, transform: "rotate(-18deg)", animation: "rotated-ray-pulse 4s ease-in-out 0s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.775, transform: "rotate(-12deg)", animation: "rotated-ray-pulse 3.5s ease-in-out 0.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.65, transform: "scale(0.9) rotate(-5deg)", animation: "rotated-ray-pulse 5s ease-in-out 1.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-7.5px)] w-[15px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.25, transform: "rotate(-3deg)", animation: "rotated-ray-pulse 3s ease-in-out 0.3s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-20px)] w-[40px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.45, transform: "scale(0.79) rotate(0deg)", animation: "rotated-ray-pulse 4.5s ease-in-out 0.8s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.45, transform: "rotate(6deg)", animation: "rotated-ray-pulse 3.2s ease-in-out 1.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.65, transform: "scale(0.83) rotate(9deg)", animation: "rotated-ray-pulse 4.2s ease-in-out 0.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 1, transform: "scale(0.9) rotate(14deg)", animation: "rotated-ray-pulse 3.8s ease-in-out 1s infinite both" } as React.CSSProperties}
/>
<div className="absolute left-[calc(50%-599px)] -top-[352px] -bottom-[46px] w-[1198px] opacity-[0.05] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-15 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-15 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
</div>
</div>
);
};
export default LightRaysCornerBackground;

View File

@@ -0,0 +1,71 @@
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
interface LoaderRevealProps {
imageSrc: string;
title: string;
onComplete?: () => void;
}
const LoaderReveal = ({ imageSrc, title, onComplete }: LoaderRevealProps) => {
const [isVisible, setIsVisible] = useState(true);
const [introComplete, setIntroComplete] = useState(false);
return (
<AnimatePresence onExitComplete={onComplete}>
{isVisible && (
<motion.div className="fixed inset-0 z-100 text-primary-cta-text">
<motion.div
className="absolute inset-0 w-full h-full bg-background-accent"
exit={{ y: "-101%" }}
transition={{ duration: 1, ease: [0.76, 0, 0.24, 1] }}
>
<motion.div
className="absolute bottom-0 left-0 right-0 h-2 bg-primary-cta-text"
initial={{ scaleX: 0 }}
animate={{ scaleX: introComplete ? 0 : 1 }}
style={{ originX: introComplete ? 1 : 0 }}
transition={{ duration: introComplete ? 0.5 : 3, ease: [0.76, 0, 0.24, 1] }}
onAnimationComplete={() => {
if (!introComplete) {
setIntroComplete(true);
} else {
setIsVisible(false);
}
}}
/>
</motion.div>
<motion.div
className="relative z-2 flex flex-col justify-center items-center w-full h-full gap-4 md:gap-5"
animate={introComplete ? { opacity: 0 } : { opacity: 1 }}
transition={{ duration: 0.5 }}
>
<div className="card p-px rounded-full">
<img
src={imageSrc}
alt=""
className="size-12 md:size-16 rounded-full object-cover"
/>
</div>
<div className="relative flex justify-center items-center">
<span className="absolute text-center text-2xl md:text-4xl font-medium tracking-tight opacity-20">
{title}
</span>
<motion.span
className="text-2xl md:text-4xl font-medium tracking-tight"
initial={{ clipPath: "inset(0% 100% 0% 0%)" }}
animate={{ clipPath: "inset(0% 0% 0% 0%)" }}
transition={{ duration: 3, ease: [0.76, 0, 0.24, 1] }}
>
{title}
</motion.span>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export default LoaderReveal;

View File

@@ -0,0 +1,76 @@
import { Children, useCallback, useEffect, useState, type ReactNode } from "react";
import useEmblaCarousel from "embla-carousel-react";
import type { EmblaCarouselType } from "embla-carousel";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
interface LoopCarouselProps {
children: ReactNode;
}
const LoopCarousel = ({ children }: LoopCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center", containScroll: "trimSnaps" });
const [selectedIndex, setSelectedIndex] = useState(0);
const items = Children.toArray(children);
const onSelect = useCallback((api: EmblaCarouselType) => {
setSelectedIndex(api.selectedScrollSnap());
}, []);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on("select", onSelect).on("reInit", onSelect);
return () => {
emblaApi.off("select", onSelect).off("reInit", onSelect);
};
}, [emblaApi, onSelect]);
return (
<div className="relative w-full md:w-content-width mx-auto">
<div ref={emblaRef} className="overflow-hidden w-full mask-fade-x-medium">
<div className="flex w-full">
{items.map((child, index) => (
<div key={index} className="shrink-0 w-content-width md:w-[clamp(18rem,50vw,48rem)] mr-3 md:mr-6">
<div
className={cls(
"transition-all duration-500 ease-out",
selectedIndex === index ? "opacity-100 scale-100" : "opacity-70 scale-90"
)}
>
{child}
</div>
</div>
))}
</div>
</div>
<div className="absolute inset-y-0 left-0 right-0 flex items-center justify-between w-content-width mx-auto pointer-events-none">
<button
onClick={scrollPrev}
type="button"
aria-label="Previous slide"
className="flex items-center justify-center h-9 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
>
<ChevronLeft className="h-2/5 aspect-square text-primary-cta-text" />
</button>
<button
onClick={scrollNext}
type="button"
aria-label="Next slide"
className="flex items-center justify-center h-9 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
>
<ChevronRight className="h-2/5 aspect-square text-primary-cta-text" />
</button>
</div>
</div>
);
};
export default LoopCarousel;

View File

@@ -0,0 +1,32 @@
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { cls } from "@/lib/utils";
type Item = { imageSrc?: string; videoSrc?: string };
const MediaStack = ({ items }: { items: [Item, Item, Item] }) => (
<div className="group/stack relative flex items-center justify-center h-full w-full rounded select-none card">
<div className={cls(
"absolute z-1 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
"translate-x-[12%] -translate-y-[8%] rotate-8 transition-all duration-500",
"group-hover/stack:translate-x-[22%] group-hover/stack:-translate-y-[14%] group-hover/stack:rotate-12"
)}>
<ImageOrVideo imageSrc={items[2].imageSrc} videoSrc={items[2].videoSrc} className="h-full rounded" />
</div>
<div className={cls(
"absolute z-2 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
"-translate-x-[12%] -translate-y-[8%] -rotate-8 transition-all duration-500",
"group-hover/stack:-translate-x-[22%] group-hover/stack:-translate-y-[14%] group-hover/stack:-rotate-12"
)}>
<ImageOrVideo imageSrc={items[1].imageSrc} videoSrc={items[1].videoSrc} className="h-full rounded" />
</div>
<div className={cls(
"absolute z-30 overflow-hidden p-1 w-3/5 aspect-4/3 rounded primary-button",
"translate-y-[10%] transition-all duration-500",
"group-hover/stack:translate-y-[20%]"
)}>
<ImageOrVideo imageSrc={items[0].imageSrc} videoSrc={items[0].videoSrc} className="h-full rounded" />
</div>
</div>
);
export default MediaStack;

View File

@@ -0,0 +1,69 @@
"use client";
import { useState, useEffect } from "react";
import type { ReactNode } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X } from "lucide-react";
import { cls } from "@/lib/utils";
interface ModalProps {
trigger: ReactNode;
title: string;
description?: string;
children: ReactNode;
className?: string;
}
const Modal = ({ trigger, title, description, children, className = "" }: ModalProps) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
if (isOpen) document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen]);
return (
<>
<div onClick={() => setIsOpen(true)} className="cursor-pointer">{trigger}</div>
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<motion.div
className="absolute inset-0 bg-background/30 backdrop-blur-[1px]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsOpen(false)}
/>
<motion.div
className={cls("relative card rounded p-3 xl:p-4 2xl:p-5", className)}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
>
<div className="flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 mb-3 xl:mb-4 2xl:mb-5">
<div className="min-w-0">
<h2 className="text-lg font-medium text-foreground truncate">{title}</h2>
{description && <p className="text-sm text-foreground truncate">{description}</p>}
</div>
<button
onClick={() => setIsOpen(false)}
className="shrink-0 flex items-center justify-center size-9 rounded secondary-button text-secondary-cta-text cursor-pointer"
>
<X className="size-4" />
</button>
</div>
{children}
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
};
export default Modal;

View File

@@ -0,0 +1,141 @@
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Plus, ArrowRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarCenteredProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose?.();
};
const NavbarCentered = ({ logo, navItems, ctaButton }: NavbarCenteredProps) => {
const [isScrolled, setIsScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 50);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuOpen) setMenuOpen(false);
};
const handleClickOutside = (e: MouseEvent) => {
if (menuOpen && menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuOpen]);
return (
<>
<nav
className={cls(
"fixed z-1000 top-0 left-0 w-full transition-all duration-500 ease-in-out",
isScrolled ? "h-15 bg-background/80 backdrop-blur-sm" : "h-20 bg-background/0 backdrop-blur-0"
)}
>
<div className="relative mx-auto flex items-center justify-between h-full w-content-width">
<a href="/" className="text-xl font-medium text-foreground">{logo}</a>
<div className="hidden md:flex absolute left-1/2 -translate-x-1/2 items-center gap-6">
{navItems.map((item) => (
<a
key={item.name}
href={item.href}
onClick={(e) => handleNavClick(e, item.href)}
className="text-base text-foreground hover:opacity-70 transition-opacity"
>
{item.name}
</a>
))}
</div>
<div className="flex items-center gap-2 xl:gap-3 2xl:gap-4">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} />
<button
className="flex md:hidden items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<Plus
className={cls("w-1/2 h-1/2 text-primary-cta-text transition-transform duration-300", menuOpen ? "rotate-45" : "rotate-0")}
strokeWidth={1.5}
/>
</button>
</div>
</div>
</nav>
<AnimatePresence>
{menuOpen && (
<motion.div
ref={menuRef}
initial={{ y: "-135%" }}
animate={{ y: 0 }}
exit={{ y: "-135%" }}
transition={{ type: "spring", damping: 26, stiffness: 170 }}
className="md:hidden fixed z-1000 top-3 left-3 right-3 p-6 rounded card"
>
<div className="flex items-center justify-between mb-6">
<p className="text-xl text-foreground">Menu</p>
<button
className="flex items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(false)}
aria-label="Close menu"
>
<Plus className="w-1/2 h-1/2 text-primary-cta-text rotate-45" strokeWidth={1.5} />
</button>
</div>
<div className="flex flex-col gap-4">
{navItems.map((item, index) => (
<div key={item.name}>
<a
href={item.href}
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
className="flex items-center justify-between py-2 text-base font-medium text-foreground"
>
{item.name}
<ArrowRight className="size-4 text-foreground" strokeWidth={1.5} />
</a>
{index < navItems.length - 1 && (
<div className="h-px bg-linear-to-r from-transparent via-foreground/20 to-transparent" />
)}
</div>
))}
</div>
<div className="mt-6">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} className="w-full" />
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default NavbarCentered;

View File

@@ -0,0 +1,109 @@
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarDropdownProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose?.();
};
const NavbarDropdown = ({ logo, navItems, ctaButton }: NavbarDropdownProps) => {
const [menuOpen, setMenuOpen] = useState(false);
const navRef = useRef<HTMLElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuOpen) setMenuOpen(false);
};
const handleClickOutside = (e: MouseEvent) => {
if (menuOpen && navRef.current && !navRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuOpen]);
return (
<nav ref={navRef} className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width">
<div className="flex items-center justify-between p-2 xl:p-3 2xl:p-4 rounded backdrop-blur-sm card">
<a href="/" className="pl-2 text-xl font-medium text-foreground">{logo}</a>
<div className="flex items-center gap-2 xl:gap-3 2xl:gap-4">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} />
<button
className="relative flex items-center justify-center size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<span
className={cls(
"absolute w-3 h-px bg-primary-cta-text transition-all duration-300",
menuOpen ? "rotate-45" : "-translate-y-1"
)}
/>
<span
className={cls(
"absolute w-3 h-px bg-primary-cta-text transition-all duration-300",
menuOpen ? "-rotate-45" : "translate-y-1"
)}
/>
</button>
</div>
</div>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
className="absolute top-full right-2 xl:right-3 2xl:right-4 -mt-1 px-4 py-1 w-3/4 md:w-3/10 2xl:w-25/100 rounded primary-button"
>
{navItems.map((item, index) => (
<div key={item.name}>
<a
href={item.href}
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
className="group flex items-center justify-between py-3 w-full"
>
<span className="text-base text-primary-cta-text group-hover:ml-2 transition-[margin] duration-300">
{item.name}
</span>
<ArrowUpRight
className="h-(--text-base) w-auto text-primary-cta-text group-hover:rotate-45 group-hover:mr-2 transition-all duration-300"
strokeWidth={1.75}
/>
</a>
{index < navItems.length - 1 && (
<div className="h-px bg-primary-cta-text/20" />
)}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</nav>
);
};
export default NavbarDropdown;

View File

@@ -0,0 +1,113 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Plus, ArrowUpRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarFloatingProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose?.();
};
const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => {
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuOpen) setMenuOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [menuOpen]);
return (
<>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.15 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="fixed inset-0 z-999 bg-foreground"
onClick={() => setMenuOpen(false)}
/>
)}
</AnimatePresence>
<nav className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width">
<div className="mx-auto w-full md:w-1/2 overflow-hidden rounded backdrop-blur-sm card">
<div className="relative z-10 flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5">
<a href="/" className="text-xl font-medium text-foreground">{logo}</a>
<button
className="flex items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<Plus
className={cls(
"w-1/2 h-1/2 text-primary-cta-text transition-transform duration-300",
menuOpen ? "rotate-45" : "rotate-0"
)}
strokeWidth={1.5}
/>
</button>
</div>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
transition={{ duration: 0.5, ease: [0.625, 0.05, 0, 1] }}
className="overflow-hidden"
>
<div className="flex flex-col gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5 pt-0 xl:pt-0 2xl:pt-0">
<div className="px-3 xl:px-4 2xl:px-5 py-0 md:py-1 2xl:py-2 rounded card">
{navItems.map((item, index) => (
<div key={item.name}>
<a
href={item.href}
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
className="group flex items-center justify-between py-3 w-full"
>
<span className="text-xl md:text-2xl font-medium text-foreground group-hover:ml-3 transition-[margin] duration-300">
{item.name}
</span>
<ArrowUpRight
className="h-(--text-xl) md:h-(--text-2xl) w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300"
strokeWidth={2}
/>
</a>
{index < navItems.length - 1 && (
<div className="h-px bg-accent/50" />
)}
</div>
))}
</div>
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} className="w-full" />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</nav>
</>
);
};
export default NavbarFloating;

View File

@@ -0,0 +1,117 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Plus, ArrowUpRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarFloatingLogoProps {
logo: string;
logoImageSrc: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose?.();
};
const NavbarFloatingLogo = ({ logo, logoImageSrc, navItems, ctaButton }: NavbarFloatingLogoProps) => {
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuOpen) setMenuOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [menuOpen]);
return (
<>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.15 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="fixed inset-0 z-999 bg-foreground"
onClick={() => setMenuOpen(false)}
/>
)}
</AnimatePresence>
<nav className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width">
<div className="mx-auto w-full md:w-1/2 overflow-hidden rounded backdrop-blur-sm card">
<div className="relative z-10 flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5">
<a href="/" className="flex items-center gap-2">
<img src={logoImageSrc} alt={logo} className="h-8 w-8 rounded-full object-cover" />
<span className="text-xl font-medium text-foreground">{logo}</span>
</a>
<button
className="flex items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<Plus
className={cls(
"w-1/2 h-1/2 text-primary-cta-text transition-transform duration-300",
menuOpen ? "rotate-45" : "rotate-0"
)}
strokeWidth={1.5}
/>
</button>
</div>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
transition={{ duration: 0.5, ease: [0.625, 0.05, 0, 1] }}
className="overflow-hidden"
>
<div className="flex flex-col gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5 pt-0 xl:pt-0 2xl:pt-0">
<div className="px-3 xl:px-4 2xl:px-5 py-0 md:py-1 2xl:py-2 rounded card">
{navItems.map((item, index) => (
<div key={item.name}>
<a
href={item.href}
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
className="group flex items-center justify-between py-3 w-full"
>
<span className="text-xl md:text-2xl font-medium text-foreground group-hover:ml-3 transition-[margin] duration-300">
{item.name}
</span>
<ArrowUpRight
className="h-(--text-xl) md:h-(--text-2xl) w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300"
strokeWidth={2}
/>
</a>
{index < navItems.length - 1 && (
<div className="h-px bg-accent/50" />
)}
</div>
))}
</div>
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} className="w-full" />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</nav>
</>
);
};
export default NavbarFloatingLogo;

View File

@@ -0,0 +1,123 @@
import { useState, useEffect } from "react";
import { ArrowUpRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarFullscreenProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose();
};
const NavbarFullscreen = ({ logo, navItems, ctaButton }: NavbarFullscreenProps) => {
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && menuOpen) setMenuOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [menuOpen]);
useEffect(() => {
if (menuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [menuOpen]);
return (
<nav className="fixed inset-0 z-1000 pointer-events-none">
<div className="absolute z-10 top-5 left-1/2 -translate-x-1/2 flex items-center justify-between w-content-width pointer-events-auto">
<a
href="/"
className={cls(
"text-xl font-medium transition-colors duration-500",
menuOpen ? "text-background" : "text-foreground"
)}
>
{logo}
</a>
<div className="flex items-center gap-2 xl:gap-3 2xl:gap-4">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} />
<button
className="relative flex items-center justify-center size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<span
className={cls(
"absolute w-3 h-px bg-primary-cta-text transition-all duration-300",
menuOpen ? "rotate-45" : "-translate-y-1"
)}
/>
<span
className={cls(
"absolute w-3 h-px bg-primary-cta-text transition-all duration-300",
menuOpen ? "-rotate-45" : "translate-y-1"
)}
/>
</button>
</div>
</div>
<div
className="absolute inset-0 flex flex-col items-center justify-center bg-foreground pointer-events-auto transition-[clip-path] duration-700 ease-[cubic-bezier(0.9,0,0.1,1)]"
style={{
clipPath: menuOpen
? "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)"
: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)"
}}
>
<div className="flex flex-col items-center">
{navItems.map((item, index) => (
<div key={item.name} className="overflow-hidden">
<a
href={item.href}
onClick={(e) => handleNavClick(e, item.href, () => setMenuOpen(false))}
className="group flex items-center gap-4 py-4"
style={{
transform: menuOpen ? "translateY(0%)" : "translateY(100%)",
transition: "transform 0.5s cubic-bezier(0.7, 0, 0.3, 1)",
transitionDelay: menuOpen
? `${0.3 + index * 0.05}s`
: `${(navItems.length - 1 - index) * 0.05}s`
}}
>
<span className="text-7xl md:text-9xl font-medium text-background group-hover:ml-4 transition-[margin] duration-300">
{item.name}
</span>
<ArrowUpRight
className="h-(--text-7xl) md:h-(--text-9xl) w-auto text-background group-hover:rotate-45 group-hover:mr-4 transition-all duration-300"
strokeWidth={1.5}
/>
</a>
{index < navItems.length - 1 && (
<div className="h-px bg-background/20" />
)}
</div>
))}
</div>
</div>
</nav>
);
};
export default NavbarFullscreen;

View File

@@ -0,0 +1,43 @@
import Button from "@/components/ui/Button";
interface NavbarInlineProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, onClose?: () => void) => {
if (href.startsWith("#")) {
e.preventDefault();
const element = document.getElementById(href.slice(1));
element?.scrollIntoView({ behavior: "smooth", block: "start" });
}
onClose?.();
};
const NavbarInline = ({ logo, navItems, ctaButton }: NavbarInlineProps) => {
return (
<nav className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width">
<div className="flex items-center justify-between p-2 xl:p-3 2xl:p-4 rounded backdrop-blur-sm card">
<a href="/" className="pl-2 text-xl font-medium text-foreground">{logo}</a>
<div className="hidden md:flex absolute left-1/2 -translate-x-1/2 items-center gap-6">
{navItems.map((item) => (
<a
key={item.name}
href={item.href}
onClick={(e) => handleNavClick(e, item.href)}
className="relative text-base text-foreground after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-current after:scale-x-0 after:origin-right after:transition-transform after:duration-300 hover:after:scale-x-100 hover:after:origin-left"
>
{item.name}
</a>
))}
</div>
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" animate={false} />
</div>
</nav>
);
};
export default NavbarInline;

View File

@@ -0,0 +1,15 @@
import { cls } from "@/lib/utils";
type NoiseBackgroundProps = {
position: "fixed" | "absolute";
};
const NoiseBackground = ({ position }: NoiseBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden bg-background-accent/10 pointer-events-none select-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")} aria-hidden="true">
<div className="absolute inset-0 bg-repeat mix-blend-overlay opacity-10 bg-[url(https://storage.googleapis.com/webild/default/noise.webp)] bg-size-[512px]" />
</div>
);
};
export default NoiseBackground;

View File

@@ -0,0 +1,16 @@
import { cls } from "@/lib/utils";
type NoiseGradientBackgroundProps = {
position: "fixed" | "absolute";
};
const NoiseGradientBackground = ({ position }: NoiseGradientBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden bg-background-accent/10 pointer-events-none select-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")} aria-hidden="true">
<div className="absolute inset-0 overflow-hidden bg-gradient-to-br from-background via-background-accent/10 to-background-accent/20" />
<div className="absolute inset-0 bg-repeat mix-blend-overlay opacity-10 bg-[url(https://storage.googleapis.com/webild/default/noise.webp)] bg-size-[512px]" />
</div>
);
};
export default NoiseGradientBackground;

View File

@@ -0,0 +1,37 @@
import type { LucideIcon } from "lucide-react";
import { resolveIcon } from "@/utils/resolve-icon";
const OrbitingIcons = ({ centerIcon, items }: { centerIcon: string | LucideIcon; items: (string | LucideIcon)[] }) => {
const CenterIcon = resolveIcon(centerIcon);
return (
<div
className="relative flex items-center justify-center h-full overflow-hidden"
style={{ perspective: "2000px", maskImage: "linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, transparent, black 10%, black 90%, transparent)", maskComposite: "intersect" }}
>
<div className="flex items-center justify-center w-full h-full" style={{ transform: "rotateY(20deg) rotateX(20deg) rotateZ(-20deg)" }}>
<div className="absolute size-60 opacity-85 border border-background-accent shadow rounded-full" />
<div className="absolute size-80 opacity-75 border border-background-accent shadow rounded-full" />
<div className="absolute size-100 opacity-65 border border-background-accent shadow rounded-full" />
<div className="absolute flex items-center justify-center size-40 border border-background-accent shadow rounded-full">
<div className="flex items-center justify-center size-20 primary-button rounded-full">
<CenterIcon className="size-10 text-primary-cta-text" strokeWidth={1.25} />
</div>
{items.map((iconInput, i) => {
const Icon = resolveIcon(iconInput);
return (
<div
key={i}
className="absolute flex items-center justify-center size-10 rounded shadow card -ml-5 -mt-5"
style={{ top: "50%", left: "50%", animation: "orbit 12s linear infinite", "--initial-position": `${(360 / items.length) * i}deg`, "--translate-position": "160px" } as React.CSSProperties}
>
<Icon className="size-4" strokeWidth={1.5} />
</div>
);
})}
</div>
</div>
</div>
);
};
export default OrbitingIcons;

View File

@@ -0,0 +1,35 @@
import { cls } from "@/lib/utils";
interface PriceDisplayProps {
price: number;
originalPrice?: number;
currency?: string;
period?: string;
className?: string;
}
const PriceDisplay = ({
price,
originalPrice,
currency = "$",
period,
className,
}: PriceDisplayProps) => (
<div className={cls("flex flex-col gap-1", className)}>
<div className="flex items-baseline gap-2">
<span className="text-5xl md:text-6xl font-semibold">
{currency}
{price}
</span>
{originalPrice && (
<span className="text-xl text-foreground/50 line-through">
{currency}
{originalPrice}
</span>
)}
</div>
{period && <span className="text-base font-medium">{period}</span>}
</div>
);
export default PriceDisplay;

View File

@@ -0,0 +1,15 @@
import { cls } from "@/lib/utils";
type RadialGradientBackgroundProps = {
position: "fixed" | "absolute";
};
const RadialGradientBackground = ({ position }: RadialGradientBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none", position === "absolute" && "mask-[linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]")} aria-hidden="true">
<div className="relative w-full h-full bg-[radial-gradient(130%_130%_at_50%_15%,var(--background)_40%,var(--color-background-accent)_100%)] mask-[linear-gradient(180deg,transparent_0%,transparent_15%,black_55%,black_100%)]" />
</div>
);
};
export default RadialGradientBackground;

View File

@@ -0,0 +1,24 @@
import { Star } from "lucide-react";
import { cls } from "@/lib/utils";
interface RatingStarsProps {
rating: number;
className?: string;
}
const RatingStars = ({ rating, className }: RatingStarsProps) => (
<div className={cls("flex gap-1.5", className)}>
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"size-5 text-accent",
index < rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
);
export default RatingStars;

View File

@@ -0,0 +1,44 @@
"use client";
import { motion } from "motion/react";
type Variant = "slide-up" | "fade-blur" | "fade";
interface ScrollRevealProps {
children: React.ReactNode;
variant: Variant;
delay?: number;
className?: string;
}
const VARIANTS = {
"slide-up": {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
},
"fade-blur": {
hidden: { opacity: 0, filter: "blur(10px)" },
visible: { opacity: 1, filter: "blur(0px)" },
},
"fade": {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
};
const ScrollReveal = ({ children, variant, delay = 0, className = "" }: ScrollRevealProps) => {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-20%" }}
variants={VARIANTS[variant]}
transition={{ duration: 0.6, delay, ease: "easeOut" }}
className={className}
>
{children}
</motion.div>
);
};
export default ScrollReveal;

View File

@@ -0,0 +1,90 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
/**
* Per-section error boundary inserted around every assembled section by the
* backend's page-assembler. Goal: a single section that throws at runtime
* (missing required prop, broken `.map`, etc.) shows a small placeholder
* instead of taking down the entire page with a white screen.
*
* Also reports the failure via the `/__webild/render-status` probe channel
* so Bob-AI's post-commit poll picks up the section name + error message and
* the model gets the signal to fix the right section on the next loop turn.
*
* The probe POST is best-effort and silent — sandbox-only (gated by
* `window.parent !== window`), so production deploys never call it.
*/
interface Props {
/** Section slug — same value the wrapping `<div data-section="…">` uses. */
name: string;
children: ReactNode;
}
interface State {
hasError: boolean;
errorMessage?: string;
}
const RENDER_STATUS_URL = "/__webild/render-status";
export default class SectionErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: unknown): State {
return {
hasError: true,
errorMessage:
error instanceof Error ? error.message : String(error ?? "unknown"),
};
}
componentDidCatch(error: Error, info: ErrorInfo) {
if (typeof window === "undefined") return;
if (window.parent === window) return;
try {
fetch(RENDER_STATUS_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ok: false,
reason: "section_error_boundary",
section: this.props.name,
error: String(error?.message || error || "unknown"),
stack: String(error?.stack || "").slice(0, 4000),
componentStack: String(info?.componentStack || "").slice(0, 4000),
t: Date.now(),
}),
keepalive: true,
}).catch(() => {});
} catch {
/* ignore */
}
}
render() {
if (this.state.hasError) {
return (
<div
aria-label="Section render error placeholder"
className="w-content-width mx-auto my-8 p-6 card rounded text-center"
>
<p className="text-base font-medium text-foreground">
This section failed to render.
</p>
<p className="mt-1 text-sm text-foreground/60">
Section: <code className="font-mono">{this.props.name}</code>
</p>
{this.state.errorMessage ? (
<p className="mt-2 text-xs text-foreground/50 max-w-xl mx-auto break-words">
{this.state.errorMessage}
</p>
) : null}
<p className="mt-3 text-xs text-foreground/40">
Tell Bob exactly what's wrong (e.g. &quot;fix the {this.props.name} section&quot;) to retry.
</p>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,73 @@
import { useRef, useEffect } from "react";
import { cls } from "@/lib/utils";
type Option = {
value: string;
label: string;
};
interface SelectorButtonProps {
options: Option[];
activeValue: string;
onValueChange: (value: string) => void;
className?: string;
}
const SelectorButton = ({ options, activeValue, onValueChange, className }: SelectorButtonProps) => {
const hoverRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
const hoverElement = hoverRef.current;
if (!container || !hoverElement) return;
const moveHoverBlock = (target: HTMLElement) => {
if (!target) return;
const targetRect = target.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
hoverElement.style.width = `${targetRect.width}px`;
hoverElement.style.transform = `translateX(${targetRect.left - containerRect.left}px)`;
};
const updatePosition = () => {
const activeButton = container.querySelector(`[data-value="${activeValue}"]`) as HTMLElement;
if (activeButton) moveHoverBlock(activeButton);
};
updatePosition();
const resizeObserver = new ResizeObserver(updatePosition);
resizeObserver.observe(container);
return () => {
resizeObserver.disconnect();
};
}, [activeValue]);
return (
<div ref={containerRef} className={cls("relative inline-flex gap-1 p-1 card rounded-full", className)}>
{options.map((option) => (
<button
key={option.value}
data-value={option.value}
onClick={() => onValueChange(option.value)}
className={cls(
"relative z-1 px-5 py-2 text-sm font-medium rounded-full cursor-pointer transition-colors duration-300",
activeValue === option.value ? "text-primary-cta-text" : "text-foreground hover:text-foreground/80"
)}
>
{option.label}
</button>
))}
<div
ref={hoverRef}
className="absolute z-0 inset-y-1 left-0 rounded-full primary-button pointer-events-none transition-all duration-300 ease-out"
/>
</div>
);
};
export default SelectorButton;

View File

@@ -0,0 +1,11 @@
import { cls } from "@/lib/utils";
interface SeparatorProps {
className?: string;
}
const Separator = ({ className = "" }: SeparatorProps) => (
<div className={cls("h-px w-full bg-foreground/10", className)} />
);
export default Separator;

View File

@@ -0,0 +1,69 @@
"use client";
import { useState, useEffect } from "react";
import type { ReactNode } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X } from "lucide-react";
import { cls } from "@/lib/utils";
interface SheetProps {
trigger: ReactNode;
title: string;
description?: string;
children: ReactNode;
className?: string;
}
const Sheet = ({ trigger, title, description, children, className = "" }: SheetProps) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
if (isOpen) document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen]);
return (
<>
<div onClick={() => setIsOpen(true)} className="cursor-pointer">{trigger}</div>
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 z-50 bg-background/30 backdrop-blur-[1px]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsOpen(false)}
/>
<motion.div
className={cls("fixed z-50 inset-y-0 right-0 card p-3 xl:p-4 2xl:p-5", className)}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
>
<div className="flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 mb-3 xl:mb-4 2xl:mb-5">
<div className="min-w-0">
<h2 className="text-lg font-medium text-foreground truncate">{title}</h2>
{description && <p className="text-sm text-foreground truncate">{description}</p>}
</div>
<button
onClick={() => setIsOpen(false)}
className="shrink-0 flex items-center justify-center size-9 rounded secondary-button text-secondary-cta-text cursor-pointer"
>
<X className="size-4" />
</button>
</div>
{children}
</motion.div>
</>
)}
</AnimatePresence>
</>
);
};
export default Sheet;

View File

@@ -0,0 +1,32 @@
"use client";
import { useStyle } from "@/components/ui/useStyle";
import AuroraBackground from "@/components/ui/AuroraBackground";
import CornerGlowBackground from "@/components/ui/CornerGlowBackground";
import FloatingGradientBackground from "@/components/ui/FloatingGradientBackground";
import GridLinesBackground from "@/components/ui/GridLinesBackground";
import NoiseBackground from "@/components/ui/NoiseBackground";
import NoiseGradientBackground from "@/components/ui/NoiseGradientBackground";
const SiteBackgroundSlot = () => {
const { siteBackground } = useStyle();
switch (siteBackground) {
case "aurora":
return <AuroraBackground position="fixed" />;
case "cornerGlow":
return <CornerGlowBackground position="fixed" />;
case "floatingGradient":
return <FloatingGradientBackground position="fixed" />;
case "gridLines":
return <GridLinesBackground position="fixed" />;
case "noise":
return <NoiseBackground position="fixed" />;
case "noiseGradient":
return <NoiseGradientBackground position="fixed" />;
default:
return null;
}
};
export default SiteBackgroundSlot;

View File

@@ -0,0 +1,47 @@
import type { LucideIcon } from "lucide-react";
import { resolveIcon } from "@/utils/resolve-icon";
import { cls } from "@/lib/utils";
type SocialLink = {
icon: string | LucideIcon;
href: string;
};
interface SocialLinksProps {
links: SocialLink[];
size?: "sm" | "base" | "lg";
className?: string;
}
const sizeStyles = {
sm: "size-8",
base: "size-9",
lg: "size-10",
};
const SocialLinks = ({ links, size = "base", className }: SocialLinksProps) => (
<div className={cls("flex gap-3", className)}>
{links.map((link, index) => {
const IconComponent = resolveIcon(link.icon);
return (
<a
key={index}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className={cls(
"flex items-center justify-center primary-button rounded",
sizeStyles[size]
)}
>
<IconComponent
className="h-2/5 w-2/5 text-primary-cta-text"
strokeWidth={1.5}
/>
</a>
);
})}
</div>
);
export default SocialLinks;

View File

@@ -0,0 +1,24 @@
import { cls } from "@/lib/utils";
interface SpinnerProps {
size?: "sm" | "md" | "lg";
className?: string;
}
const SIZES = {
sm: "size-4 border-1",
md: "size-6 border-2",
lg: "size-8 border-2",
};
const Spinner = ({ size = "md", className = "" }: SpinnerProps) => (
<div
className={cls(
"animate-spin rounded-full border-foreground/10 border-t-foreground",
SIZES[size],
className
)}
/>
);
export default Spinner;

View File

@@ -0,0 +1,32 @@
"use client";
import { type ReactNode } from "react";
import {
StyleContext,
type ButtonVariant,
type HeroBackgroundVariant,
type SiteBackgroundVariant,
} from "@/components/ui/useStyle";
interface StyleProviderProps {
buttonVariant?: ButtonVariant;
siteBackground?: SiteBackgroundVariant;
heroBackground?: HeroBackgroundVariant;
children: ReactNode;
}
export function StyleProvider({
buttonVariant = "default",
siteBackground = "none",
heroBackground = "none",
children,
}: StyleProviderProps) {
return (
<StyleContext.Provider value={{ buttonVariant, siteBackground, heroBackground }}>
{children}
</StyleContext.Provider>
);
}
export default StyleProvider;

View File

@@ -0,0 +1,30 @@
"use client";
import { cls } from "@/lib/utils";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}
const Switch = ({ checked, onChange, disabled = false, className = "" }: SwitchProps) => (
<div
onClick={() => !disabled && onChange(!checked)}
className={cls(
"relative flex items-center h-5 aspect-2/1 secondary-button rounded-full cursor-pointer",
disabled && "opacity-50 cursor-not-allowed",
className
)}
>
<div
className={cls(
"absolute left-0 top-1/2 -translate-y-1/2 h-full aspect-square rounded-full primary-button transition-all duration-300",
checked ? "translate-x-5 opacity-100" : "opacity-50"
)}
/>
</div>
);
export default Switch;

17
src/components/ui/Tag.tsx Normal file
View File

@@ -0,0 +1,17 @@
import type { LucideIcon } from "lucide-react";
import { cls } from "@/lib/utils";
interface TagProps {
text: string;
icon?: LucideIcon;
className?: string;
}
const Tag = ({ text, icon: Icon, className = "" }: TagProps) => (
<div className={cls("flex items-center gap-1 px-3 py-1 text-sm card rounded w-fit", className)}>
{Icon && <Icon className="h-(--text-sm) w-auto" />}
<p>{text}</p>
</div>
);
export default Tag;

View File

@@ -0,0 +1,96 @@
import { useState, useEffect } from "react";
import { motion } from "motion/react";
import { cls } from "@/lib/utils";
type Variant = "slide-up" | "fade-blur" | "fade";
interface TextAnimationProps {
text: string;
variant: Variant;
gradientText: boolean;
tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div";
className?: string;
}
const VARIANTS = {
"slide-up": {
hidden: { opacity: 0, y: "50%" },
visible: { opacity: 1, y: 0 },
},
"fade-blur": {
hidden: { opacity: 0, filter: "blur(10px)" },
visible: { opacity: 1, filter: "none" },
},
"fade": {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
};
const EASING: Record<Variant, [number, number, number, number]> = {
"slide-up": [0.25, 0.46, 0.45, 0.94],
"fade-blur": [0.45, 0, 0.55, 1],
"fade": [0.45, 0, 0.55, 1],
};
const TextAnimation = ({ text, variant, gradientText, tag = "p", className = "" }: TextAnimationProps) => {
const Tag = motion[tag] as typeof motion.p;
const words = text.split(" ");
const [animationComplete, setAnimationComplete] = useState(false);
const [reverted, setReverted] = useState(false);
const gradientClass = gradientText
? "bg-gradient-to-r from-foreground to-primary-cta bg-clip-text text-transparent pb-[0.1em] -mb-[0.1em]"
: "";
useEffect(() => {
if (animationComplete && !reverted) {
const delay = variant === "fade-blur" && gradientText ? 0 : 700;
const timer = setTimeout(() => {
setReverted(true);
}, delay);
return () => clearTimeout(timer);
}
}, [animationComplete, reverted, variant, gradientText]);
if (reverted) {
return (
<Tag
className={cls("leading-[1.2]", gradientClass, className)}
initial={false}
>
{text}
</Tag>
);
}
return (
<Tag
className={cls(
"leading-[1.2] transition-all duration-700",
animationComplete && gradientClass,
className
)}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-20%" }}
transition={{ staggerChildren: 0.04 }}
onAnimationComplete={() => setAnimationComplete(true)}
>
{words.map((word, i) => (
<span key={i}>
{i > 0 && " "}
<motion.span
className="inline-block"
variants={VARIANTS[variant]}
transition={{ duration: 0.6, ease: EASING[variant] }}
>
{word}
</motion.span>
</span>
))}
</Tag>
);
};
export default TextAnimation;

View File

@@ -0,0 +1,53 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
interface TextBoxProps {
tag?: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
className?: string;
}
const TextBox = ({
tag,
title,
description,
primaryButton,
secondaryButton,
className,
}: TextBoxProps) => (
<div className={`flex flex-col items-center gap-2 w-content-width mx-auto ${className || ""}`}>
{tag && (
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
)}
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h2"
className="md:max-w-8/10 text-6xl 2xl:text-7xl leading-[1.15] font-semibold text-balance text-center"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-7/10 text-lg md:text-xl leading-snug text-balance text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
);
export default TextBox;

View File

@@ -0,0 +1,33 @@
"use client";
import { useButtonClick } from "@/hooks/useButtonClick";
import { cls } from "@/lib/utils";
interface TextLinkProps {
text: string;
href?: string;
onClick?: () => void;
className?: string;
}
const TextLink = ({ text, href = "#", onClick, className = "" }: TextLinkProps) => {
const handleClick = useButtonClick(href, onClick);
return (
<a
href={href}
onClick={handleClick}
className={cls(
"relative text-sm text-foreground cursor-pointer",
"after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-current",
"after:scale-x-0 after:origin-right after:transition-transform after:duration-300",
"hover:after:scale-x-100 hover:after:origin-left",
className
)}
>
{text}
</a>
);
};
export default TextLink;

View File

@@ -0,0 +1,18 @@
import { cls } from "@/lib/utils";
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
className?: string;
}
const Textarea = ({ className = "", ...props }: TextareaProps) => (
<textarea
className={cls(
"w-full min-h-24 px-3 py-2 rounded secondary-button text-secondary-cta-text text-sm",
"placeholder:text-secondary-cta-text/50 focus:outline-none resize-none",
className
)}
{...props}
/>
);
export default Textarea;

View File

@@ -0,0 +1,85 @@
import { useState, useEffect, useRef } from "react";
import { motion } from "motion/react";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type TiltedCarouselProps = {
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
autoPlayInterval?: number;
};
const TiltedCarousel = ({ items, autoPlayInterval = 4000 }: TiltedCarouselProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const [isFirstRender, setIsFirstRender] = useState(true);
const autoPlayRef = useRef<ReturnType<typeof setInterval> | null>(null);
const itemCount = items.length;
useEffect(() => {
if (isFirstRender) {
const timeout = setTimeout(() => setIsFirstRender(false), 800);
return () => clearTimeout(timeout);
}
}, [isFirstRender]);
useEffect(() => {
if (autoPlayRef.current) clearInterval(autoPlayRef.current);
autoPlayRef.current = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % itemCount);
}, autoPlayInterval);
return () => {
if (autoPlayRef.current) clearInterval(autoPlayRef.current);
};
}, [autoPlayInterval, itemCount]);
return (
<div className="relative flex items-center justify-center w-full overflow-hidden">
<div className="w-[70%] md:w-[40%] aspect-square md:aspect-video opacity-0" />
{[-2, -1, 0, 1, 2].map((position) => {
const itemIndex = (activeIndex + position + itemCount) % itemCount;
const item = items[itemIndex];
const isCenter = position === 0;
const distance = Math.abs(position);
const scale = distance === 0 ? 1 : distance === 1 ? 0.88 : 0.8;
const opacity = distance <= 1 ? 1 : 0;
const xPercent = position * 100;
const yPercent = distance === 0 ? 0 : distance === 1 ? 5 : 10;
const rotate = position * 2;
const initialState = distance <= 1 && isFirstRender
? isCenter
? { opacity: 0, y: "25px", scale: 1, x: "0%", rotate: 0 }
: { opacity: 0, scale: 0.88, x: `calc(${xPercent}% + ${position > 0 ? 20 : -20}px)`, y: "5%", rotate }
: { scale, opacity, x: `${xPercent}%`, y: `${yPercent}%`, rotate };
return (
<motion.div
key={itemIndex}
className="absolute w-[70%] md:w-[40%] aspect-square md:aspect-video p-2 xl:p-3 2xl:p-4 card rounded-lg overflow-hidden"
style={{ zIndex: isCenter ? 10 : 5 - distance }}
initial={initialState}
animate={{ scale, opacity, x: `${xPercent}%`, y: `${yPercent}%`, rotate }}
transition={{
duration: 0.8,
ease: [0.65, 0, 0.35, 1],
delay: distance <= 1 && isFirstRender ? (isCenter ? 0.45 : 0.6) : 0,
}}
>
<ImageOrVideo
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
className="w-full h-full rounded-lg object-cover"
/>
<motion.div
className="absolute inset-0 bg-background/50 backdrop-blur-[1px] pointer-events-none"
initial={{ opacity: isCenter ? 0 : 1 }}
animate={{ opacity: isCenter ? 0 : 1 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
</motion.div>
);
})}
</div>
);
};
export default TiltedCarousel;

View File

@@ -0,0 +1,29 @@
import type { LucideIcon } from "lucide-react";
import { cls } from "@/lib/utils";
import { resolveIcon } from "@/utils/resolve-icon";
type Item = { icon: string | LucideIcon; title: string; subtitle: string; detail: string };
const POS = ["-translate-y-14 hover:-translate-y-20", "translate-x-16 hover:-translate-y-4", "translate-x-32 translate-y-16 hover:translate-y-10"];
const TiltedStackCards = ({ items }: { items: [Item, Item, Item] }) => (
<div
className="h-full grid place-items-center [grid-template-areas:'stack']"
style={{ maskImage: "linear-gradient(to bottom, transparent, black 10%, black 90%, transparent), linear-gradient(to right, black, black 80%, transparent)", maskComposite: "intersect" }}
>
{items.map((item, i) => (
<div key={i} className={cls("flex flex-col justify-between gap-2 p-6 w-80 h-36 card rounded transition-all duration-500 -skew-y-[8deg] [grid-area:stack] 2xl:w-90", POS[i])}>
<div className="flex items-center gap-2">
<div className="flex items-center justify-center size-5 rounded primary-button">
{(() => { const Icon = resolveIcon(item.icon); return <Icon className="size-3 text-primary-cta-text" strokeWidth={1.5} />; })()}
</div>
<p className="text-base">{item.title}</p>
</div>
<p className="text-lg whitespace-nowrap">{item.subtitle}</p>
<p className="text-base">{item.detail}</p>
</div>
))}
</div>
);
export default TiltedStackCards;

View File

@@ -0,0 +1,53 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { cls } from "@/lib/utils";
interface TooltipProps {
content: string;
position?: "top" | "bottom" | "left" | "right";
children: React.ReactNode;
className?: string;
}
const POSITIONS = {
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",
};
const Tooltip = ({ content, position = "top", children, className = "" }: TooltipProps) => {
const [isVisible, setIsVisible] = useState(false);
return (
<div
className="relative inline-block"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
<AnimatePresence>
{isVisible && (
<motion.div
className={cls(
"absolute z-50 whitespace-nowrap rounded px-2 py-1 text-xs pointer-events-none",
"secondary-button text-secondary-cta-text",
POSITIONS[position],
className
)}
initial={{ opacity: 0, scale: 0.95, y: position === "bottom" ? -4 : position === "top" ? 4 : 0 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
>
{content}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default Tooltip;

View File

@@ -0,0 +1,37 @@
import { motion } from "motion/react";
import type { ReactNode } from "react";
interface TransitionProps {
children: ReactNode;
className?: string;
transitionType?: "full" | "fade";
whileInView?: boolean;
}
const Transition = ({
children,
className = "flex flex-col w-full gap-6",
transitionType = "full",
whileInView = true,
}: TransitionProps) => {
const initial = transitionType === "full"
? { opacity: 0, y: 20 }
: { opacity: 0 };
const target = transitionType === "full"
? { opacity: 1, y: 0 }
: { opacity: 1 };
return (
<motion.div
initial={initial}
{...(whileInView ? { whileInView: target, viewport: { once: true, margin: "-15%" } } : { animate: target })}
transition={{ duration: 0.6, ease: "easeOut" }}
className={className}
>
{children}
</motion.div>
);
};
export default Transition;

View File

@@ -0,0 +1,14 @@
# Background Components
AuroraBackground
ColumnWaveBackground
CornerGlowBackground
FloatingGradientBackground
GradientBarsBackground
GridLinesBackground
HorizonGlowBackground
LightRaysCenterBackground
LightRaysCornerBackground
NoiseBackground
NoiseGradientBackground
RadialGradientBackground

View File

@@ -0,0 +1,54 @@
"use client";
import { createContext, useContext } from "react";
export type ButtonVariant =
| "default"
| "arrow"
| "bounce"
| "bubble"
| "elastic"
| "expand"
| "flip"
| "magnetic"
| "pill"
| "shift"
| "slide"
| "stagger";
export type SiteBackgroundVariant =
| "none"
| "aurora"
| "cornerGlow"
| "floatingGradient"
| "gridDots"
| "gridLines"
| "noise"
| "noiseGradient";
export type HeroBackgroundVariant =
| "none"
| "lightRaysCenter"
| "lightRaysCorner"
| "gradientBars"
| "radialGradient"
| "cornerGlow"
| "horizonGlow";
export interface StyleContextValue {
buttonVariant: ButtonVariant;
siteBackground: SiteBackgroundVariant;
heroBackground: HeroBackgroundVariant;
}
export const DEFAULT_STYLE_VALUE: StyleContextValue = {
buttonVariant: "default",
siteBackground: "none",
heroBackground: "none",
};
export const StyleContext = createContext<StyleContextValue>(DEFAULT_STYLE_VALUE);
export function useStyle(): StyleContextValue {
return useContext(StyleContext);
}