Download src/components/reactbits/SplitText.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 4.46 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/reactbits/SplitText.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/reactbits/SplitText.tsx
-
curl -L -o SplitText.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/reactbits/SplitText.tsx
4.46 kB
| "use client"; | |
| import { useEffect, useRef } from "react"; | |
| import { gsap } from "gsap"; | |
| import { ScrollTrigger } from "gsap/ScrollTrigger"; | |
| type SplitTextProps = { | |
| text: string; | |
| className?: string; | |
| delay?: number; | |
| duration?: number; | |
| ease?: string; | |
| splitType?: "chars" | "words" | "lines" | "chars,words" | "words,lines" | "chars,words,lines"; | |
| from?: gsap.TweenVars; | |
| to?: gsap.TweenVars; | |
| threshold?: number; | |
| rootMargin?: string; | |
| textAlign?: "left" | "center" | "right"; | |
| tag?: "p" | "h1" | "h2" | "h3" | "span"; | |
| onLetterAnimationComplete?: () => void; | |
| showCallback?: boolean; | |
| }; | |
| export default function SplitText({ | |
| text = "", | |
| className = "", | |
| delay = 50, | |
| duration = 1.25, | |
| ease = "power3.out", | |
| splitType = "chars", | |
| from = { opacity: 0, y: 40 }, | |
| to = { opacity: 1, y: 0 }, | |
| threshold = 0.1, | |
| rootMargin = "-100px", | |
| textAlign = "center", | |
| tag = "p", | |
| onLetterAnimationComplete, | |
| showCallback = false, | |
| }: SplitTextProps) { | |
| const ref = useRef<HTMLElement | null>(null); | |
| const animationCompletedRef = useRef(false); | |
| const onCompleteRef = useRef(onLetterAnimationComplete); | |
| useEffect(() => { | |
| onCompleteRef.current = onLetterAnimationComplete; | |
| }, [onLetterAnimationComplete]); | |
| useEffect(() => { | |
| if (typeof window === "undefined" || !ref.current || !text) return; | |
| if (animationCompletedRef.current) return; | |
| gsap.registerPlugin(ScrollTrigger); | |
| const el = ref.current; | |
| const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| if (reduceMotion) { | |
| animationCompletedRef.current = true; | |
| onCompleteRef.current?.(); | |
| return; | |
| } | |
| const startPct = (1 - threshold) * 100; | |
| const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(rootMargin); | |
| const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0; | |
| const marginUnit = marginMatch ? marginMatch[2] || "px" : "px"; | |
| const sign = | |
| marginValue === 0 | |
| ? "" | |
| : marginValue < 0 | |
| ? `-=${Math.abs(marginValue)}${marginUnit}` | |
| : `+=${marginValue}${marginUnit}`; | |
| const start = `top ${startPct}%${sign}`; | |
| // Select targets based on splitType | |
| let targets: Element[] = []; | |
| if (splitType.includes("chars")) { | |
| targets = Array.from(el.querySelectorAll(".split-char")); | |
| } else if (splitType.includes("words")) { | |
| targets = Array.from(el.querySelectorAll(".split-word")); | |
| } else { | |
| targets = [el]; | |
| } | |
| // Set initial state | |
| gsap.set(targets, { ...from }); | |
| // Animate using scrollTrigger | |
| const anim = gsap.to(targets, { | |
| ...to, | |
| duration, | |
| ease, | |
| stagger: delay / 1000, | |
| scrollTrigger: { | |
| trigger: el, | |
| start, | |
| once: true, | |
| fastScrollEnd: true, | |
| anticipatePin: 0.4, | |
| }, | |
| onComplete: () => { | |
| animationCompletedRef.current = true; | |
| onCompleteRef.current?.(); | |
| }, | |
| force3D: true, | |
| }); | |
| return () => { | |
| ScrollTrigger.getAll().forEach((trigger) => { | |
| if (trigger.trigger === el) trigger.kill(); | |
| }); | |
| anim.kill(); | |
| }; | |
| }, [ | |
| delay, | |
| duration, | |
| ease, | |
| from, | |
| rootMargin, | |
| showCallback, | |
| splitType, | |
| text, | |
| threshold, | |
| to, | |
| ]); | |
| const Tag = tag; | |
| const words = text.split(" "); | |
| return ( | |
| <Tag | |
| ref={(node) => { | |
| ref.current = node; | |
| }} | |
| className={`split-parent ${className}`.trim()} | |
| style={{ | |
| textAlign, | |
| overflow: "hidden", | |
| display: "inline-block", | |
| whiteSpace: "normal", | |
| wordWrap: "break-word", | |
| willChange: "transform, opacity", | |
| }} | |
| > | |
| {splitType.includes("chars") ? ( | |
| words.map((word, wIdx) => ( | |
| <span key={wIdx} className="split-word inline-block" style={{ whiteSpace: "nowrap" }}> | |
| {word.split("").map((char, cIdx) => ( | |
| <span key={cIdx} className="split-char inline-block" style={{ display: "inline-block" }}> | |
| {char} | |
| </span> | |
| ))} | |
| {wIdx < words.length - 1 ? "\u00A0" : ""} | |
| </span> | |
| )) | |
| ) : splitType.includes("words") ? ( | |
| words.map((word, wIdx) => ( | |
| <span key={wIdx} className="split-word inline-block" style={{ display: "inline-block" }}> | |
| {word} | |
| {wIdx < words.length - 1 ? "\u00A0" : ""} | |
| </span> | |
| )) | |
| ) : ( | |
| text | |
| )} | |
| </Tag> | |
| ); | |
| } | |