Damn, I like this website!
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://nyxbui.design/schema.json",
|
||||||
"style": "default",
|
"style": "miami",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
7495
package-lock.json
generated
7495
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,18 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cobe": "^0.6.3",
|
||||||
|
"framer-motion": "^11.2.12",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-spring": "^9.7.3",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,8 +1,15 @@
|
|||||||
|
import Features from "@/components/features";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Header from "@/components/header";
|
||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
|
<Header />
|
||||||
|
<Features />
|
||||||
|
<Footer />
|
||||||
|
<Navigation /> {/* At the bottom of the page */}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/components/features.tsx
Normal file
10
src/components/features.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import TextReveal from "./ui/text-reveal";
|
||||||
|
|
||||||
|
export default function Features() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TextReveal text="Zen will change the way you browse the web. 🌟" />
|
||||||
|
TODO: Features here!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/components/footer.tsx
Normal file
12
src/components/footer.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Logo from "./logo";
|
||||||
|
import TextReveal from "./ui/text-reveal";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<div className="font-medium border-t w-full border-grey py-10 mt-10 flex justify-center align-center">
|
||||||
|
Zen Browser © {new Date().getFullYear()} -
|
||||||
|
Made with ❤️ by the Zen team.
|
||||||
|
<a className="ml-2 font-bold" href="https://github.com/zen-browser" target="_blank">Source Code</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/components/header.tsx
Normal file
71
src/components/header.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ny } from "@/lib/utils";
|
||||||
|
import AnimatedGridPattern from "./ui/animated-grid-pattern";
|
||||||
|
import AnimatedGradientText from "./ui/animated-gradient-text";
|
||||||
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import WordPullUp from "./ui/word-pull-up";
|
||||||
|
import ShinyButton from "./ui/shiny-button";
|
||||||
|
import GridPattern from "./ui/grid-pattern";
|
||||||
|
import BlurIn from "./ui/blur-in";
|
||||||
|
import { FadeText } from "./ui/fade-text";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
return (
|
||||||
|
<div className="w-full relative flex h-screen justify-center flex-col align-center">
|
||||||
|
<GridPattern
|
||||||
|
numSquares={30}
|
||||||
|
maxOpacity={0.5}
|
||||||
|
height={50}
|
||||||
|
width={50}
|
||||||
|
duration={3}
|
||||||
|
repeatDelay={1}
|
||||||
|
x={-1}
|
||||||
|
y={-1}
|
||||||
|
strokeDasharray="4 2"
|
||||||
|
className={ny(
|
||||||
|
'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]',
|
||||||
|
'w-full z-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="z-10 flex mb-10 items-center justify-center">
|
||||||
|
<AnimatedGradientText>
|
||||||
|
🎉
|
||||||
|
{' '}
|
||||||
|
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />
|
||||||
|
{' '}
|
||||||
|
<span
|
||||||
|
className={ny(
|
||||||
|
`inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Introducing Zen Beta
|
||||||
|
</span>
|
||||||
|
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||||
|
</AnimatedGradientText>
|
||||||
|
</div>
|
||||||
|
<WordPullUp
|
||||||
|
className="text-3xl font-bold tracking-[-0.02em] text-black dark:text-white md:text-7xl md:leading-[5rem]"
|
||||||
|
words="Make It Yours"
|
||||||
|
/>
|
||||||
|
<BlurIn
|
||||||
|
word="Firefox based browser with a focus on privacy and customization."
|
||||||
|
className="!text-xl text-muted-foreground !font-medium"
|
||||||
|
/>
|
||||||
|
<div className="max-w-1/4 mt-10 flex items-center justify-center">
|
||||||
|
<a href="/docs" className="mr-5">
|
||||||
|
<FadeText
|
||||||
|
className="text-md font-medium text-black dark:text-white"
|
||||||
|
direction="up"
|
||||||
|
framerProps={{
|
||||||
|
show: { transition: { delay: 0.2 } },
|
||||||
|
}}
|
||||||
|
text="Fade Up"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ShinyButton text="Download now" />
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="absolute bottom-5 left-1/2 size-7 mb-10 animate-bounce" style={{
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
src/components/logo.tsx
Normal file
6
src/components/logo.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export default function Logo() {
|
||||||
|
return (
|
||||||
|
<img src="/logo.png" alt="Zen Logo" className="w-12 h-12" />
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/components/navigation.tsx
Normal file
123
src/components/navigation.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
import { ny } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from "@/components/ui/navigation-menu"
|
||||||
|
import Logo from "./logo"
|
||||||
|
|
||||||
|
const components: { title: string; href: string; description: string }[] = [
|
||||||
|
{
|
||||||
|
title: "Privacy Policy",
|
||||||
|
href: "/privacy-policy",
|
||||||
|
description: "Learn how we handle your data. Don't worry, we don't collect anything!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Discord",
|
||||||
|
href: "https://discord.gg/nnShMQzR4b",
|
||||||
|
description: "Join our Discord server to chat with the community.",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Navigation() {
|
||||||
|
return (
|
||||||
|
<div className="absolute z-10 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
||||||
|
<NavigationMenu>
|
||||||
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
||||||
|
<li className="row-span-3">
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<a
|
||||||
|
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<Logo />
|
||||||
|
<div className="mb-2 mt-4 text-lg font-medium">
|
||||||
|
Zen Browser
|
||||||
|
</div>
|
||||||
|
<p className="text-sm leading-tight text-muted-foreground">
|
||||||
|
Firefox based browser with a focus on privacy and
|
||||||
|
customization.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
<ListItem href="/download" title="Download">
|
||||||
|
Start using Zen Browser today with just a few clicks.
|
||||||
|
</ListItem>
|
||||||
|
<ListItem href="https://github.com/zen-browser" title="Source Code" target="_blank">
|
||||||
|
View the source code on GitHub and maybe leave a star!
|
||||||
|
</ListItem>
|
||||||
|
<ListItem href="/release-notes" title="Release Notes">
|
||||||
|
Stay up to date with the latest changes.
|
||||||
|
</ListItem>
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>Useful Links</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||||
|
{components.map((component) => (
|
||||||
|
<ListItem
|
||||||
|
key={component.title}
|
||||||
|
title={component.title}
|
||||||
|
href={component.href}
|
||||||
|
>
|
||||||
|
{component.description}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<Link href="/docs" legacyBehavior passHref>
|
||||||
|
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||||
|
Documentation
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = React.forwardRef<
|
||||||
|
React.ElementRef<"a">,
|
||||||
|
React.ComponentPropsWithoutRef<"a">
|
||||||
|
>(({ className, title, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<a
|
||||||
|
ref={ref}
|
||||||
|
className={ny(
|
||||||
|
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium leading-none">{title}</div>
|
||||||
|
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ListItem.displayName = "ListItem"
|
||||||
23
src/components/ui/animated-gradient-text.tsx
Normal file
23
src/components/ui/animated-gradient-text.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
export default function AnimatedGradientText({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ny(
|
||||||
|
'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
146
src/components/ui/animated-grid-pattern.tsx
Normal file
146
src/components/ui/animated-grid-pattern.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useEffect, useId, useRef, useState } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface GridPatternProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
strokeDasharray?: any
|
||||||
|
numSquares?: number
|
||||||
|
className?: string
|
||||||
|
maxOpacity?: number
|
||||||
|
duration?: number
|
||||||
|
repeatDelay?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GridPattern({
|
||||||
|
width = 40,
|
||||||
|
height = 40,
|
||||||
|
x = -1,
|
||||||
|
y = -1,
|
||||||
|
strokeDasharray = 0,
|
||||||
|
numSquares = 50,
|
||||||
|
className,
|
||||||
|
maxOpacity = 0.5,
|
||||||
|
duration = 4,
|
||||||
|
repeatDelay = 0.5,
|
||||||
|
...props
|
||||||
|
}: GridPatternProps) {
|
||||||
|
const id = useId()
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||||
|
const [squares, setSquares] = useState(() => generateSquares(numSquares))
|
||||||
|
|
||||||
|
function getPos() {
|
||||||
|
return [
|
||||||
|
Math.floor((Math.random() * dimensions.width) / width),
|
||||||
|
Math.floor((Math.random() * dimensions.height) / height),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||||
|
function generateSquares(count: number) {
|
||||||
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
pos: getPos(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update a single square's position
|
||||||
|
const updateSquarePosition = (id: number) => {
|
||||||
|
setSquares(currentSquares =>
|
||||||
|
currentSquares.map(sq =>
|
||||||
|
sq.id === id
|
||||||
|
? {
|
||||||
|
...sq,
|
||||||
|
pos: getPos(),
|
||||||
|
}
|
||||||
|
: sq,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update squares to animate in
|
||||||
|
useEffect(() => {
|
||||||
|
if (dimensions.width && dimensions.height)
|
||||||
|
setSquares(generateSquares(numSquares))
|
||||||
|
}, [dimensions, numSquares])
|
||||||
|
|
||||||
|
// Resize observer to update container dimensions
|
||||||
|
useEffect(() => {
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
setDimensions({
|
||||||
|
width: entry.contentRect.width,
|
||||||
|
height: entry.contentRect.height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (containerRef.current)
|
||||||
|
resizeObserver.observe(containerRef.current)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (containerRef.current)
|
||||||
|
resizeObserver.unobserve(containerRef.current)
|
||||||
|
}
|
||||||
|
}, [containerRef])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={containerRef}
|
||||||
|
aria-hidden="true"
|
||||||
|
className={ny(
|
||||||
|
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id={id}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={`M.5 ${height}V.5H${width}`}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
||||||
|
<svg x={x} y={y} className="overflow-visible">
|
||||||
|
{squares.map(({ pos: [x, y], id }, index) => (
|
||||||
|
<motion.rect
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: maxOpacity }}
|
||||||
|
transition={{
|
||||||
|
duration,
|
||||||
|
repeat: 1,
|
||||||
|
delay: index * 0.1,
|
||||||
|
repeatType: 'reverse',
|
||||||
|
}}
|
||||||
|
onAnimationComplete={() => updateSquarePosition(id)}
|
||||||
|
key={`${x}-${y}-${index}`}
|
||||||
|
width={width - 1}
|
||||||
|
height={height - 1}
|
||||||
|
x={x * width + 1}
|
||||||
|
y={y * height + 1}
|
||||||
|
fill="currentColor"
|
||||||
|
strokeWidth="0"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GridPattern
|
||||||
39
src/components/ui/animated-shiny-text.tsx
Normal file
39
src/components/ui/animated-shiny-text.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { CSSProperties, FC, ReactNode } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface AnimatedShinyTextProps {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
shimmerWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
shimmerWidth = 100,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--shimmer-width': `${shimmerWidth}px`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
className={ny(
|
||||||
|
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',
|
||||||
|
|
||||||
|
// Shimmer effect
|
||||||
|
'animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||||
|
|
||||||
|
// Shimmer gradient
|
||||||
|
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimatedShinyText
|
||||||
37
src/components/ui/blur-in.tsx
Normal file
37
src/components/ui/blur-in.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface BlurIntProps {
|
||||||
|
word: string
|
||||||
|
className?: string
|
||||||
|
variant?: {
|
||||||
|
hidden: { filter: string, opacity: number }
|
||||||
|
visible: { filter: string, opacity: number }
|
||||||
|
}
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
function BlurIn({ word, className, variant, duration = 1 }: BlurIntProps) {
|
||||||
|
const defaultVariants = {
|
||||||
|
hidden: { filter: 'blur(10px)', opacity: 0 },
|
||||||
|
visible: { filter: 'blur(0px)', opacity: 1 },
|
||||||
|
}
|
||||||
|
const combinedVariants = variant || defaultVariants
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.h1
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
transition={{ duration }}
|
||||||
|
variants={combinedVariants}
|
||||||
|
className={ny(
|
||||||
|
className,
|
||||||
|
'font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
</motion.h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlurIn
|
||||||
55
src/components/ui/dot-pattern.tsx
Normal file
55
src/components/ui/dot-pattern.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface DotPatternProps {
|
||||||
|
width?: any
|
||||||
|
height?: any
|
||||||
|
x?: any
|
||||||
|
y?: any
|
||||||
|
cx?: any
|
||||||
|
cy?: any
|
||||||
|
cr?: any
|
||||||
|
className?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
export function DotPattern({
|
||||||
|
width = 16,
|
||||||
|
height = 16,
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
cx = 1,
|
||||||
|
cy = 1,
|
||||||
|
cr = 1,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DotPatternProps) {
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={ny(
|
||||||
|
'pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id={id}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
patternContentUnits="userSpaceOnUse"
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
>
|
||||||
|
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DotPattern
|
||||||
60
src/components/ui/fade-text.tsx
Normal file
60
src/components/ui/fade-text.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Variants } from 'framer-motion'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
interface FadeTextProps {
|
||||||
|
className?: string
|
||||||
|
direction?: 'up' | 'down' | 'left' | 'right'
|
||||||
|
framerProps?: Variants
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FadeText({
|
||||||
|
direction = 'up',
|
||||||
|
className,
|
||||||
|
framerProps = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: { opacity: 1, transition: { type: 'spring' } },
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
}: FadeTextProps) {
|
||||||
|
const directionOffset = useMemo(() => {
|
||||||
|
const map = { up: 10, down: -10, left: -10, right: 10 }
|
||||||
|
return map[direction]
|
||||||
|
}, [direction])
|
||||||
|
|
||||||
|
const axis = direction === 'up' || direction === 'down' ? 'y' : 'x'
|
||||||
|
|
||||||
|
const FADE_ANIMATION_VARIANTS = useMemo(() => {
|
||||||
|
const { hidden, show, ...rest } = framerProps as {
|
||||||
|
[name: string]: { [name: string]: number, opacity: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
hidden: {
|
||||||
|
...(hidden ?? {}),
|
||||||
|
opacity: hidden?.opacity ?? 0,
|
||||||
|
[axis]: hidden?.[axis] ?? directionOffset,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
...(show ?? {}),
|
||||||
|
opacity: show?.opacity ?? 1,
|
||||||
|
[axis]: show?.[axis] ?? 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [directionOffset, axis, framerProps])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
viewport={{ once: true }}
|
||||||
|
variants={FADE_ANIMATION_VARIANTS}
|
||||||
|
>
|
||||||
|
<motion.span className={className}>{text}</motion.span>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
src/components/ui/gradual-spacing.tsx
Normal file
44
src/components/ui/gradual-spacing.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Variants } from 'framer-motion'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface GradualSpacingProps {
|
||||||
|
text: string
|
||||||
|
duration?: number
|
||||||
|
delayMultiple?: number
|
||||||
|
framerProps?: Variants
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GradualSpacing({
|
||||||
|
text,
|
||||||
|
duration = 0.5,
|
||||||
|
delayMultiple = 0.04,
|
||||||
|
framerProps = {
|
||||||
|
hidden: { opacity: 0, x: -20 },
|
||||||
|
visible: { opacity: 1, x: 0 },
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
}: GradualSpacingProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-1">
|
||||||
|
<AnimatePresence>
|
||||||
|
{text.split('').map((char, i) => (
|
||||||
|
<motion.h1
|
||||||
|
key={i}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={framerProps}
|
||||||
|
transition={{ duration, delay: i * delayMultiple }}
|
||||||
|
className={ny('drop-shadow-sm ', className)}
|
||||||
|
>
|
||||||
|
{char === ' ' ? <span> </span> : char}
|
||||||
|
</motion.h1>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
71
src/components/ui/grid-pattern.tsx
Normal file
71
src/components/ui/grid-pattern.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface GridPatternProps {
|
||||||
|
width?: any
|
||||||
|
height?: any
|
||||||
|
x?: any
|
||||||
|
y?: any
|
||||||
|
squares?: Array<[x: number, y: number]>
|
||||||
|
strokeDasharray?: any
|
||||||
|
className?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GridPattern({
|
||||||
|
width = 40,
|
||||||
|
height = 40,
|
||||||
|
x = -1,
|
||||||
|
y = -1,
|
||||||
|
strokeDasharray = 0,
|
||||||
|
squares,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: GridPatternProps) {
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={ny(
|
||||||
|
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id={id}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={`M.5 ${height}V.5H${width}`}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||||
|
{squares && (
|
||||||
|
<svg x={x} y={y} className="overflow-visible">
|
||||||
|
{squares.map(([x, y]) => (
|
||||||
|
<rect
|
||||||
|
strokeWidth="0"
|
||||||
|
key={`${x}-${y}`}
|
||||||
|
width={width - 1}
|
||||||
|
height={height - 1}
|
||||||
|
x={x * width + 1}
|
||||||
|
y={y * height + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GridPattern
|
||||||
128
src/components/ui/navigation-menu.tsx
Normal file
128
src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { ny } from "@/lib/utils"
|
||||||
|
|
||||||
|
const NavigationMenu = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={ny(
|
||||||
|
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<NavigationMenuViewport />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
))
|
||||||
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const NavigationMenuList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={ny(
|
||||||
|
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||||
|
|
||||||
|
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={ny(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDown
|
||||||
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const NavigationMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={ny(
|
||||||
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||||
|
|
||||||
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className={ny("absolute left-0 top-full flex justify-center")}>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
className={ny(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
NavigationMenuViewport.displayName =
|
||||||
|
NavigationMenuPrimitive.Viewport.displayName
|
||||||
|
|
||||||
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
ref={ref}
|
||||||
|
className={ny(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
))
|
||||||
|
NavigationMenuIndicator.displayName =
|
||||||
|
NavigationMenuPrimitive.Indicator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
}
|
||||||
78
src/components/ui/pulsating-button.tsx
Normal file
78
src/components/ui/pulsating-button.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface PulsatingButtonProps {
|
||||||
|
text: string
|
||||||
|
pulseColor: string
|
||||||
|
backgroundColor: string
|
||||||
|
textColor: string
|
||||||
|
animationDuration: string
|
||||||
|
buttonWidth: string
|
||||||
|
buttonHeight: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||||
|
text,
|
||||||
|
pulseColor,
|
||||||
|
backgroundColor,
|
||||||
|
textColor,
|
||||||
|
animationDuration,
|
||||||
|
buttonWidth,
|
||||||
|
buttonHeight,
|
||||||
|
}) => {
|
||||||
|
const pulseKeyframes = {
|
||||||
|
'--tw-pulse-color': pulseColor,
|
||||||
|
'animation': `pulse ${animationDuration} linear infinite`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="relative block text-center cursor-pointer flex justify-center items-center"
|
||||||
|
style={{
|
||||||
|
color: textColor,
|
||||||
|
backgroundColor,
|
||||||
|
width: buttonWidth,
|
||||||
|
height: buttonHeight,
|
||||||
|
borderRadius: '12px',
|
||||||
|
...pulseKeyframes,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{text}</div>
|
||||||
|
<style jsx>
|
||||||
|
{`
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(var(--tw-pulse-color), 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: inherit;
|
||||||
|
animation: inherit;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PulsatingButton
|
||||||
77
src/components/ui/radial-gradient.tsx
Normal file
77
src/components/ui/radial-gradient.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
|
type Type = 'circle' | 'ellipse'
|
||||||
|
|
||||||
|
type Origin =
|
||||||
|
| 'center'
|
||||||
|
| 'top'
|
||||||
|
| 'bottom'
|
||||||
|
| 'left'
|
||||||
|
| 'right'
|
||||||
|
| 'top left'
|
||||||
|
| 'top right'
|
||||||
|
| 'bottom left'
|
||||||
|
| 'bottom right'
|
||||||
|
|
||||||
|
interface RadialProps {
|
||||||
|
/**
|
||||||
|
* The type of radial gradient
|
||||||
|
* @default circle
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
type?: Type
|
||||||
|
/**
|
||||||
|
* The color to transition from
|
||||||
|
* @default #00000000
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
from?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color to transition to
|
||||||
|
* @default #290A5C
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
to?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the gradient in pixels
|
||||||
|
* @default 300
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
size?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The origin of the gradient
|
||||||
|
* @default center
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
origin?: Origin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class name to apply to the gradient
|
||||||
|
* @default ""
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadialGradient({
|
||||||
|
type = 'circle',
|
||||||
|
from = 'rgba(120,119,198,0.3)',
|
||||||
|
to = 'hsla(0, 0%, 0%, 0)',
|
||||||
|
size = 300,
|
||||||
|
origin = 'center',
|
||||||
|
className,
|
||||||
|
}: RadialProps) {
|
||||||
|
const styles: CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
inset: 0,
|
||||||
|
backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={className} style={styles} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RadialGradient
|
||||||
32
src/components/ui/retro-grid.tsx
Normal file
32
src/components/ui/retro-grid.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
export default function RetroGrid({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ny(
|
||||||
|
'pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="absolute inset-0 [transform:rotateX(35deg)]">
|
||||||
|
<div
|
||||||
|
className={ny(
|
||||||
|
'animate-grid',
|
||||||
|
|
||||||
|
'[background-repeat:repeat] [background-size:60px_60px] [height:300vh] [inset:0%_0px] [margin-left:-50%] [transform-origin:100%_0_0] [width:600vw]',
|
||||||
|
|
||||||
|
// Light Styles
|
||||||
|
'[background-image:linear-gradient(to_right,rgba(0,0,0,0.3)_1px,transparent_0),linear-gradient(to_bottom,rgba(0,0,0,0.3)_1px,transparent_0)]',
|
||||||
|
|
||||||
|
// Dark styles
|
||||||
|
'dark:[background-image:linear-gradient(to_right,rgba(255,255,255,0.2)_1px,transparent_0),linear-gradient(to_bottom,rgba(255,255,255,0.2)_1px,transparent_0)]',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Background Gradient */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
src/components/ui/shine-border.tsx
Normal file
66
src/components/ui/shine-border.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
type TColorProp = `#${string}` | `#${string}`[]
|
||||||
|
interface ShineBorderProps {
|
||||||
|
borderRadius?: number
|
||||||
|
borderWidth?: number
|
||||||
|
duration?: number
|
||||||
|
color?: TColorProp
|
||||||
|
className?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Shine Border
|
||||||
|
* @description It is an animated background border effect component with easy to use and configurable props.
|
||||||
|
* @param borderRadius defines the radius of the border.
|
||||||
|
* @param borderWidth defines the width of the border.
|
||||||
|
* @param duration defines the animation duration to be applied on the shining border
|
||||||
|
* @param color a string or string array to define border color.
|
||||||
|
* @param className defines the class name to be applied to the component
|
||||||
|
* @param children contains react node elements.
|
||||||
|
*/
|
||||||
|
export default function ShineBorder({
|
||||||
|
borderRadius = 8,
|
||||||
|
borderWidth = 1,
|
||||||
|
duration = 14,
|
||||||
|
color = '#fff',
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: ShineBorderProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--border-radius': `${borderRadius}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={ny(
|
||||||
|
'relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--border-width': `${borderWidth}px`,
|
||||||
|
'--border-radius': `${borderRadius}px`,
|
||||||
|
'--border-radius-child': `${borderRadius * 0.2}px`,
|
||||||
|
'--shine-pulse-duration': `${duration}s`,
|
||||||
|
'--mask-linear-gradient': `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||||
|
'--background-radial-gradient': `radial-gradient(transparent,transparent, ${
|
||||||
|
!Array.isArray(color) ? color : color.join(',')
|
||||||
|
},transparent,transparent)`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={`before:bg-shine-size before:absolute before:inset-[0] before:aspect-square before:h-full before:w-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:var(--background-radial-gradient)] before:[background-size:300%_300%] before:[mask:var(--mask-linear-gradient)] motion-safe:before:animate-[shine-pulse_var(--shine-pulse-duration)_infinite_linear]`}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div className="z-[1] h-full w-full rounded-[--border-radius-child]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
src/components/ui/shiny-button.tsx
Normal file
52
src/components/ui/shiny-button.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
'use client'
|
||||||
|
import { type AnimationProps, motion } from 'framer-motion'
|
||||||
|
|
||||||
|
const animationProps = {
|
||||||
|
initial: { '--x': '100%', 'scale': 0.8 },
|
||||||
|
animate: { '--x': '-100%', 'scale': 1 },
|
||||||
|
whileTap: { scale: 0.95 },
|
||||||
|
transition: {
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: 'loop',
|
||||||
|
repeatDelay: 1,
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 20,
|
||||||
|
damping: 15,
|
||||||
|
mass: 2,
|
||||||
|
scale: {
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 5,
|
||||||
|
mass: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as AnimationProps
|
||||||
|
|
||||||
|
function ShinyButton({ text = 'shiny-button' }) {
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
{...animationProps}
|
||||||
|
className="relative rounded-lg px-6 py-2 font-medium backdrop-blur-xl transition-[box-shadow] duration-300 ease-in-out hover:shadow dark:bg-[radial-gradient(circle_at_50%_0%,hsl(var(--primary)/10%)_0%,transparent_60%)] dark:hover:shadow-[0_0_20px_hsl(var(--primary)/10%)]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="relative block h-full w-full text-sm uppercase tracking-wide text-[rgb(0,0,0,65%)] dark:font-light dark:text-[rgb(255,255,255,90%)]"
|
||||||
|
style={{
|
||||||
|
maskImage:
|
||||||
|
'linear-gradient(-75deg,hsl(var(--primary)) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),hsl(var(--primary)) calc(var(--x) + 100%))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
mask: 'linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box,linear-gradient(rgb(0,0,0), rgb(0,0,0))',
|
||||||
|
maskComposite: 'exclude',
|
||||||
|
}}
|
||||||
|
className="absolute inset-0 z-10 block rounded-[inherit] bg-[linear-gradient(-75deg,hsl(var(--primary)/10%)_calc(var(--x)+20%),hsl(var(--primary)/50%)_calc(var(--x)+25%),hsl(var(--primary)/10%)_calc(var(--x)+100%))] p-px"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShinyButton
|
||||||
64
src/components/ui/text-reveal.tsx
Normal file
64
src/components/ui/text-reveal.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||||
|
import type { FC, ReactNode } from 'react'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface TextRevealByWordProps {
|
||||||
|
text: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextRevealByWord: FC<TextRevealByWordProps> = ({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const targetRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: targetRef,
|
||||||
|
})
|
||||||
|
const words = text.split(' ')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={targetRef} className={ny('relative z-0 h-[200vh]', className)}>
|
||||||
|
<div className="sticky top-0 mx-auto flex h-[50%] max-w-4xl items-center bg-transparent px-[1rem] py-[5rem]">
|
||||||
|
<p
|
||||||
|
ref={targetRef}
|
||||||
|
className="flex flex-wrap p-5 text-2xl font-bold text-black/20 dark:text-white/20 md:p-8 md:text-3xl lg:p-10 lg:text-4xl xl:text-5xl"
|
||||||
|
>
|
||||||
|
{words.map((word, i) => {
|
||||||
|
const start = i / words.length
|
||||||
|
const end = start + 1 / words.length
|
||||||
|
return (
|
||||||
|
<Word key={i} progress={scrollYProgress} range={[start, end]}>
|
||||||
|
{word}
|
||||||
|
</Word>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WordProps {
|
||||||
|
children: ReactNode
|
||||||
|
progress: any
|
||||||
|
range: [number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Word: FC<WordProps> = ({ children, progress, range }) => {
|
||||||
|
const opacity = useTransform(progress, range, [0, 1])
|
||||||
|
return (
|
||||||
|
<span className="xl:lg-3 relative mx-1 lg:mx-2.5">
|
||||||
|
<span className="absolute opacity-30">{children}</span>
|
||||||
|
<motion.span style={{ opacity }} className="text-black dark:text-white">
|
||||||
|
{children}
|
||||||
|
</motion.span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextRevealByWord
|
||||||
46
src/components/ui/typing-animation.tsx
Normal file
46
src/components/ui/typing-animation.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface TypingAnimationProps {
|
||||||
|
text: string
|
||||||
|
duration?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TypingAnimation({
|
||||||
|
text,
|
||||||
|
duration = 200,
|
||||||
|
className,
|
||||||
|
}: TypingAnimationProps) {
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>('')
|
||||||
|
const [i, setI] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const typingEffect = setInterval(() => {
|
||||||
|
if (i < text.length) {
|
||||||
|
setDisplayedText(text.substring(0, i + 1))
|
||||||
|
setI(i + 1)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
clearInterval(typingEffect)
|
||||||
|
}
|
||||||
|
}, duration)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(typingEffect)
|
||||||
|
}
|
||||||
|
}, [duration, i])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
className={ny(
|
||||||
|
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{displayedText || text}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
58
src/components/ui/wavy-text.tsx
Normal file
58
src/components/ui/wavy-text.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface WavyTextProps {
|
||||||
|
word: string
|
||||||
|
className?: string
|
||||||
|
variant?: {
|
||||||
|
hidden: { y: number }
|
||||||
|
visible: { y: number }
|
||||||
|
}
|
||||||
|
duration?: number
|
||||||
|
delay?: number
|
||||||
|
}
|
||||||
|
function WavyText({
|
||||||
|
word,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
duration = 0.5,
|
||||||
|
delay = 0.05,
|
||||||
|
}: WavyTextProps) {
|
||||||
|
const defaultVariants = {
|
||||||
|
hidden: { y: 10 },
|
||||||
|
visible: { y: -10 },
|
||||||
|
}
|
||||||
|
const combinedVariants = variant || defaultVariants
|
||||||
|
const characters = useMemo(() => word.split(''), [word])
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center space-x-2 overflow-hidden p-3">
|
||||||
|
<AnimatePresence>
|
||||||
|
{characters.map((char, i) => (
|
||||||
|
<motion.h1
|
||||||
|
key={i}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
yoyo: Infinity,
|
||||||
|
duration,
|
||||||
|
delay: i * delay,
|
||||||
|
}}
|
||||||
|
className={ny(
|
||||||
|
className,
|
||||||
|
'font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</motion.h1>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WavyText
|
||||||
53
src/components/ui/word-pull-up.tsx
Normal file
53
src/components/ui/word-pull-up.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Variants } from 'framer-motion'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ny } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface WordPullUpProps {
|
||||||
|
words: string
|
||||||
|
delayMultiple?: number
|
||||||
|
wrapperFramerProps?: Variants
|
||||||
|
framerProps?: Variants
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WordPullUp({
|
||||||
|
words,
|
||||||
|
wrapperFramerProps = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
framerProps = {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
show: { y: 0, opacity: 1 },
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
}: WordPullUpProps) {
|
||||||
|
return (
|
||||||
|
<motion.h1
|
||||||
|
variants={wrapperFramerProps}
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
className={ny(
|
||||||
|
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{words.split(' ').map((word, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
variants={framerProps}
|
||||||
|
style={{ display: 'inline-block', paddingRight: '8px' }}
|
||||||
|
>
|
||||||
|
{word === '' ? <span> </span> : word}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</motion.h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function ny(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,26 @@ const config = {
|
|||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: "0" },
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
|
grid: {
|
||||||
|
"0%": { transform: "translateY(-50%)" },
|
||||||
|
"100%": { transform: "translateY(0)" },
|
||||||
|
},
|
||||||
|
"shine-pulse": {
|
||||||
|
"0%": {
|
||||||
|
"background-position": "0% 0%",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
"background-position": "100% 100%",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
"background-position": "0% 0%",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
grid: "grid 15s linear infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user