Damn, I like this website!
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"$schema": "https://nyxbui.design/schema.json",
|
||||
"style": "miami",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
@@ -10,7 +10,7 @@
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cobe": "^0.6.3",
|
||||
"framer-motion": "^11.2.12",
|
||||
"lucide-react": "^0.400.0",
|
||||
"next": "14.2.4",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-spring": "^9.7.3",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"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 |
@@ -16,7 +16,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning >
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
|
||||
@@ -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() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
|
||||
</main>
|
||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||
<Header />
|
||||
<Features />
|
||||
<Footer />
|
||||
<Navigation /> {/* At the bottom of the page */}
|
||||
</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 { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
export function ny(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const config = {
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
@@ -67,10 +67,26 @@ const config = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
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: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
grid: "grid 15s linear infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user