Skip to content

Commit 341839b

Browse files
committed
feat: add Badge component and implement MomentumLines and FlashlightTabs animations
1 parent e63ec73 commit 341839b

File tree

6 files changed

+366
-31
lines changed

6 files changed

+366
-31
lines changed

src/components/animations/component-preview.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { cn } from "@/lib/utils";
2+
import { ExternalLink } from "lucide-react";
23
import React, { useState } from "react";
34

45
interface ComponentPreviewProps {
56
children: React.ReactNode;
67
height?: number;
78
notReady?: boolean;
89
className?: string;
10+
href?: string;
911
}
1012

1113
const ComponentPreview = ({
1214
children,
1315
height,
1416
notReady,
1517
className,
18+
href,
1619
}: ComponentPreviewProps) => {
1720
const [minHeight] = useState<number>(500);
1821

@@ -23,14 +26,32 @@ const ComponentPreview = ({
2326
<div className="w-full border-y relative flex items-center justify-center">
2427
<div
2528
className={cn(
26-
"max-w-screen-lg w-full flex items-center justify-center relative p-2 bg-muted border-x",
27-
className,
29+
"max-w-screen-lg w-full flex items-center justify-center relative p-2 bg-muted border-x group",
30+
className
2831
)}
2932
style={{
30-
height: `${Math.max(100, height ? minHeight + height : minHeight)}px`,
33+
height: `${Math.max(
34+
100,
35+
height ? minHeight + height : minHeight
36+
)}px`,
3137
}}
3238
>
3339
{children}
40+
{href && (
41+
<a
42+
href={href}
43+
target="_blank"
44+
className={cn(
45+
"absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 border border-t-0 border-r-0 bg-red-50 px-2 py-0.5",
46+
"flex items-center gap-2"
47+
)}
48+
>
49+
<p className="flex items-center gap-2 text-sm">
50+
<span>Inspiration </span>
51+
<ExternalLink className="size-4 " />
52+
</p>
53+
</a>
54+
)}
3455
</div>
3556
</div>
3657
</div>

src/components/animations/light.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client";
2+
3+
import { motion } from "motion/react";
4+
import { Fragment, useState } from "react";
5+
6+
const tabs = [
7+
{ id: "home", label: "Home" },
8+
{ id: "blog", label: "Blog" },
9+
{ id: "about", label: "About" },
10+
{ id: "work", label: "Work" },
11+
{ id: "contact", label: "Contact" },
12+
];
13+
14+
const FlashlightTabs = () => {
15+
const [activeTab, setActiveTab] = useState(0);
16+
17+
return (
18+
<article className="grid aspect-video place-items-center rounded-xl border border-white/10 bg-white/5">
19+
<nav className="relative isolate">
20+
<ul className="flex overflow-hidden rounded-full border border-white/30 bg-text-light-accent p-1.5">
21+
{tabs.map((tab, index) => (
22+
<li key={tab.id} className="group relative isolate">
23+
<button
24+
onClick={() => setActiveTab(index)}
25+
className={`relative z-10 px-4 py-2 text-sm font-medium transition-all duration-300 ${
26+
activeTab === index
27+
? "text-white [text-shadow:rgba(255,255,255,0.5)_1px_1px_12px]"
28+
: "text-white/70 hover:text-white"
29+
}`}
30+
>
31+
{tab.label}
32+
</button>
33+
{activeTab === index && (
34+
<Fragment>
35+
<motion.div
36+
layoutId="active-pill"
37+
className="absolute inset-0 rounded-full bg-white/5 group-first-of-type:rounded-l-3xl group-first-of-type:rounded-r-md group-last-of-type:rounded-l-md group-last-of-type:rounded-r-3xl"
38+
style={{ zIndex: -1 }}
39+
transition={{
40+
type: "spring",
41+
duration: 0.7,
42+
}}
43+
/>
44+
<motion.div
45+
layoutId="active-shade"
46+
className="absolute -bottom-[90px] left-0 h-[100px] w-full rounded-full bg-white/10 blur-[7px]"
47+
style={{ zIndex: -2 }}
48+
transition={{
49+
type: "spring",
50+
duration: 0.7,
51+
}}
52+
/>
53+
</Fragment>
54+
)}
55+
</li>
56+
))}
57+
</ul>
58+
<div className="absolute -top-px z-10 h-px w-full bg-linear-to-r from-transparent from-20% via-white/60 via-50% to-transparent to-80%"></div>
59+
60+
<div className="absolute inset-0 -bottom-px -z-10 overflow-hidden rounded-full">
61+
<motion.div
62+
className="absolute inset-0 w-1/5"
63+
animate={{
64+
x: `${100 * activeTab}%`,
65+
}}
66+
transition={{
67+
type: "spring",
68+
duration: 0.7,
69+
}}
70+
>
71+
<div className="h-full w-full scale-x-150 bg-linear-to-r from-transparent via-white/60 via-40% to-transparent"></div>
72+
</motion.div>
73+
</div>
74+
</nav>
75+
</article>
76+
);
77+
};
78+
79+
export default FlashlightTabs;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
import { motion } from "motion/react";
3+
import { MouseEvent, useRef } from "react";
4+
5+
const THRESHOLD = 200;
6+
const ARRAY_LENGTH = 17;
7+
8+
export default function MomentumLines() {
9+
const containerRef = useRef<HTMLDivElement>(null);
10+
11+
const onMouseMove = (e: MouseEvent) => {
12+
if (!containerRef.current) return;
13+
const mouseX = e.nativeEvent.offsetX;
14+
const containerWidth = containerRef.current.offsetWidth;
15+
const lines = containerRef.current.querySelectorAll(".line");
16+
17+
lines.forEach((line, index) => {
18+
const lineDiv = line as HTMLDivElement;
19+
const relativeLeft =
20+
lineDiv.offsetLeft - containerRef.current!.offsetLeft;
21+
const xPercentage = mouseX / containerWidth;
22+
const linePercentage = relativeLeft / containerWidth;
23+
const gapPercentage = Math.abs(xPercentage - linePercentage);
24+
const isLastOrFirst = !index || index === lines.length - 1;
25+
const threshold = isLastOrFirst ? THRESHOLD / 2 : THRESHOLD;
26+
27+
const gapDiff = Math.min(
28+
Math.max(mouseX - relativeLeft, -threshold),
29+
threshold
30+
);
31+
32+
lineDiv.style.setProperty(
33+
"--x-position",
34+
`${gapDiff * (1 - gapPercentage)}px`
35+
);
36+
lineDiv.style.setProperty("--opacity-value", `${1 - gapPercentage}`);
37+
});
38+
};
39+
40+
const onMouseOut = () => {
41+
const lines = containerRef.current?.querySelectorAll(".line");
42+
lines?.forEach((line) => {
43+
const lineDiv = line as HTMLDivElement;
44+
lineDiv.style.setProperty("--x-position", "0px");
45+
lineDiv.style.setProperty("--opacity-value", "1");
46+
});
47+
};
48+
49+
return (
50+
<article className="grid aspect-video place-items-center rounded-xl border border-white/10 bg-white/5">
51+
<div
52+
onMouseMove={onMouseMove}
53+
onMouseOut={onMouseOut}
54+
className="flex h-full w-full justify-between p-10"
55+
ref={containerRef}
56+
>
57+
{[...new Array(ARRAY_LENGTH).fill(0)].map((_, index) => (
58+
<motion.div
59+
key={index}
60+
className="line pointer-events-none h-full w-px bg-white"
61+
animate={{
62+
x: "var(--x-position, 0px)",
63+
opacity: "var(--opacity-value, 1)",
64+
}}
65+
transition={{
66+
type: "spring",
67+
duration: 0.8,
68+
bounce: 0,
69+
}}
70+
/>
71+
))}
72+
</div>
73+
</article>
74+
);
75+
}

src/components/animations/toolbar.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// "use client";
2+
3+
// import { useRef, useState } from "react";
4+
// import { DocumentIcon, HomeIcon } from "@heroicons/react/24/outline";
5+
// import WorkIcon from "@/app/_components/Icons/WorkIcon";
6+
// import PlaygroundIcon from "@/app/_components/Icons/PlaygroundIcon";
7+
// import clsx from "clsx";
8+
9+
// interface NavLink {
10+
// name: string;
11+
// slug: string;
12+
// icon: React.ReactNode;
13+
// }
14+
15+
// const navLinks: NavLink[] = [
16+
// { name: "Home", slug: "/", icon: <HomeIcon /> },
17+
// { name: "Work", slug: "/work", icon: <WorkIcon /> },
18+
// { name: "Colophon", slug: "/colophon", icon: <DocumentIcon /> },
19+
// { name: "Playground", slug: "/playground", icon: <PlaygroundIcon /> },
20+
// ];
21+
22+
// interface TooltipSetting {
23+
// left: number;
24+
// x: number;
25+
// width: number;
26+
// offsetLeft: number;
27+
// id: string | null;
28+
// }
29+
30+
// export default function SpatialTooltip() {
31+
// const [tooltipSetting, setTooltipSetting] = useState<TooltipSetting>({
32+
// left: 0,
33+
// x: 0,
34+
// width: 0,
35+
// offsetLeft: 0,
36+
// id: null,
37+
// });
38+
// const toolTipRef = useRef<HTMLDivElement>(null);
39+
40+
// const handleMouseEnter = (index: number) => {
41+
// const listItems = toolTipRef.current?.querySelectorAll("li");
42+
// if (listItems) {
43+
// const itemWidth = listItems[index].clientWidth;
44+
// const offsetLeft = -listItems[index].offsetLeft;
45+
// const x = (itemWidth - 36) / 2; //36 is the size of the button
46+
47+
// setTooltipSetting({
48+
// left: (index / navLinks.length) * 100,
49+
// x: -x,
50+
// width: itemWidth,
51+
// offsetLeft,
52+
// id: navLinks[index].slug,
53+
// });
54+
// }
55+
// };
56+
57+
// const handleOnMouseLeave = () => {
58+
// setTooltipSetting({
59+
// ...tooltipSetting,
60+
// id: null,
61+
// });
62+
// };
63+
64+
// return (
65+
// <article className="relative grid min-h-96 place-items-center rounded-xl border border-white/10 bg-white/5 p-3 md:aspect-8/3 md:min-h-0">
66+
// <nav className="grid place-items-center text-black">
67+
// <div className="relative isolate">
68+
// <div
69+
// ref={toolTipRef}
70+
// className="absolute bottom-[calc(100%+10px)] overflow-hidden rounded-2xl bg-black/50 transition-all duration-300"
71+
// style={{
72+
// left: `${tooltipSetting.left}%`,
73+
// transform: `translateX(${tooltipSetting.x}px)`,
74+
// width: `${tooltipSetting.width}px`,
75+
// opacity: tooltipSetting.id ? 1 : 0,
76+
// }}
77+
// >
78+
// <ul
79+
// className="flex transition-all duration-300"
80+
// style={{
81+
// transform: `translateX(${tooltipSetting.offsetLeft}px)`,
82+
// }}
83+
// >
84+
// {navLinks.map(({ name, slug }) => (
85+
// <li key={slug} className="relative isolate grid px-3 py-2">
86+
// <span
87+
// className={clsx(
88+
// "text-white transition-all duration-300",
89+
// tooltipSetting.id === slug ? "delay-75" : "blur-xs"
90+
// )}
91+
// >
92+
// {name}
93+
// </span>
94+
// </li>
95+
// ))}
96+
// </ul>
97+
// </div>
98+
// <ul className="flex">
99+
// {navLinks.map(({ icon, name, slug }, index) => (
100+
// <li key={slug} className="grid">
101+
// <button
102+
// className="group relative inline-block size-9 rounded-xl p-2 text-white transition duration-300 hover:bg-background-medium-accent hover:text-white"
103+
// onMouseEnter={() => handleMouseEnter(index)}
104+
// onMouseLeave={handleOnMouseLeave}
105+
// >
106+
// {icon}
107+
// <span className="sr-only">{name}</span>
108+
// </button>
109+
// </li>
110+
// ))}
111+
// </ul>
112+
113+
// <div className="absolute -inset-2 -z-10 rounded-[20px] bg-black/50"></div>
114+
// </div>
115+
// </nav>
116+
// </article>
117+
// );
118+
// }

src/components/ui/badge.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const badgeVariants = cva(
7+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13+
secondary:
14+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15+
destructive:
16+
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17+
outline: "text-foreground",
18+
},
19+
},
20+
defaultVariants: {
21+
variant: "default",
22+
},
23+
}
24+
)
25+
26+
export interface BadgeProps
27+
extends React.HTMLAttributes<HTMLDivElement>,
28+
VariantProps<typeof badgeVariants> {}
29+
30+
function Badge({ className, variant, ...props }: BadgeProps) {
31+
return (
32+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
33+
)
34+
}
35+
36+
export { Badge, badgeVariants }

0 commit comments

Comments
 (0)