"use client"
import { ElementType } from "react"
import { motion, Transition, Variants } from "motion/react"
import { cn } from "@/lib/utils"
interface TextProps extends React.HTMLAttributes<HTMLElement> {
/**
* The content to be displayed and animated
*/
children: React.ReactNode
/**
* HTML Tag to render the component as
*/
as?: ElementType
/**
* Initial font variation settings
*/
fromFontVariationSettings: string
/**
* Target font variation settings to animate to
*/
toFontVariationSettings: string
/**
* Animation transition configuration
* @default { duration: 1.5, ease: "easeInOut" }
*/
transition?: Transition
/**
* Duration of stagger delay between elements in seconds
* @default 0.1
*/
staggerDuration?: number
/**
* Direction to stagger animations from
* @default "first"
*/
staggerFrom?: "first" | "last" | "center" | number
/**
* Delay between animation repeats in seconds
* @default 0.1
*/
repeatDelay?: number
}
const BreathingText = ({
children,
as = "span",
fromFontVariationSettings,
toFontVariationSettings,
transition = {
duration: 1.5,
ease: "easeInOut",
},
staggerDuration = 0.1,
staggerFrom = "first",
repeatDelay = 0.1,
className,
...props
}: TextProps) => {
const letterVariants: Variants = {
initial: { fontVariationSettings: fromFontVariationSettings },
animate: (i) => ({
fontVariationSettings: toFontVariationSettings,
transition: {
...transition,
repeat: Infinity,
repeatType: "mirror",
delay: i * staggerDuration,
repeatDelay: repeatDelay,
},
}),
}
const getCustomIndex = (index: number, total: number) => {
if (typeof staggerFrom === "number") {
return Math.abs(index - staggerFrom)
}
switch (staggerFrom) {
case "first":
return index
case "last":
return total - 1 - index
case "center":
default:
return Math.abs(index - Math.floor(total / 2))
}
}
const letters = String(children).split("")
const ElementTag = as
return (
<ElementTag
className={cn(
className,
// an after pseudo element is used to create a container large enough to hold the text with full weight. Helps avoid layout shifts
"relative after:absolute after:content-[attr(data-text)] after:font-black after:pointer-none after:overflow-hidden after:select-none after:invisible after:h-0"
)}
{...props}
data-text={children}
>
{letters.map((letter: string, i: number) => (
<motion.span
key={i}
className="inline-block whitespace-pre"
aria-hidden="true"
variants={letterVariants}
initial="initial"
animate="animate"
custom={getCustomIndex(i, letters.length)}
>
{letter}
</motion.span>
))}
<span className="sr-only">{children}</span>
</ElementTag>
)
}
export default BreathingText