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,