Preview
Preparing...
Introduction
In this block, we'll create a copy button with animated text using Motion-Primitives by Julien Thibeaut. The button will morph between "Copy" and "Copied" when clicked, providing visual feedback to the user.
Installation
shadcn/ui
shadcn/ui Command
pnpm dlx shadcn@latest add https://code-blocks.pheralb.dev/r/block-copy-text-morph.jsonManual
- Install motion:
Motion
pnpm i motion- Create the basic Code Block structure:
reactCode Block
The main structure of the Code Block component with header and content areas.
- Create the
CodeblockShikiclient component:
reactCode Block Client with Shiki
Create a client-side Code Block component using Shiki for syntax highlighting.
- Finally, create the
CopyTextMorphcomponent:
.tsx
"use client";
import {
useMemo,
useId,
useEffect,
useState,
type ComponentProps,
} from "react";
import {
motion,
AnimatePresence,
type Transition,
type Variants,
} from "motion/react";
import { cn } from "@/utils/cn";
import { copyToClipboard } from "@/utils/copy";
interface CopyTextAnimatedProps extends ComponentProps<"button"> {
content: string;
size?: "xs" | "sm";
}
export type TextMorphProps = {
children: string;
as?: React.ElementType;
className?: string;
style?: React.CSSProperties;
variants?: Variants;
transition?: Transition;
};
export function TextMorph({
children,
as: Component = "p",
className,
style,
variants,
transition,
}: TextMorphProps) {
const uniqueId = useId();
const characters = useMemo(() => {
const charCounts: Record<string, number> = {};
return children.split("").map((char) => {
const lowerChar = char.toLowerCase();
charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
return {
id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
label: char === " " ? "\u00A0" : char,
};
});
}, [children, uniqueId]);
const defaultVariants: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
const defaultTransition: Transition = {
type: "spring",
stiffness: 280,
damping: 18,
mass: 0.3,
};
return (
<Component className={cn(className)} aria-label={children} style={style}>
<AnimatePresence mode="popLayout" initial={false}>
{characters.map((character) => (
<motion.span
key={character.id}
layoutId={character.id}
className="inline-block"
aria-hidden="true"
initial="initial"
animate="animate"
exit="exit"
variants={variants || defaultVariants}
transition={transition || defaultTransition}
>
{character.label}
</motion.span>
))}
</AnimatePresence>
</Component>
);
}
const CopyTextMorph = ({
content,
size = "sm",
className,
...props
}: CopyTextAnimatedProps) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
useEffect(() => {
if (!isCopied) return;
const timeout = setTimeout(() => {
setIsCopied(false);
}, 2000);
return () => clearTimeout(timeout);
}, [isCopied]);
const handleCopy = async () => {
await copyToClipboard(content);
setIsCopied(true);
};
return (
<button
title="Copy to clipboard"
className={cn(
"cursor-pointer",
"transition-colors duration-200 ease-in-out",
"text-neutral-700 hover:text-neutral-950 dark:text-neutral-300 dark:hover:text-neutral-50",
"rounded-md bg-neutral-300/60 px-1.5 py-1 dark:bg-neutral-700/60",
"border border-transparent hover:border-neutral-400/60 dark:hover:border-neutral-600/60",
size === "xs" && "text-xs",
size === "sm" && "text-sm",
isCopied && "text-neutral-950 dark:text-neutral-50",
className,
)}
onClick={handleCopy}
{...props}
>
<TextMorph>{isCopied ? `Copied` : `Copy`}</TextMorph>
</button>
);
};
export { CopyTextMorph };Usage
.tsx
import { CopyTextMorph } from "@/components/code-block/blocks/copy-text-morph";
const Example = () => {
return <CopyTextMorph content="Text to be copied" />;
};
export default Example;Or with <CodeBlock> component:
.tsx
import {
CodeBlock,
CodeBlockHeader,
CodeBlockContent,
} from "@/components/code-block/code-block";
import { CopyTextMorph } from "@/components/code-block/blocks/copy-text-morph";
const code = `const greet = () => {
console.log("Hello, World!");
};`;
const Example = () => {
return (
<CodeBlock>
<CodeBlockHeader>
<CopyTextMorph content={code} />
</CodeBlockHeader>
<CodeBlockContent>
<CodeblockShiki language="ts" code={code} />
</CodeBlockContent>
</CodeBlock>
);
};
export default Example;Props
React Props
TextMorph
| Prop | Type | Required |
|---|---|---|
| as | ElementType<any, keyof IntrinsicElements> | No |
| className | string | No |
| style | CSSProperties | No |
| variants | Variants | No |
| transition | Transition | No |
CopyTextMorph
| Prop | Type | Required | |
|---|---|---|---|
| content | string | Yes | |
| size | "xs" | "sm" | No | |
| Included: DOMAttributes | |||