-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat(apps/web/registry/special): add multiple animated components f…
…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
Showing
20 changed files
with
1,490 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.