Damn, I like this website!

This commit is contained in:
Mauro Balades
2024-07-03 17:03:11 +02:00
parent ef3e94cb8d
commit 2ab3801100
31 changed files with 8838 additions and 54 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -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"

View File

@@ -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>
);
}

View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
export default function Logo() {
return (
<img src="/logo.png" alt="Zen Logo" className="w-12 h-12" />
);
}

View 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"

View 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>
)
}

View 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

View 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

View 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

View 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

View 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>
)
}

View 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>&nbsp;</span> : char}
</motion.h1>
))}
</AnimatePresence>
</div>
)
}

View 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

View 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,
}

View 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

View 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

View 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>
)
}

View 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>
)
}

View 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

View 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

View 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>
)
}

View 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

View 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>&nbsp;</span> : word}
</motion.span>
))}
</motion.h1>
)
}

View File

@@ -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))
}

View File

@@ -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",
},
},
},