233 lines
5.5 KiB
TypeScript
233 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { ReactNode } from "react";
|
|
import Image from "next/image";
|
|
import { motion, Variants } from "framer-motion";
|
|
import { C } from "./palette";
|
|
|
|
export { C };
|
|
|
|
const viewport = { once: true, amount: 0.2 } as const;
|
|
|
|
/* Shared smooth easing — a soft, premium ease-out curve reused across reveals. */
|
|
const smoothEase = [0.22, 1, 0.36, 1] as const;
|
|
|
|
/* stagger container variants */
|
|
export const stagger: Variants = {
|
|
hidden: {},
|
|
show: { transition: { staggerChildren: 0.11, delayChildren: 0.06 } },
|
|
};
|
|
export const riseItem: Variants = {
|
|
hidden: { opacity: 0, y: 34, scale: 0.96 },
|
|
show: {
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: { duration: 0.7, ease: smoothEase },
|
|
},
|
|
};
|
|
|
|
/* ------- Section ------- */
|
|
type SectionProps = {
|
|
id: string;
|
|
children: ReactNode;
|
|
variant?: "cream" | "deep" | "night" | "plain";
|
|
className?: string;
|
|
};
|
|
|
|
export function Section({ id, children, variant = "deep", className = "" }: SectionProps) {
|
|
const bg =
|
|
variant === "cream"
|
|
? "bg-cream text-[#0a0a0a]"
|
|
: variant === "night"
|
|
? "bg-night text-white"
|
|
: variant === "plain"
|
|
? "bg-[#120a1f] text-white"
|
|
: "bg-deep text-white";
|
|
return (
|
|
<section
|
|
id={id}
|
|
className={`relative m-5 flex min-h-[calc(100vh-40px)] flex-col justify-center overflow-hidden rounded-[20px] px-6 py-20 sm:px-10 lg:px-20 ${bg} ${className}`}
|
|
>
|
|
<div className="mx-auto w-full max-w-6xl">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/* ------- Reveal (fade/slide up on scroll) ------- */
|
|
export function Reveal({
|
|
children,
|
|
className = "",
|
|
delay = 0,
|
|
y = 26,
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
delay?: number;
|
|
y?: number;
|
|
}) {
|
|
return (
|
|
<motion.div
|
|
className={className}
|
|
initial={{ opacity: 0, y }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={viewport}
|
|
transition={{ duration: 0.7, ease: smoothEase, delay }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
/* ------- Group (stagger children that use riseItem) ------- */
|
|
export function Group({
|
|
children,
|
|
className = "",
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<motion.div
|
|
className={className}
|
|
variants={stagger}
|
|
initial="hidden"
|
|
whileInView="show"
|
|
viewport={{ once: true, amount: 0.15 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
/* ------- Pill ------- */
|
|
export function Pill({
|
|
children,
|
|
bg = C.pink,
|
|
text = "#fff",
|
|
}: {
|
|
children: ReactNode;
|
|
bg?: string;
|
|
text?: string;
|
|
}) {
|
|
return (
|
|
<motion.span
|
|
className="nb inline-flex items-center gap-2.5 rounded-full px-6 py-3 text-lg font-extrabold uppercase tracking-wide sm:px-8 sm:py-4 sm:text-2xl"
|
|
style={{ background: bg, color: text }}
|
|
initial={{ opacity: 0, scale: 0.8, y: 12 }}
|
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
|
viewport={viewport}
|
|
transition={{ type: "spring", stiffness: 320, damping: 15 }}
|
|
whileHover={{ rotate: -2, scale: 1.04 }}
|
|
>
|
|
{children}
|
|
</motion.span>
|
|
);
|
|
}
|
|
|
|
/* ------- Card (scroll-reveal + neobrutalist hover) ------- */
|
|
export function Card({
|
|
children,
|
|
bg = "#fff",
|
|
text = "#0a0a0a",
|
|
className = "",
|
|
rounded = "rounded-2xl",
|
|
delay = 0,
|
|
group = false,
|
|
}: {
|
|
children: ReactNode;
|
|
bg?: string;
|
|
text?: string;
|
|
className?: string;
|
|
rounded?: string;
|
|
delay?: number;
|
|
/** set true when rendered inside a <Group> so stagger drives the entrance */
|
|
group?: boolean;
|
|
}) {
|
|
const entrance = group
|
|
? { variants: riseItem }
|
|
: {
|
|
initial: { opacity: 0, y: 30, scale: 0.96 },
|
|
whileInView: { opacity: 1, y: 0, scale: 1 },
|
|
viewport,
|
|
transition: { duration: 0.7, ease: smoothEase, delay },
|
|
};
|
|
return (
|
|
<motion.div
|
|
className={`nb ${rounded} ${className}`}
|
|
style={{ background: bg, color: text }}
|
|
{...entrance}
|
|
whileHover={{
|
|
y: -6,
|
|
x: -3,
|
|
boxShadow: "11px 13px 0 0 #0a0a0a",
|
|
transition: { type: "spring", stiffness: 400, damping: 15 },
|
|
}}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
/* ------- Chip ------- */
|
|
export function Chip({
|
|
children,
|
|
bg = C.lime,
|
|
text = "#0a0a0a",
|
|
delay = 0,
|
|
}: {
|
|
children: ReactNode;
|
|
bg?: string;
|
|
text?: string;
|
|
delay?: number;
|
|
}) {
|
|
return (
|
|
<motion.span
|
|
className="nb-sm inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm font-extrabold sm:px-4"
|
|
style={{ background: bg, color: text }}
|
|
initial={{ opacity: 0, scale: 0.85, y: 10 }}
|
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
|
viewport={viewport}
|
|
transition={{ type: "spring", stiffness: 300, damping: 16, delay }}
|
|
whileHover={{ scale: 1.06, rotate: -1.5 }}
|
|
>
|
|
{children}
|
|
</motion.span>
|
|
);
|
|
}
|
|
|
|
/* ------- Heading (self-animating) ------- */
|
|
export function Heading({
|
|
children,
|
|
className = "",
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<motion.h2
|
|
className={`font-sora text-4xl font-extrabold leading-[0.95] tracking-tight sm:text-5xl lg:text-6xl ${className}`}
|
|
initial={{ opacity: 0, y: 28 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={viewport}
|
|
transition={{ duration: 0.75, ease: smoothEase }}
|
|
>
|
|
{children}
|
|
</motion.h2>
|
|
);
|
|
}
|
|
|
|
/* LytsUp wordmark */
|
|
export function Logo({ className = "" }: { className?: string }) {
|
|
return (
|
|
<Image
|
|
src="/images/lytsup_app.png"
|
|
alt="LytsUp"
|
|
width={2048}
|
|
height={554}
|
|
priority
|
|
className={`h-10 w-auto sm:h-12 ${className}`}
|
|
/>
|
|
);
|
|
}
|