Vertical Cut Reveal
Vertical Cut Reveal é um componente para React e Tailwind CSS, da biblioteca Fancy Components, com licença MIT. Copia e cola no teu projeto.
Fancy Components
MIT
other
O que é
Vertical Cut Reveal é um componente para React e Tailwind CSS, da biblioteca Fancy Components, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de Fancy Components e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Vertical Cut Reveal
"use client"
import { AnimationOptions, motion } from "motion/react"
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react"
import { cn } from "@/lib/utils"
interface TextProps {
children: React.ReactNode
reverse?: boolean
transition?: AnimationOptions
splitBy?: "words" | "characters" | "lines" | string
staggerDuration?: number
staggerFrom?: "first" | "last" | "center" | "random" | number
containerClassName?: string
wordLevelClassName?: string
elementLevelClassName?: string
onClick?: () => void
onStart?: () => void
onComplete?: () => void
autoStart?: boolean // Whether to start the animation automatically
}
// Ref interface to allow external control of the animation
export interface VerticalCutRevealRef {
startAnimation: () => void
reset: () => void
}
interface WordObject {
characters: string[]
needsSpace: boolean
}
const VerticalCutReveal = forwardRef<VerticalCutRevealRef, TextProps>(
(
{
children,
reverse = false,
transition = {
type: "spring",
stiffness: 190,
damping: 22,
},
splitBy = "words",
staggerDuration = 0.2,
staggerFrom = "first",
containerClassName,
wordLevelClassName,
elementLevelClassName,
onClick,
onStart,
onComplete,
autoStart = true,
...props
},
ref
) => {
const containerRef = useRef<HTMLSpanElement>(null)
const text =
typeof children === "string" ? children : children?.toString() || ""
const [isAnimating, setIsAnimating] = useState(false)
// handy function to split text into characters with support for unicode and emojis
const splitIntoCharacters = (text: string): string[] => {
if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" })
return Array.from(segmenter.segment(text), ({ segment }) => segment)
}
// Fallback for browsers that don't support Intl.Segmenter
return Array.from(text)
}
// Split text based on splitBy parameter
const elements = useMemo(() => {
const words = text.split(" ")
if (splitBy === "characters") {
return words.map((word, i) => ({
characters: splitIntoCharacters(word),
needsSpace: i !== words.length - 1,
}))
}
return splitBy === "words"
? text.split(" ")
: splitBy === "lines"
? text.split("\n")
: text.split(splitBy)
}, [text, splitBy])
// Calculate stagger delays based on staggerFrom
const getStaggerDelay = useCallback(
(index: number) => {
const total =
splitBy === "characters"
? elements.reduce(
(acc, word) =>
acc +
(typeof word === "string"
? 1
: word.characters.length + (word.needsSpace ? 1 : 0)),
0
)
: elements.length
if (staggerFrom === "first") return index * staggerDuration
if (staggerFrom === "last") return (total - 1 - index) * staggerDuration
if (staggerFrom === "center") {
const center = Math.floor(total / 2)
return Math.abs(center - index) * staggerDuration
}
if (staggerFrom === "random") {
const randomIndex = Math.floor(Math.random() * total)
return Math.abs(randomIndex - index) * staggerDuration
}
return Math.abs(staggerFrom - index) * staggerDuration
},
[elements.length, staggerFrom, staggerDuration]
)
const startAnimation = useCallback(() => {
setIsAnimating(true)
onStart?.()
}, [onStart])
// Expose the startAnimation function via ref
useImperativeHandle(ref, () => ({
startAnimation,
reset: () => setIsAnimating(false),
}))
// Auto start animation
useEffect(() => {
if (autoStart) {
startAnimation()
}
}, [autoStart])
const variants = {
hidden: { y: reverse ? "-100%" : "100%" },
visible: (i: number) => ({
y: 0,
transition: {
...transition,
delay: ((transition?.delay as number) || 0) + getStaggerDelay(i),
},
}),
}
return (
<span
className={cn(
containerClassName,
"flex flex-wrap whitespace-pre-wrap",
splitBy === "lines" && "flex-col"
)}
onClick={onClick}
ref={containerRef}
{...props}
>
<span className="sr-only">{text}</span>
{(splitBy === "characters"
? (elements as WordObject[])
: (elements as string[]).map((el, i) => ({
characters: [el],
needsSpace: i !== elements.length - 1,
}))
).map((wordObj, wordIndex, array) => {
const previousCharsCount = array
.slice(0, wordIndex)
.reduce((sum, word) => sum + word.characters.length, 0)
return (
<span
key={wordIndex}
aria-hidden="true"
className={cn("inline-flex overflow-hidden", wordLevelClassName)}
>
{wordObj.characters.map((char, charIndex) => (
<span
className={cn(
elementLevelClassName,
"whitespace-pre-wrap relative"
)}
key={charIndex}
>
<motion.span
custom={previousCharsCount + charIndex}
initial="hidden"
animate={isAnimating ? "visible" : "hidden"}
variants={variants}
onAnimationComplete={
wordIndex === elements.length - 1 &&
charIndex === wordObj.characters.length - 1
? onComplete
: undefined
}
className="inline-block"
>
{char}
</motion.span>
</span>
))}
{wordObj.needsSpace && <span> </span>}
</span>
)
})}
</span>
)
}
)
VerticalCutReveal.displayName = "VerticalCutReveal"
export default VerticalCutReveal