feat(website): add shadcn and magicUI components
This commit is contained in:
140
packages/website/src/components/ui/hyper-text.tsx
Normal file
140
packages/website/src/components/ui/hyper-text.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { AnimatePresence, MotionProps, motion } from 'motion/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type CharacterSet = string[] | readonly string[]
|
||||
|
||||
interface HyperTextProps extends MotionProps {
|
||||
/** The text content to be animated */
|
||||
children: string
|
||||
/** Optional className for styling */
|
||||
className?: string
|
||||
/** Duration of the animation in milliseconds */
|
||||
duration?: number
|
||||
/** Delay before animation starts in milliseconds */
|
||||
delay?: number
|
||||
/** Component to render as - defaults to div */
|
||||
as?: React.ElementType
|
||||
/** Whether to start animation when element comes into view */
|
||||
startOnView?: boolean
|
||||
/** Whether to trigger animation on hover */
|
||||
animateOnHover?: boolean
|
||||
/** Custom character set for scramble effect. Defaults to uppercase alphabet */
|
||||
characterSet?: CharacterSet
|
||||
}
|
||||
|
||||
const DEFAULT_CHARACTER_SET = Object.freeze(
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
) as readonly string[]
|
||||
|
||||
const getRandomInt = (max: number): number => Math.floor(Math.random() * max)
|
||||
|
||||
export function HyperText({
|
||||
children,
|
||||
className,
|
||||
duration = 800,
|
||||
delay = 0,
|
||||
as: Component = 'div',
|
||||
startOnView = false,
|
||||
animateOnHover = true,
|
||||
characterSet = DEFAULT_CHARACTER_SET,
|
||||
...props
|
||||
}: HyperTextProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
})
|
||||
|
||||
const [displayText, setDisplayText] = useState<string[]>(() => children.split(''))
|
||||
const [isAnimating, setIsAnimating] = useState(false)
|
||||
const iterationCount = useRef(0)
|
||||
const elementRef = useRef<HTMLElement>(null)
|
||||
|
||||
const handleAnimationTrigger = () => {
|
||||
if (animateOnHover && !isAnimating) {
|
||||
iterationCount.current = 0
|
||||
setIsAnimating(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle animation start based on view or delay
|
||||
useEffect(() => {
|
||||
if (!startOnView) {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setIsAnimating(true)
|
||||
}, delay)
|
||||
return () => clearTimeout(startTimeout)
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
setIsAnimating(true)
|
||||
}, delay)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '-30% 0px -30% 0px' }
|
||||
)
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [delay, startOnView])
|
||||
|
||||
// Handle scramble animation
|
||||
useEffect(() => {
|
||||
if (!isAnimating) return
|
||||
|
||||
const maxIterations = children.length
|
||||
const startTime = performance.now()
|
||||
let animationFrameId: number
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
iterationCount.current = progress * maxIterations
|
||||
|
||||
setDisplayText((currentText) =>
|
||||
currentText.map((letter, index) =>
|
||||
letter === ' '
|
||||
? letter
|
||||
: index <= iterationCount.current
|
||||
? children[index]
|
||||
: characterSet[getRandomInt(characterSet.length)]
|
||||
)
|
||||
)
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
} else {
|
||||
setIsAnimating(false)
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
|
||||
return () => cancelAnimationFrame(animationFrameId)
|
||||
}, [children, duration, isAnimating, characterSet])
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn('overflow-hidden py-2 text-4xl font-bold', className)}
|
||||
onMouseEnter={handleAnimationTrigger}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{displayText.map((letter, index) => (
|
||||
<motion.span key={index} className={cn('font-mono', letter === ' ' ? 'w-3' : '')}>
|
||||
{letter.toUpperCase()}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</MotionComponent>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user