diff --git a/config/docs.ts b/config/docs.ts index 1edb0ac4..a1e4ed75 100644 --- a/config/docs.ts +++ b/config/docs.ts @@ -356,6 +356,12 @@ export const docsConfig: DocsConfig = { items: [], label: "", }, + { + title: "Morphing Text", + href: `/docs/components/morphing-text`, + items: [], + label: "New", + } ], }, { diff --git a/content/docs/components/morphing-text.mdx b/content/docs/components/morphing-text.mdx new file mode 100644 index 00000000..debc7c3d --- /dev/null +++ b/content/docs/components/morphing-text.mdx @@ -0,0 +1,78 @@ +--- +title: Morphing Text +date: 2024-09-02 +description: A dynamic text morphing component for Magic UI. +author: magicui +published: true +--- + + + +## Installation + + + + + CLI + Manual + + + +```bash +npx magicui-cli add morphing-text +``` + + + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + + + + + +## Props + +| Prop | Type | Description | Default | +| --------- | -------- | --------------------------------- | ------- | +| `texts` | `string[]` | Array of texts to morph between | `[]` | +| `className` | `string?` | Additional classes for the container | `""` | + +## Usage Example + +```tsx +import MorphingText from "../magicui/morphing-text"; + +const texts = [ + "Dynamic", + "Morphing", + "Text", + "Animation", + "React", + "Component", + "Smooth", + "Transition", + "Engaging", +]; + +export default function MorphingTextDemo() { + return ; +} +``` + +This `MorphingText` component dynamically transitions between an array of text strings, creating a smooth, engaging visual effect. + +## Credits + +- Credit to [@luis-code](https://luis-code.vercel.app/) diff --git a/registry/components/example/morphing-text-demo.tsx b/registry/components/example/morphing-text-demo.tsx new file mode 100644 index 00000000..85326761 --- /dev/null +++ b/registry/components/example/morphing-text-demo.tsx @@ -0,0 +1,17 @@ +import MorphingText from "../magicui/morphing-text"; + +const texts = [ + "Dynamic", + "Morphing", + "Text", + "Animation", + "React", + "Component", + "Smooth", + "Transition", + "Engaging", +]; + +export default function MorphingTextDemo() { + return ; +} diff --git a/registry/components/magicui/morphing-text.tsx b/registry/components/magicui/morphing-text.tsx new file mode 100644 index 00000000..27c0844b --- /dev/null +++ b/registry/components/magicui/morphing-text.tsx @@ -0,0 +1,139 @@ +import { useCallback, useEffect, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +const morphTime = 1.5; +const cooldownTime = 0.5; + +const useMorphingText = (texts: string[]) => { + const textIndexRef = useRef(0); + const morphRef = useRef(0); + const cooldownRef = useRef(0); + const timeRef = useRef(new Date()); + + const text1Ref = useRef(null); + const text2Ref = useRef(null); + + const setStyles = useCallback( + (fraction: number) => { + const [current1, current2] = [text1Ref.current, text2Ref.current]; + if (!current1 || !current2) return; + + current2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`; + current2.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`; + + const invertedFraction = 1 - fraction; + current1.style.filter = `blur(${Math.min(8 / invertedFraction - 8, 100)}px)`; + current1.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`; + + current1.textContent = texts[textIndexRef.current % texts.length]; + current2.textContent = texts[(textIndexRef.current + 1) % texts.length]; + }, + [texts], + ); + + const doMorph = useCallback(() => { + morphRef.current -= cooldownRef.current; + cooldownRef.current = 0; + + let fraction = morphRef.current / morphTime; + + if (fraction > 1) { + cooldownRef.current = cooldownTime; + fraction = 1; + } + + setStyles(fraction); + + if (fraction === 1) { + textIndexRef.current++; + } + }, [setStyles]); + + const doCooldown = useCallback(() => { + morphRef.current = 0; + const [current1, current2] = [text1Ref.current, text2Ref.current]; + if (current1 && current2) { + current2.style.filter = "none"; + current2.style.opacity = "100%"; + current1.style.filter = "none"; + current1.style.opacity = "0%"; + } + }, []); + + useEffect(() => { + let animationFrameId: number; + + const animate = () => { + animationFrameId = requestAnimationFrame(animate); + + const newTime = new Date(); + const dt = (newTime.getTime() - timeRef.current.getTime()) / 1000; + timeRef.current = newTime; + + cooldownRef.current -= dt; + + if (cooldownRef.current <= 0) doMorph(); + else doCooldown(); + }; + + animate(); + return () => { + cancelAnimationFrame(animationFrameId); + }; + }, [doMorph, doCooldown]); + + return { text1Ref, text2Ref }; +}; + +interface MorphingTextProps { + className?: string; + texts: string[]; +} + +const Texts: React.FC> = ({ texts }) => { + const { text1Ref, text2Ref } = useMorphingText(texts); + return ( + <> + + + + ); +}; + +const SvgFilters: React.FC = () => ( + + + + + + + +); + +const MorphingText: React.FC = ({ texts, className }) => ( +
+ + +
+); + +export default MorphingText; diff --git a/registry/index.tsx b/registry/index.tsx index ab6aa1ac..ac165b52 100644 --- a/registry/index.tsx +++ b/registry/index.tsx @@ -269,6 +269,11 @@ const ui: Registry = { type: "components:magicui", files: ["registry/components/magicui/iphone-15-pro.tsx"], }, + "morphing-text": { + name: "morphing-text", + type: "components:magicui", + files: ["registry/components/magicui/morphing-text.tsx"], + }, }; const example: Registry = { @@ -995,6 +1000,14 @@ const example: Registry = { () => import("@/registry/components/example/iphone-15-pro-demo-2"), ), }, + "morphing-text-demo": { + name: "morphing-text-demo", + type: "components:example", + files: ["registry/components/example/morphing-text-demo.tsx"], + component: React.lazy( + () => import("@/registry/components/example/morphing-text-demo"), + ), + }, }; export const registry: Registry = { ...ui,