Skip to content

Commit

Permalink
✨ feat(apps/web/registry/special): add multiple animated components f…
Browse files Browse the repository at this point in the history
…or enhanced UI effects

This commit introduces a variety of new React components designed to enhance the user interface with animations and special effects. These components include animated grids, lines, lists, text, buttons, and more, each tailored to provide dynamic, visually appealing user interactions. The implementation leverages technologies like Framer Motion for smooth animations and React's latest features for optimal performance and flexibility. This addition aims to improve user engagement and the overall aesthetic of the web applications.
  • Loading branch information
nyxb committed Jun 18, 2024
1 parent 167cd7f commit c709884
Show file tree
Hide file tree
Showing 20 changed files with 1,490 additions and 0 deletions.
146 changes: 146 additions & 0 deletions apps/web/registry/special/animated-grid-pattern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use client'

import { motion } from 'framer-motion'
import { useEffect, useId, useRef, useState } from 'react'
import { cn } 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={cn(
'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
34 changes: 34 additions & 0 deletions apps/web/registry/special/animated-lines.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default function AnimatedLines() {
return (
<svg
viewBox="0 0 1005 758"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="stroke-[5px] [mask-repeat:no-repeat] [mask-size:40px]"
>
<path
d="M0.000366211 3C382.5 3 313 362.999 1005 362.999"
stroke="white"
/>

<path d="M1004.78 383.5H0.000366211" stroke="white" />
<path
d="M1005 404.5C313 404.5 379.5 755.5 0.000366211 755.5"
stroke="white"
/>

<path
d="M0.000366211 3C382.5 3 313 362.999 1005 362.999"
className="animate-line stroke-blue-500 [mask-image:linear-gradient(to_right,transparent,black,transparent)]"
/>
<path
d="M1004.78 383.5H0.000366211"
className="animate-line stroke-red-500 [mask-image:linear-gradient(to_right,transparent,black,transparent)]"
/>
<path
d="M1005 404.5C313 404.5 379.5 755.5 0.000366211 755.5"
className="animate-line stroke-green-500 [mask-image:linear-gradient(to_right,transparent,black,transparent)]"
/>
</svg>
)
}
62 changes: 62 additions & 0 deletions apps/web/registry/special/animated-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use client'

import { AnimatePresence, motion } from 'framer-motion'
import type { ReactElement } from 'react'
import React, { useEffect, useMemo, useState } from 'react'

export const AnimatedList = React.memo(
({
className,
children,
delay = 1000,
}: {
className?: string
children: React.ReactNode
delay?: number
}) => {
const [index, setIndex] = useState(0)
const childrenArray = React.Children.toArray(children)

useEffect(() => {
const interval = setInterval(() => {
setIndex(prevIndex => (prevIndex + 1) % childrenArray.length)
}, delay)

return () => clearInterval(interval)
}, [childrenArray.length, delay])

const itemsToShow = useMemo(
() => childrenArray.slice(0, index + 1).reverse(),
[index, childrenArray],
)

return (
<div className={`flex flex-col items-center gap-4 ${className}`}>
<AnimatePresence>
{itemsToShow.map(item => (
<AnimatedListItem key={(item as ReactElement).key}>
{item}
</AnimatedListItem>
))}
</AnimatePresence>
</div>
)
},
)

AnimatedList.displayName = 'AnimatedList'

export function AnimatedListItem({ children }: { children: React.ReactNode }) {
const animations = {
initial: { scale: 0, opacity: 0 },
animate: { scale: 1, opacity: 1, originY: 0 },
exit: { scale: 0, opacity: 0 },
transition: { type: 'spring', stiffness: 350, damping: 40 },
}

return (
<motion.div {...animations} layout className="mx-auto w-full">
{children}
</motion.div>
)
}
39 changes: 39 additions & 0 deletions apps/web/registry/special/animated-shiny-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { CSSProperties, FC, ReactNode } from 'react'
import { cn } 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={cn(
'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
68 changes: 68 additions & 0 deletions apps/web/registry/special/animated-subscribe-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client'

import { AnimatePresence, motion } from 'framer-motion'
import React, { useState } from 'react'

interface AnimatedSubscribeButtonProps {
buttonColor: string
buttonTextColor?: string
subscribeStatus: boolean
initialText: React.ReactElement | string
changeText: React.ReactElement | string
}

export const AnimatedSubscribeButton: React.FC<
AnimatedSubscribeButtonProps
> = ({
buttonColor,
subscribeStatus,
buttonTextColor,
changeText,
initialText,
}) => {
const [isSubscribed, setIsSubscribed] = useState<boolean>(subscribeStatus)

return (
<AnimatePresence mode="wait">
{isSubscribed
? (
<motion.button
className="relative flex w-[200px] items-center justify-center overflow-hidden rounded-md bg-white p-[10px] outline outline-1 outline-black"
onClick={() => setIsSubscribed(false)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.span
key="action"
className="relative block h-full w-full font-semibold"
initial={{ y: -50 }}
animate={{ y: 0 }}
style={{ color: buttonColor }}
>
{changeText}
</motion.span>
</motion.button>
)
: (
<motion.button
className="relative flex w-[200px] cursor-pointer items-center justify-center rounded-md border-none p-[10px]"
style={{ backgroundColor: buttonColor, color: buttonTextColor }}
onClick={() => setIsSubscribed(true)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.span
key="reaction"
className="relative block font-semibold"
initial={{ x: 0 }}
exit={{ x: 50, transition: { duration: 0.1 } }}
>
{initialText}
</motion.span>
</motion.button>
)}
</AnimatePresence>
)
}
36 changes: 36 additions & 0 deletions apps/web/registry/special/avatar-circles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import React from 'react'
import { cn } from '~/lib/utils'

interface AvatarCirclesProps {
className?: string
numPeople?: number
avatarUrls: string[]
}

function AvatarCircles({ numPeople, className, avatarUrls }: AvatarCirclesProps) {
return (
<div className={cn('z-10 flex -space-x-4 rtl:space-x-reverse', className)}>
{avatarUrls.map((url, index) => (
<img
key={index}
className="h-10 w-10 rounded-full border-2 border-white dark:border-gray-800"
src={url}
width={40}
height={40}
alt={`Avatar ${index + 1}`}
/>
))}
<a
className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-white bg-black text-center text-xs font-medium text-white hover:bg-gray-600 dark:border-gray-800 dark:bg-white dark:text-black"
href=""
>
+
{numPeople}
</a>
</div>
)
}

export default AvatarCircles
Loading

0 comments on commit c709884

Please sign in to comment.