Initial commit

This commit is contained in:
2026-02-09 18:23:51 +02:00
commit 3872dbd01c
656 changed files with 77397 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
"use client";
import { memo } from "react";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type GridCardItem = {
name: string;
icon: LucideIcon;
};
interface Bento3DCardGridProps {
useInvertedBackground: InvertedBackground;
items: [GridCardItem, GridCardItem, GridCardItem, GridCardItem];
centerIcon: LucideIcon;
className?: string;
}
const gridItemStyle = {
perspective: '1000px',
transformStyle: 'preserve-3d' as const,
};
const EmptyCell = () => (
<div
className="relative aspect-square card shadow rounded-theme-capped opacity-50"
style={gridItemStyle}
/>
);
const cardTranslateZ = [
'group-hover:[transform:translateZ(10px)]',
'group-hover:[transform:translateZ(14px)]',
'group-hover:[transform:translateZ(18px)]',
'group-hover:[transform:translateZ(22px)]',
] as const;
const CardCell = ({ name, Icon, cardIndex }: { name: string; Icon: LucideIcon; cardIndex: number }) => (
<div
className={cls(
"relative card shadow aspect-square rounded-theme-capped flex flex-col justify-between p-3 transition-transform duration-500",
cardTranslateZ[cardIndex]
)}
style={gridItemStyle}
>
<div className="h-6 w-[var(--height-6)] aspect-square rounded-theme primary-button flex items-center justify-center">
<Icon className="h-4/10 w-4/10 text-background" strokeWidth={1.5} />
</div>
<p className="text-xs text-foreground leading-tight line-clamp-4">{name}</p>
</div>
);
const CenterCell = ({ Icon }: { Icon: LucideIcon }) => (
<div
className="aspect-square flex items-center justify-center bg-transparent border-none overflow-visible"
style={gridItemStyle}
>
<div className="card shadow rounded-full h-6/10 aspect-square flex items-center justify-center">
<Icon className="h-4/10 w-4/10 text-foreground" strokeWidth={1.25} />
</div>
</div>
);
const Bento3DCardGrid = ({
useInvertedBackground,
items,
centerIcon: CenterIcon,
className = "",
}: Bento3DCardGridProps) => {
void useInvertedBackground;
const gridPositions = [
{ type: 'empty' },
{ type: 'card', index: 0 },
{ type: 'empty' },
{ type: 'card', index: 1 },
{ type: 'center' },
{ type: 'card', index: 2 },
{ type: 'empty' },
{ type: 'card', index: 3 },
{ type: 'empty' },
] as const;
return (
<div
className={cls("group w-full h-full", className)}
style={{
maskImage: 'linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%), linear-gradient(to bottom, transparent 0%, black 5%, black 95%, transparent 100%)',
maskComposite: 'intersect',
WebkitMaskImage: 'linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%), linear-gradient(to bottom, transparent 0%, black 5%, black 95%, transparent 100%)',
WebkitMaskComposite: 'source-in',
}}
>
<div
className="w-full h-full grid grid-cols-3 gap-4 -translate-y-9 -translate-x-8"
style={{
gridAutoRows: '1fr',
perspective: '5000px',
transformStyle: 'preserve-3d',
transform: 'rotateX(45deg) rotateY(20deg) rotate(-25deg) scale(1.1)',
}}
>
{gridPositions.map((pos, index) => {
switch (pos.type) {
case 'card':
const item = items[pos.index];
return <CardCell key={index} name={item.name} Icon={item.icon} cardIndex={pos.index} />;
case 'center':
return <CenterCell key={index} Icon={CenterIcon} />;
default:
return <EmptyCell key={index} />;
}
})}
</div>
</div>
);
};
Bento3DCardGrid.displayName = "Bento3DCardGrid";
export default memo(Bento3DCardGrid);

View File

@@ -0,0 +1,117 @@
"use client";
import { memo } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
interface StackCardProps {
Icon: LucideIcon;
title: string;
subtitle: string;
detail: string;
iconClassName?: string;
titleClassName?: string;
subtitleClassName?: string;
detailClassName?: string;
}
interface Bento3DStackCardProps extends StackCardProps {
className?: string;
useInvertedBackground: InvertedBackground;
}
const StackCard = memo(({
className = "",
Icon,
title,
subtitle,
detail,
iconClassName = "",
titleClassName = "",
subtitleClassName = "",
detailClassName = "",
useInvertedBackground,
}: Bento3DStackCardProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div
className={cls(
"relative flex h-35 w-80 md:w-25 p-6 -skew-y-[8deg] card rounded-theme-capped flex-col justify-between transition-all duration-700",
className
)}
>
<div className="flex items-center gap-2">
<div
className={cls(
"relative h-5 aspect-square primary-button rounded-theme flex items-center justify-center",
iconClassName
)}
>
<Icon className="h-1/2 w-auto aspect-square text-background" strokeWidth={1.5} />
</div>
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
{title}
</p>
</div>
<p className={cls("whitespace-nowrap text-lg", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
{subtitle}
</p>
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", detailClassName)}>
{detail}
</p>
</div>
);
});
StackCard.displayName = "StackCard";
interface Bento3DStackCardsProps {
cards: StackCardProps[];
useInvertedBackground: InvertedBackground;
className?: string;
}
const Bento3DStackCards = ({
cards,
useInvertedBackground,
className = "",
}: Bento3DStackCardsProps) => {
const baseClassNames = [
"[grid-area:stack] -translate-y-14 hover:-translate-y-20",
"[grid-area:stack] translate-x-15 translate-y-0 hover:-translate-y-5",
"[grid-area:stack] translate-x-31 translate-y-15 hover:translate-y-10",
];
const displayCards = cards.slice(0, 3).map((card, index) => ({
...card,
className: `${baseClassNames[index]} ${card.iconClassName || ""}`,
}));
return (
<div
className={cls("h-full grid [grid-template-areas:'stack'] place-items-center opacity-100 animate-in fade-in-0 duration-700", className)}
style={{
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, black 0%, black 80%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, black 0%, black 80%, transparent 100%)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in"
}}
>
{displayCards.map((cardProps, index) => (
<StackCard
key={index}
{...cardProps}
useInvertedBackground={useInvertedBackground}
/>
))}
</div>
);
};
Bento3DStackCards.displayName = "Bento3DStackCards";
export default memo(Bento3DStackCards);

View File

@@ -0,0 +1,97 @@
"use client";
import { memo, Fragment } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type TaskItem = {
icon: LucideIcon;
label: string;
time: string;
};
interface Bento3DTaskListProps {
title: string;
items: TaskItem[];
useInvertedBackground: InvertedBackground;
className?: string;
}
const Bento3DTaskList = ({
title,
items,
useInvertedBackground,
className = "",
}: Bento3DTaskListProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<div
className={cls("h-full w-full flex items-center justify-center", className)}
style={{
perspective: "1200px",
transformStyle: "preserve-3d",
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in"
}}
>
<div
className={cls(
"relative w-80 md:w-25 p-6 card rounded-theme-capped flex flex-col gap-3 translate-x-4 -translate-y-5"
)}
style={{
transform: "rotateX(30deg) rotateY(30deg) rotateZ(-30deg)",
transformStyle: "preserve-3d"
}}
>
<div className="flex items-center gap-2">
<div className="h-[var(--text-base)] w-auto aspect-square rounded-theme primary-button" />
<h3 className={cls("text-base leading-tight", shouldUseLightText ? "text-background" : "text-foreground")}>
{title}
</h3>
</div>
<div className="relative w-full min-w-0 secondary-button rounded-theme-capped flex flex-col p-5 gap-3">
{items.map((item, index) => {
const Icon = item.icon;
return (
<Fragment key={index}>
<div
className={cls(
"w-full min-w-0 flex items-center justify-between gap-3"
)}
>
<div className="w-full min-w-0 flex items-center gap-3">
<div
className="h-6 w-auto aspect-square rounded-theme flex items-center justify-center primary-button"
>
<Icon className="h-4/10 w-4/10 aspect-square text-background" strokeWidth={1.5} />
</div>
<p className={cls("text-sm truncate", shouldUseLightText ? "text-background" : "text-foreground")}>
{item.label}
</p>
</div>
<p className={cls("text-xs text-nowrap", shouldUseLightText ? "text-background/75" : "text-foreground/75")}>
{item.time}
</p>
</div>
{index !== items.length - 1 && (
<div className="h-px bg-background-accent/50" />
)}
</Fragment>
);
})}
</div>
</div>
</div>
);
};
Bento3DTaskList.displayName = "Bento3DTaskList";
export default memo(Bento3DTaskList);

View File

@@ -0,0 +1,77 @@
"use client";
import { memo, useState, useEffect } from "react";
import { cls } from "@/lib/utils";
type BarData = {
defaultHeight: number;
hoverHeight: number;
};
interface BentoAnimatedBarChartProps {
bars?: BarData[];
className?: string;
barClassName?: string;
}
const defaultBars: BarData[] = [
{ defaultHeight: 100, hoverHeight: 40 },
{ defaultHeight: 84, hoverHeight: 100 },
{ defaultHeight: 62, hoverHeight: 75 },
{ defaultHeight: 90, hoverHeight: 50 },
{ defaultHeight: 70, hoverHeight: 90 },
{ defaultHeight: 50, hoverHeight: 60 },
{ defaultHeight: 75, hoverHeight: 85 },
{ defaultHeight: 80, hoverHeight: 70 },
];
const BentoAnimatedBarChart = ({
bars = defaultBars,
className = "",
barClassName = "",
}: BentoAnimatedBarChartProps) => {
const [activeBar, setActiveBar] = useState(2); // Start at third bar (index 2)
useEffect(() => {
const interval = setInterval(() => {
setActiveBar((prev) => (prev + 1) % bars.length);
}, 3000);
return () => clearInterval(interval);
}, [bars.length]);
return (
<div className={cls("group w-full h-full [mask-image:linear-gradient(to_bottom,black_40%,transparent_100%)]", className)}>
<style>{`
.bento-bar {
height: var(--default-height);
}
@media (min-width: 768px) {
.group:hover .bento-bar {
height: var(--hover-height) !important;
}
}
`}</style>
<div className="w-full h-full flex items-end gap-5">
{bars.map((bar, index) => (
<div
key={index}
className={cls("relative bento-bar w-full rounded-theme transition-all duration-500 ease bg-background-accent", barClassName)}
style={
{
"--default-height": `${bar.defaultHeight}%`,
"--hover-height": `${bar.hoverHeight}%`,
} as React.CSSProperties
}
>
<div className={cls("absolute! inset-0 primary-button rounded-theme transition-opacity ease-in-out duration-500", activeBar === index ? "opacity-100" : "opacity-0")} />
</div>
))}
</div>
</div>
);
};
BentoAnimatedBarChart.displayName = "BentoAnimatedBarChart";
export default memo(BentoAnimatedBarChart);

View File

@@ -0,0 +1,96 @@
"use client";
import { memo } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import { Send } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type ChatExchange = {
userMessage: string;
aiResponse: string;
};
interface BentoChatAnimationProps {
aiIcon: LucideIcon;
userIcon: LucideIcon;
exchanges: ChatExchange[];
placeholder: string;
useInvertedBackground: InvertedBackground;
className?: string;
}
const BentoChatAnimation = ({
aiIcon: AiIcon,
userIcon: UserIcon,
exchanges,
placeholder,
useInvertedBackground,
className = "",
}: BentoChatAnimationProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const messages = exchanges.flatMap((exchange) => [
{ content: exchange.userMessage, isUser: true },
{ content: exchange.aiResponse, isUser: false },
]);
const duplicatedMessages = [...messages, ...messages];
return (
<div
className={cls(
"relative h-full w-full flex flex-col overflow-hidden",
className
)}
>
<div className="flex-1 overflow-hidden mask-fade-y">
<div className="flex flex-col animate-marquee-vertical px-4">
{duplicatedMessages.map((message, index) => (
<div
key={index}
className={cls(
"flex items-end gap-2 shrink-0 mb-4",
message.isUser ? "flex-row-reverse" : "flex-row"
)}
>
{message.isUser ? (
<div className="shrink-0 h-8 aspect-square rounded-theme primary-button flex items-center justify-center">
<UserIcon className="h-4/10 w-auto text-background" />
</div>
) : (
<div className="shrink-0 h-8 aspect-square rounded-theme card shadow flex items-center justify-center">
<AiIcon className={cls("h-4/10 w-auto", shouldUseLightText ? "text-background" : "text-foreground")} />
</div>
)}
<div
className={cls(
"max-w-75/100 px-4 py-3 text-sm leading-tight",
message.isUser
? "primary-button rounded-theme-capped rounded-br-none text-background"
: "card rounded-theme-capped rounded-bl-none",
!message.isUser && (shouldUseLightText ? "text-background" : "text-foreground")
)}
>
{message.content}
</div>
</div>
))}
</div>
</div>
<div className="card shadow rounded-theme p-2 pl-5 flex items-center gap-2">
<p className={cls("flex-1 text-sm truncate", shouldUseLightText ? "text-background/75" : "text-foreground/75")}>
{placeholder}
</p>
<div className="h-7 w-auto aspect-square primary-button rounded-theme flex items-center justify-center">
<Send className="h-4/10 w-auto text-background" strokeWidth={1.75} />
</div>
</div>
</div>
);
};
BentoChatAnimation.displayName = "BentoChatAnimation";
export default memo(BentoChatAnimation);

View File

@@ -0,0 +1,204 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { cls } from "@/lib/utils";
import createGlobe, { COBEOptions } from "cobe";
// Helper function to convert CSS color to RGB array
const getRGBFromCSSVar = (varName: string): [number, number, number] => {
if (typeof window === "undefined") return [0.5, 0.5, 0.5];
const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
// Handle CSS named colors by creating a temporary element to get computed RGB
if (value && !value.startsWith("#") && !value.startsWith("rgb") && !value.includes("%") && !value.match(/^\d+\s+\d+\s+\d+$/)) {
const temp = document.createElement("div");
temp.style.color = value;
document.body.appendChild(temp);
const computed = getComputedStyle(temp).color;
document.body.removeChild(temp);
if (computed && computed.startsWith("rgb")) {
const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (match) {
const r = parseInt(match[1]) / 255;
const g = parseInt(match[2]) / 255;
const b = parseInt(match[3]) / 255;
return [r, g, b];
}
}
}
// Handle rgba/rgb format (e.g., "rgba(18, 0, 6, .9)" or "rgb(255, 255, 255)")
if (value.startsWith("rgb")) {
const match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (match) {
const r = parseInt(match[1]) / 255;
const g = parseInt(match[2]) / 255;
const b = parseInt(match[3]) / 255;
return [r, g, b];
}
}
// Handle hex format (e.g., "#ffffff", "#ffffffaa", or shorthand "#fff", "#f0f")
if (value.startsWith("#")) {
let hex = value.replace("#", "");
// Expand shorthand hex (e.g., "93f" -> "9933ff")
if (hex.length === 3 || hex.length === 4) {
hex = hex.split("").map(c => c + c).join("").substring(0, 6);
}
// Take only first 6 characters (ignore alpha channel if present)
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
return [r, g, b];
}
// Handle HSL format (e.g., "0 0% 100%")
if (value.includes("%")) {
const [h, s, l] = value.split(/\s+/).map(v => parseFloat(v));
// Convert HSL to RGB
const sNorm = s / 100;
const lNorm = l / 100;
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = lNorm - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [(r + m), (g + m), (b + m)];
}
// Handle RGB format (e.g., "255 255 255")
const [r, g, b] = value.split(/\s+/).map(v => parseFloat(v) / 255);
return [r || 0.5, g || 0.5, b || 0.5];
};
const getGlobeConfig = (): COBEOptions => ({
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.3,
dark: 0,
diffuse: 0.4,
mapSamples: 16000,
mapBrightness: 1.2,
baseColor: getRGBFromCSSVar("--secondary-cta"),
markerColor: getRGBFromCSSVar("--primary-cta"),
glowColor: getRGBFromCSSVar("--card"),
markers: [
{ location: [14.5995, 120.9842], size: 0.03 },
{ location: [19.076, 72.8777], size: 0.1 },
{ location: [23.8103, 90.4125], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.07 },
{ location: [39.9042, 116.4074], size: 0.08 },
{ location: [-23.5505, -46.6333], size: 0.1 },
{ location: [19.4326, -99.1332], size: 0.1 },
{ location: [40.7128, -74.006], size: 0.1 },
{ location: [34.6937, 135.5022], size: 0.05 },
{ location: [41.0082, 28.9784], size: 0.06 },
],
});
interface GlobeProps {
className?: string;
config?: COBEOptions;
}
const GlobeComponent = ({
className = "",
config,
}: GlobeProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const globeRef = useRef<{ destroy: () => void } | null>(null);
const phiRef = useRef(0);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [globeConfig, setGlobeConfig] = useState<COBEOptions | null>(null);
const onRender = useCallback(
(state: Record<string, number>) => {
phiRef.current += 0.005;
state.phi = phiRef.current;
state.width = dimensions.width * 2;
state.height = dimensions.width * 2;
},
[dimensions]
);
const onResize = useCallback(() => {
if (canvasRef.current) {
const newWidth = canvasRef.current.offsetWidth;
setDimensions(prev => {
if (prev.width === newWidth) return prev;
return { width: newWidth, height: newWidth };
});
}
}, []);
useEffect(() => {
window.addEventListener("resize", onResize);
onResize();
return () => {
window.removeEventListener("resize", onResize);
};
}, [onResize]);
useEffect(() => {
// Initialize globe config with CSS variables
const defaultConfig = getGlobeConfig();
setGlobeConfig(config ? { ...defaultConfig, ...config } : defaultConfig);
}, [config]);
useEffect(() => {
if (!canvasRef.current || dimensions.width === 0 || !globeConfig) return;
if (globeRef.current) {
globeRef.current.destroy();
}
globeRef.current = createGlobe(canvasRef.current, {
...globeConfig,
width: dimensions.width * 2,
height: dimensions.width * 2,
onRender,
});
setTimeout(() => {
if (canvasRef.current) {
canvasRef.current.style.opacity = "1";
}
});
return () => {
if (globeRef.current) {
globeRef.current.destroy();
globeRef.current = null;
}
};
}, [dimensions, globeConfig, onRender]);
return (
<div
className={cls(
"absolute inset-0 mx-auto w-full aspect-square",
className
)}
>
<canvas
className="size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]"
ref={canvasRef}
/>
</div>
);
};
GlobeComponent.displayName = "BentoGlobe";
export const BentoGlobe = React.memo(GlobeComponent);

View File

@@ -0,0 +1,72 @@
"use client";
import { memo } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type BentoInfoItem = {
icon: LucideIcon;
label: string;
value: string;
};
interface BentoIconInfoCardsProps {
items: BentoInfoItem[];
useInvertedBackground: InvertedBackground;
className?: string;
cardClassName?: string;
iconWrapperClassName?: string;
iconClassName?: string;
labelClassName?: string;
valueClassName?: string;
}
const BentoIconInfoCards = ({
items,
useInvertedBackground,
className = "",
cardClassName = "",
iconWrapperClassName = "",
iconClassName = "",
labelClassName = "",
valueClassName = "",
}: BentoIconInfoCardsProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const duplicatedItems = [...items, ...items];
return (
<div className={cls("h-full min-h-0 overflow-hidden mask-fade-y", className)}>
<div className="flex flex-col animate-marquee-vertical px-px">
{duplicatedItems.map((item, index) => {
const Icon = item.icon;
return (
<div
key={index}
className={cls("card shadow rounded-theme-capped p-3 flex items-center justify-between flex-shrink-0 mb-4", cardClassName)}
>
<div className="w-full min-w-0 flex items-center gap-3">
<div className={cls("h-10 w-auto aspect-square rounded-theme flex items-center justify-center secondary-button", iconWrapperClassName)}>
<Icon className={cls("h-4/10 w-4/10 text-foreground", iconClassName)} strokeWidth={1.5} />
</div>
<p className={cls("text-base truncate", shouldUseLightText ? "text-background" : "text-foreground", labelClassName)}>
{item.label}
</p>
</div>
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
{item.value}
</p>
</div>
);
})}
</div>
</div>
);
};
BentoIconInfoCards.displayName = "BentoIconInfoCards";
export default memo(BentoIconInfoCards);

View File

@@ -0,0 +1,145 @@
"use client";
import { memo } from "react";
import {
Area,
AreaChart,
CartesianGrid,
YAxis,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { formatNumber, calculateYAxisWidth, type ChartDataItem } from "./utils";
import CustomTooltip from "./CustomTooltip";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
interface BentoLineChartProps {
data?: ChartDataItem[];
dataKey?: string;
metricLabel?: string;
isPercentage?: boolean;
useInvertedBackground: InvertedBackground;
className?: string;
}
const defaultData: ChartDataItem[] = [
{ value: 120 },
{ value: 180 },
{ value: 150 },
{ value: 280 },
{ value: 220 },
{ value: 350 },
{ value: 300 },
{ value: 250 },
];
const BentoLineChart = memo<BentoLineChartProps>(
({
data = defaultData,
dataKey = "value",
metricLabel = "Value",
isPercentage = false,
useInvertedBackground,
className = "",
}) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const yAxisWidth = calculateYAxisWidth(data, isPercentage);
const strokeColor = "var(--primary-cta)";
const gridColor = "color-mix(in srgb, var(--background-accent) 30%, transparent)";
const tickColor = shouldUseLightText ? "var(--background)" : "var(--foreground)";
return (
<div
className={cls("w-full h-full **:outline-none **:focus:outline-none", className)}
style={{
maskImage: "linear-gradient(to bottom, black 40%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 40%, transparent 100%)",
}}
>
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{
top: 10,
right: 5,
left: 0,
bottom: 14,
}}
>
<defs>
<linearGradient id="bentoLineChartFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={strokeColor} stopOpacity={0.4} />
<stop offset="95%" stopColor={strokeColor} stopOpacity={0} />
</linearGradient>
<linearGradient id="bentoFadeGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="black" stopOpacity={0} />
<stop offset="5%" stopColor="black" stopOpacity={0} />
<stop offset="15%" stopColor="white" stopOpacity={1} />
<stop offset="95%" stopColor="white" stopOpacity={1} />
<stop offset="100%" stopColor="black" stopOpacity={0} />
</linearGradient>
<mask id="bentoFadeMask">
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="url(#bentoFadeGradient)"
/>
</mask>
</defs>
<CartesianGrid
vertical={false}
stroke={gridColor}
strokeWidth={1}
/>
<YAxis
tickLine={false}
axisLine={false}
tick={{
fill: tickColor,
fontSize: 10,
}}
width={yAxisWidth}
tickFormatter={(value) =>
isPercentage ? `${value}%` : formatNumber(value)
}
/>
<Tooltip
content={
<CustomTooltip
metricLabel={metricLabel}
isPercentage={isPercentage}
totalItems={data.length}
/>
}
cursor={{
stroke: gridColor,
}}
/>
<Area
dataKey={dataKey}
type="monotone"
fill="url(#bentoLineChartFill)"
stroke={strokeColor}
strokeWidth={2}
mask="url(#bentoFadeMask)"
activeDot={{
fill: strokeColor,
r: 5,
}}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}
);
BentoLineChart.displayName = "BentoLineChart";
export default BentoLineChart;

View File

@@ -0,0 +1,64 @@
"use client";
import { memo } from "react";
import { formatNumber } from "./utils";
interface CustomTooltipProps {
active?: boolean;
payload?: Array<{
value: number;
color: string;
}>;
label?: number;
metricLabel?: string;
isPercentage?: boolean;
totalItems: number;
}
const CustomTooltip = memo<CustomTooltipProps>(
({
active,
payload,
label = 0,
metricLabel = "Value",
isPercentage = false,
totalItems,
}: CustomTooltipProps) => {
if (active && payload && payload.length) {
const value = isPercentage
? `${payload[0].value}%`
: formatNumber(payload[0].value);
const today = new Date();
const daysAgo = totalItems - 1 - label;
const date = new Date(today);
date.setDate(today.getDate() - daysAgo);
return (
<div className="card rounded-theme-capped p-3">
<p className="text-xs text-foreground mb-2">
{date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</p>
<div className="flex items-center gap-2">
<div
className="h-1.5 aspect-square rounded-full"
style={{
backgroundColor: payload[0].color,
}}
/>
<span className="text-xs text-foreground">
{metricLabel}: {value}
</span>
</div>
</div>
);
}
return null;
}
);
CustomTooltip.displayName = "CustomTooltip";
export default CustomTooltip;

View File

@@ -0,0 +1,33 @@
export const formatNumber = (value: number): string => {
if (value >= 100000) {
const millions = value / 1000000;
return `${millions.toFixed(1)}M`;
}
if (value >= 1000) {
const thousands = value / 1000;
const rounded = Math.round(thousands * 10) / 10;
return `${rounded}K`;
}
return value.toString();
};
export interface ChartDataItem {
value: number;
}
export const calculateYAxisWidth = (
data: ChartDataItem[],
isPercentage: boolean
): number => {
const maxValue = Math.max(...data.map((item) => item.value));
const formattedMax = isPercentage ? `${maxValue}%` : formatNumber(maxValue);
let multiplier = 9;
if (formattedMax.length === 2) {
multiplier = 11;
} else if (formattedMax.length === 3) {
multiplier = 13;
}
return formattedMax.length * multiplier;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
"use client";
import { memo } from "react";
import Marquee from "react-fast-marquee";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type BentoMarqueeProps = {
centerIcon: LucideIcon;
useInvertedBackground: InvertedBackground;
className?: string;
} & (
| { variant: "text"; texts: string[] }
| { variant: "icon"; icons: LucideIcon[] }
);
const BentoMarquee = (props: BentoMarqueeProps) => {
const { centerIcon, useInvertedBackground, className = "" } = props;
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const CenterIcon = centerIcon;
const items = props.variant === "text"
? [...props.texts, ...props.texts]
: [...props.icons, ...props.icons];
return (
<div
className={cls("relative h-full w-full flex flex-col overflow-hidden", className)}
style={{
maskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)",
WebkitMaskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)"
}}
>
<div className="absolute top-1/2 left-1/2 -translate-1/2 h-auto w-full flex flex-col justify-center gap-2 opacity-60">
{Array.from({ length: 10 }).map((_, rowIndex) => (
<Marquee
key={rowIndex}
gradient={false}
speed={10}
direction={rowIndex % 2 === 0 ? "left" : "right"}
>
{items.map((item, itemIndex) => (
<div
key={itemIndex}
className={cls("relative mx-1 card rounded-theme flex items-center justify-center", props.variant === "icon" ? "p-2 aspect-square" : "px-4 py-2")}
>
{props.variant === "text" ? (
<p className={cls("text-sm leading-tight", shouldUseLightText ? "text-background" : "text-foreground")}>{item as string}</p>
) : (
(() => {
const Icon = item as LucideIcon;
return <Icon className={cls("h-1/2 w-1/2", shouldUseLightText ? "text-background" : "text-foreground")} strokeWidth={1.5} />;
})()
)}
</div>
))}
</Marquee>
))}
</div>
<div className="absolute! top-1/2 left-1/2 -translate-1/2 z-10 h-18 w-auto aspect-square primary-button backdrop-blur-xs rounded-theme flex items-center justify-center">
<CenterIcon className="h-4/10 w-4/10 text-background" strokeWidth={1.5} />
</div>
</div>
);
};
BentoMarquee.displayName = "BentoMarquee";
export default memo(BentoMarquee);

View File

@@ -0,0 +1,82 @@
"use client";
import { memo } from "react";
import { cls } from "@/lib/utils";
import MediaContent from "@/components/shared/MediaContent";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type MediaStackItem = {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
};
interface BentoMediaStackProps {
items: [MediaStackItem, MediaStackItem, MediaStackItem];
useInvertedBackground: InvertedBackground;
className?: string;
}
const BentoMediaStack = ({
items,
className = "",
}: BentoMediaStackProps) => {
return (
<div
className={cls("group/stack relative w-full h-full card shadow rounded-theme-capped flex items-center justify-center select-none", className)}
>
<div
className={cls(
"absolute! w-3/5 2xl:w-1/2 aspect-[4/3] p-1 rounded-theme-capped overflow-hidden primary-button z-[1]",
"rotate-8 translate-x-[12%] -translate-y-[8%] transition-all duration-500 ease-out",
"2xl:translate-x-[8%] 2xl:-translate-y-[6%]",
"group-hover/stack:translate-x-[22%] group-hover/stack:rotate-12 group-hover/stack:-translate-y-[14%]",
"2xl:group-hover/stack:translate-x-[16%] 2xl:group-hover/stack:-translate-y-[10%]"
)}
>
<MediaContent
imageSrc={items[2].imageSrc}
videoSrc={items[2].videoSrc}
imageAlt={items[2].imageAlt}
imageClassName="h-full rounded-[calc(var(--radius-theme-capped)*0.95)]!"
/>
</div>
<div
className={cls(
"absolute! w-3/5 2xl:w-1/2 aspect-[4/3] p-1 rounded-theme-capped overflow-hidden primary-button z-[2]",
"-rotate-8 -translate-x-[12%] -translate-y-[8%] transition-all duration-500 ease-out",
"2xl:-translate-x-[8%] 2xl:-translate-y-[6%]",
"group-hover/stack:-translate-x-[22%] group-hover/stack:-rotate-12 group-hover/stack:-translate-y-[14%]",
"2xl:group-hover/stack:-translate-x-[16%] 2xl:group-hover/stack:-translate-y-[10%]"
)}
>
<MediaContent
imageSrc={items[1].imageSrc}
videoSrc={items[1].videoSrc}
imageAlt={items[1].imageAlt}
imageClassName="h-full rounded-[calc(var(--radius-theme-capped)*0.95)]!"
/>
</div>
<div
className={cls(
"absolute! w-3/5 2xl:w-1/2 aspect-[4/3] p-1 rounded-theme-capped overflow-hidden primary-button z-30",
"translate-y-[10%] transition-all duration-500 ease-out",
"2xl:translate-y-[7%]",
"group-hover/stack:translate-y-[20%]",
"2xl:group-hover/stack:translate-y-[14%]"
)}
>
<MediaContent
imageSrc={items[0].imageSrc}
videoSrc={items[0].videoSrc}
imageAlt={items[0].imageAlt}
imageClassName="h-full rounded-[calc(var(--radius-theme-capped)*0.95)]!"
/>
</div>
</div>
);
};
BentoMediaStack.displayName = "BentoMediaStack";
export default memo(BentoMediaStack);

View File

@@ -0,0 +1,104 @@
"use client";
import { memo } from "react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type OrbitingItem = {
icon: LucideIcon;
ring?: 1 | 2 | 3; // Which ring to orbit on (1=innermost, 3=outermost), defaults to 2
duration?: number; // Animation duration in seconds, defaults to 10
};
interface BentoOrbitingIconsProps {
centerIcon: LucideIcon;
items: OrbitingItem[];
useInvertedBackground: InvertedBackground;
className?: string;
}
const BentoOrbitingIcons = ({
centerIcon,
items,
useInvertedBackground,
className = "",
}: BentoOrbitingIconsProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const CenterIcon = centerIcon;
const circleStyles = "secondary-button border border-background-accent! shadow rounded-full";
return (
<div
className={cls("relative h-full flex flex-col overflow-hidden", className)}
style={{
perspective: "2000px",
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in"
}}
>
<div
className="flex-1 rounded-t-theme-capped gap-2 flex items-center justify-center w-full h-full inset-x-0 p-2 relative"
style={{
transform: "rotateY(20deg) rotateX(20deg) rotateZ(-20deg)"
}}
>
{/* Background concentric circles */}
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[15rem] w-[15rem] z-[9] opacity-85", circleStyles)} />
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[20rem] w-[20rem] z-[8] opacity-65", circleStyles)} />
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[25rem] w-[25rem] z-[7] opacity-45", circleStyles)} />
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[30rem] w-[30rem] z-[6] opacity-25", circleStyles)} />
{/* Center circle with icon */}
<div className={cls("absolute! inset-0 shrink-0 h-40 w-[10rem] z-10 m-auto flex items-center justify-center", circleStyles)}>
<div className="absolute! primary-button h-[5rem] w-[5rem] rounded-full flex items-center justify-center" >
<CenterIcon className="absolute h-1/2 w-1/2 text-background" strokeWidth={1.25} />
</div>
{/* Orbiting items */}
{items.map((item, index) => {
const Icon = item.icon;
const ring = item.ring || 2;
// Ring radii: 7.5rem=120px, 10rem=160px, 12.5rem=200px
const radiusMap = { 1: 120, 2: 160, 3: 200 };
const radius = radiusMap[ring];
const duration = item.duration || 10;
// Evenly distribute items around the circle
const initialPosition = (360 / items.length) * index;
return (
<div
key={index}
className={cls("!absolute top-1/2 left-1/2 h-[2.5rem] w-[2.5rem] card shadow rounded-theme flex items-center justify-center")}
style={{
marginLeft: '-1.25rem',
marginTop: '-1.25rem',
animation: `orbit ${duration}s linear infinite`,
"--initial-position": `${initialPosition}deg`,
"--translate-position": `${radius}px`,
"--orbit-duration": `${duration}s`,
} as React.CSSProperties & {
"--initial-position": string;
"--translate-position": string;
"--orbit-duration": string;
}}
>
<Icon className={cls("h-4/10 w-4/10", shouldUseLightText ? "text-background" : "text-foreground")} strokeWidth={1.5} />
</div>
);
})}
</div>
</div>
</div>
);
};
BentoOrbitingIcons.displayName = "BentoOrbitingIcons";
export default memo(BentoOrbitingIcons);

View File

@@ -0,0 +1,115 @@
"use client";
import { memo } from "react";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type PhoneApp = {
name: string;
icon: LucideIcon;
};
export type PhoneApps8 = [PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp];
interface BentoPhoneAnimationProps {
statusIcon: LucideIcon;
alertIcon: LucideIcon;
alertTitle: string;
alertMessage: string;
apps: PhoneApps8;
useInvertedBackground: InvertedBackground;
className?: string;
}
const BentoPhoneAnimation = ({
statusIcon: StatusIcon,
alertIcon: AlertIcon,
alertTitle,
alertMessage,
apps,
className = "",
}: BentoPhoneAnimationProps) => {
return (
<div
className={cls(
"group/phone relative h-full flex flex-auto items-center justify-center overflow-hidden cursor-pointer",
className
)}
style={{
maskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
}}
>
<div
className={cls(
"absolute inset-x-0 top-0 h-full overflow-hidden isolate",
"pt-8 transition-[padding] duration-500 ease-out group-hover/phone:pt-0",
)}
>
<div
className={cls(
"relative mx-auto card shadow h-100 w-[calc(100%-var(--vw-2)*2)] rounded-[3vw] p-2",
)}
>
<div className="w-full min-w-0 relative h-full overflow-hidden secondary-button rounded-[2.6vw] p-8 pt-6" >
<div
className="relative z-10 mx-auto h-7 w-auto aspect-square card shadow flex items-center justify-center rounded-full"
>
<StatusIcon className="h-4/10 w-4/10 text-foreground transition-colors duration-300 group-hover/phone:text-primary-cta" />
</div>
<div
className={cls(
"absolute! left-8 right-8 z-2 gap-[0.5vw] p-3 card flex flex-row items-center rounded-theme-capped",
"-translate-y-30 scale-90 blur-[2px] opacity-50",
"transition-all duration-500 ease-out",
"group-hover/phone:translate-y-0 group-hover/phone:scale-100 group-hover/phone:blur-none group-hover/phone:opacity-100",
)}
style={{ top: "calc(var(--vw-1_5) + var(--height-7) + var(--vw-1_5))" }}
>
<div
className={cls(
"relative h-8 w-auto aspect-square primary-button flex shrink-0 items-center justify-center rounded-theme",
)}
>
<AlertIcon className="h-4/10 w-4/10 text-background" />
</div>
<div className="min-w-0 flex flex-col gap-0">
<h3
className={cls(
"text-sm leading-tight text-foreground",
)}
>
{alertTitle}
</h3>
<p
className={cls(
"text-xs text-foreground/75 leading-tight truncate",
)}
>
{alertMessage}
</p>
</div>
</div>
<div className="w-full min-w-0 grid grid-cols-4 gap-6 mt-6">
{apps.map(({ name, icon: Icon }) => (
<div key={name} className="w-full min-w-0 flex flex-col items-center gap-2">
<div className="aspect-square w-full primary-button rounded-theme-capped flex items-center justify-center">
<Icon className="h-2/5 w-2/5 text-background" strokeWidth={1.5} />
</div>
<p className="w-full text-xs text-foreground text-center truncate">
{name}
</p>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
BentoPhoneAnimation.displayName = "BentoPhoneAnimation";
export default memo(BentoPhoneAnimation);

View File

@@ -0,0 +1,83 @@
"use client";
import { memo } from "react";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
interface BentoRevealIconProps {
icon: LucideIcon;
useInvertedBackground: InvertedBackground;
className?: string;
}
const BentoRevealIcon = ({
icon: Icon,
useInvertedBackground,
className = "",
}: BentoRevealIconProps) => {
void useInvertedBackground;
return (
<div
className={cls(
"group relative h-full w-full flex items-center justify-center overflow-hidden",
className
)}
style={{
maskImage: "linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)",
WebkitMaskImage: "linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)",
maskComposite: "intersect",
WebkitMaskComposite: "source-in",
}}
>
<div className="relative h-26 w-[6.5rem]">
<div
className="absolute right-full top-1/2 -mt-48 transition-transform duration-500 ease-out group-hover:-translate-x-12"
style={{ transform: "translateX(calc(52px + 1px - 2px))" }}
>
<div className="relative h-96 aspect-[224/280] -scale-x-100">
<svg viewBox="0 0 224 280" fill="none" className="absolute inset-0 h-full w-full overflow-visible">
<path fill="currentColor" className="text-background-accent/10" d="M8 .25a8 8 0 0 0-8 8v91.704c0 2.258.954 4.411 2.628 5.927l10.744 9.738A7.998 7.998 0 0 1 16 121.546v36.408a7.998 7.998 0 0 1-2.628 5.927l-10.744 9.738A7.998 7.998 0 0 0 0 179.546v92.204a8 8 0 0 0 8 8h308a8 8 0 0 0 8-8V8.25a8 8 0 0 0-8-8H8Z" />
<path stroke="currentColor" className="text-background-accent" d="M.5 99.954V8.25A7.5 7.5 0 0 1 8 .75h308a7.5 7.5 0 0 1 7.5 7.5v263.5a7.5 7.5 0 0 1-7.5 7.5H8a7.5 7.5 0 0 1-7.5-7.5v-92.204a7.5 7.5 0 0 1 2.464-5.557l10.744-9.737a8.5 8.5 0 0 0 2.792-6.298v-36.408a8.5 8.5 0 0 0-2.792-6.298l-10.744-9.737A7.5 7.5 0 0 1 .5 99.954Z" />
</svg>
</div>
</div>
<div
className="absolute left-full top-1/2 -mt-48 transition-transform duration-500 ease-out group-hover:translate-x-12"
style={{ transform: "translateX(calc(-52px - 1px + 2px))" }}
>
<div className="relative h-96 aspect-[224/280]">
<svg viewBox="0 0 224 280" fill="none" className="absolute inset-0 h-full w-full overflow-visible">
<path fill="currentColor" className="text-background-accent/10" d="M8 .25a8 8 0 0 0-8 8v91.704c0 2.258.954 4.411 2.628 5.927l10.744 9.738A7.998 7.998 0 0 1 16 121.546v36.408a7.998 7.998 0 0 1-2.628 5.927l-10.744 9.738A7.998 7.998 0 0 0 0 179.546v92.204a8 8 0 0 0 8 8h308a8 8 0 0 0 8-8V8.25a8 8 0 0 0-8-8H8Z" />
<path stroke="currentColor" className="text-background-accent" d="M.5 99.954V8.25A7.5 7.5 0 0 1 8 .75h308a7.5 7.5 0 0 1 7.5 7.5v263.5a7.5 7.5 0 0 1-7.5 7.5H8a7.5 7.5 0 0 1-7.5-7.5v-92.204a7.5 7.5 0 0 1 2.464-5.557l10.744-9.737a8.5 8.5 0 0 0 2.792-6.298v-36.408a8.5 8.5 0 0 0-2.792-6.298l-10.744-9.737A7.5 7.5 0 0 1 .5 99.954Z" />
</svg>
</div>
</div>
<div className="relative w-full h-full p-2">
<div className="relative w-full h-full primary-button rounded-theme flex items-center justify-center">
<Icon className="relative z-10 h-4/10 w-auto text-background" strokeWidth={1.25} />
</div>
</div>
<div
className="absolute inset-px z-10 rounded-full mix-blend-overlay"
style={{ clipPath: "circle(50%)" }}
>
<div
className="absolute inset-0 z-10 transition-transform duration-500 ease-out group-hover:translate-x-0 group-hover:translate-y-0"
style={{
backgroundImage: "linear-gradient(to bottom right, transparent 30%, black, transparent 70%)",
transform: "translate(-65px, -65px)",
}}
/>
</div>
</div>
</div>
);
};
BentoRevealIcon.displayName = "BentoRevealIcon";
export default memo(BentoRevealIcon);

View File

@@ -0,0 +1,115 @@
"use client";
import { memo } from "react";
import { cls } from "@/lib/utils";
import { Check, Loader } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
export type TimelineItem = {
label: string;
detail: string;
};
interface BentoTimelineProps {
heading: string;
subheading: string;
items: [TimelineItem, TimelineItem, TimelineItem];
completedLabel: string;
useInvertedBackground: InvertedBackground;
className?: string;
}
const itemDelays = [
{ check: 'delay-[150ms]', label: 'delay-[200ms]', detail: 'delay-[250ms]' },
{ check: 'delay-[350ms]', label: 'delay-[400ms]', detail: 'delay-[450ms]' },
{ check: 'delay-[550ms]', label: 'delay-[600ms]', detail: 'delay-[650ms]' },
] as const;
const BentoTimeline = ({
heading,
subheading,
items,
completedLabel,
useInvertedBackground,
className = "",
}: BentoTimelineProps) => {
void useInvertedBackground;
return (
<div
className={cls(
"group relative h-full w-full flex items-center justify-center overflow-hidden",
className
)}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-100" />
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-80" />
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-60" />
</div>
<div className="relative max-w-full min-w-0 flex flex-col gap-3 p-4">
<div className="card shadow rounded-theme-capped p-3 flex items-center gap-2">
<Loader className="h-[var(--text-sm)] w-auto text-primary transition-transform duration-1000 ease-out group-hover:rotate-[360deg]" strokeWidth={1.5} />
<p className="text-xs text-foreground truncate">{heading}</p>
<p className="text-xs text-foreground/75 ml-auto text-nowrap">{subheading}</p>
</div>
{items.map((item, index) => (
<div
key={index}
className="card shadow rounded-theme-capped px-3 py-2 flex items-center gap-2"
>
<div className="relative h-6 w-auto aspect-square card shadow rounded-theme flex items-center justify-center">
<div className="absolute h-3/10 w-3/10 primary-button rounded-theme transition-opacity duration-300 group-hover:opacity-0" />
<div
className={cls(
"absolute! inset-0 rounded-theme primary-button flex items-center justify-center",
"opacity-0 scale-75 transition-all duration-300",
`group-hover:opacity-100 group-hover:scale-100 ${itemDelays[index].check}`
)}
>
<Check className="h-1/2 w-1/2 text-background" strokeWidth={2} />
</div>
</div>
<div className="w-full min-w-0 max-w-full flex-1 flex items-center gap-10 justify-between">
<p
className={cls(
"text-xs text-foreground truncate opacity-0 transition-all duration-300",
`group-hover:opacity-100 ${itemDelays[index].label}`
)}
>
{item.label}
</p>
<p
className={cls(
"text-xs text-foreground/75 text-nowrap opacity-0 translate-y-1 transition-all duration-300",
`group-hover:opacity-100 group-hover:translate-y-0 ${itemDelays[index].detail}`
)}
>
{item.detail}
</p>
</div>
</div>
))}
<div className="primary-button rounded-theme-capped p-3 flex items-center justify-center">
<div className="absolute flex gap-2 transition-opacity duration-500 delay-[900ms] group-hover:opacity-0">
{[0, 1, 2].map((i) => (
<div key={i} className="h-2 w-auto aspect-square rounded-theme bg-background" />
))}
</div>
<p
className="text-xs text-background truncate opacity-0 transition-opacity duration-500 delay-[900ms] group-hover:opacity-100"
>
{completedLabel}
</p>
</div>
</div>
</div>
);
};
BentoTimeline.displayName = "BentoTimeline";
export default memo(BentoTimeline);