HyperText

HyperText is a component that animates text.

H

E

L

L

O

W

O

R

L

D

N

O

A

N

I

M

A

T

I

O

N

O

N

H

O

V

E

R

Install the framer-motion library.

npm install framer-motion

Copy the code above and paste it into your project.

'use client'
 
import { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion, Variants } from 'framer-motion'
 
import { cn } from '@/lib/utils'
 
interface HyperTextProps {
  text: string
  duration?: number
  framerProps?: Variants
  className?: string
  animateOnLoad?: boolean
  animateOnHover?: boolean
}
 
const alphabets = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
 
const getRandomInt = (max: number) => Math.floor(Math.random() * max)
 
export function HyperText({
  text,
  duration = 800,
  framerProps = {
    initial: { opacity: 0, y: 0 },
    animate: { opacity: 1, y: 0 },
    exit: { opacity: 0, y: 0 },
  },
  className,
  animateOnLoad = true,
  animateOnHover = true,
}: HyperTextProps) {
  const [displayText, setDisplayText] = useState(text.split(''))
  const [trigger, setTrigger] = useState(false)
  const interations = useRef(0)
  const isFirstRender = useRef(true)
 
  const triggerAnimation = () => {
    interations.current = 0
    setTrigger(true)
  }
 
  const getDisplayText = (letter: string, index: number) => {
    if (letter === ' ') {
      return letter
    }
    if (index <= interations.current) {
      return text[index]
    }
    return alphabets[getRandomInt(26)]
  }
 
  useEffect(() => {
    const interval = setInterval(
      () => {
        if (!animateOnLoad && isFirstRender.current) {
          clearInterval(interval)
          isFirstRender.current = false
          return
        }
        if (interations.current < text.length) {
          setDisplayText(t => t.map((l, i) => getDisplayText(l, i)))
          interations.current = interations.current + 0.1
        } else {
          setTrigger(false)
          clearInterval(interval)
        }
      },
      duration / (text.length * 10)
    )
    // Clean up interval on unmount
    return () => clearInterval(interval)
  }, [text, duration, trigger, animateOnLoad])
 
  return (
    <div
      className="flex scale-100 cursor-default overflow-hidden py-2"
      onMouseEnter={animateOnHover ? triggerAnimation : undefined}
    >
      <AnimatePresence mode="wait">
        {displayText.map((letter, i) => (
          <motion.h1
            key={i}
            className={cn('font-mono', letter === ' ' ? 'w-3' : '', className)}
            {...framerProps}
          >
            {letter.toUpperCase()}
          </motion.h1>
        ))}
      </AnimatePresence>
    </div>
  )
}