diff --git a/apps/website/src/app/api/docs/og/route.tsx b/apps/website/src/app/api/docs/og/route.tsx index c19db4f..d1c494c 100644 --- a/apps/website/src/app/api/docs/og/route.tsx +++ b/apps/website/src/app/api/docs/og/route.tsx @@ -27,6 +27,7 @@ export async function GET(request: Request) { const docData = getDocument({ folder: folder, document: document, + withGenerals: true, }); if (!docData) { diff --git a/apps/website/src/components/code-block/blocks/copy-text-morph.tsx b/apps/website/src/components/code-block/blocks/copy-text-morph.tsx new file mode 100644 index 0000000..effd651 --- /dev/null +++ b/apps/website/src/components/code-block/blocks/copy-text-morph.tsx @@ -0,0 +1,136 @@ +"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; + iconSize?: number; +} + +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 = {}; + + 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 ( + + + {characters.map((character) => ( + + ))} + + + ); +} + +const CopyTextMorph = ({ + content, + className, + ...props +}: CopyTextAnimatedProps) => { + const [isCopied, setIsCopied] = useState(false); + + useEffect(() => { + if (!isCopied) return; + + const timeout = setTimeout(() => { + setIsCopied(false); + }, 2000); + return () => clearTimeout(timeout); + }, [isCopied]); + + const handleCopy = async () => { + await copyToClipboard(content); + setIsCopied(true); + }; + + return ( + + ); +}; + +export { CopyTextMorph }; diff --git a/apps/website/src/components/docs/show-categories.tsx b/apps/website/src/components/docs/show-categories.tsx index 0672912..ed2f053 100644 --- a/apps/website/src/components/docs/show-categories.tsx +++ b/apps/website/src/components/docs/show-categories.tsx @@ -12,12 +12,18 @@ import { TagIcon, } from "lucide-react"; +import { + Shiki, + SugarHigh, + React, + RadixUI, + BaseUI, + Motion, +} from "@/components/ui/svgs"; + import { cn } from "@/utils/cn"; import { Badge, badgeVariants } from "@/components/ui/badge"; import { ExternalLink } from "@/components/ui/external-link"; -import { Shiki, SugarHigh, React } from "@/components/ui/svgs"; -import { RadixUI } from "../ui/svgs/radix-ui"; -import { BaseUI } from "../ui/svgs/base-ui"; const categorySvgs = [ { @@ -65,6 +71,11 @@ const categorySvgs = [ icon: BaseUI, url: "https://base-ui.com/", }, + { + name: "Motion", + icon: Motion, + url: "https://motion.dev/", + }, { name: "Blocks", icon: BoxIcon, diff --git a/apps/website/src/components/docs/sidebar-data.ts b/apps/website/src/components/docs/sidebar-data.ts index c1e22b3..b3f9d17 100644 --- a/apps/website/src/components/docs/sidebar-data.ts +++ b/apps/website/src/components/docs/sidebar-data.ts @@ -90,6 +90,10 @@ export const SidebarLinksData: SidebarLinks[] = [ title: "Multi Tabs", href: "/docs/react/blocks/multi-tabs", }, + { + title: "Copy with Text Morph", + href: "/docs/react/blocks/copy-text-morph", + }, { title: "Persist Package Manager", href: "/docs/react/blocks/persist-package-manager", diff --git a/apps/website/src/components/github-link.tsx b/apps/website/src/components/github-link.tsx index b8646b6..6801919 100644 --- a/apps/website/src/components/github-link.tsx +++ b/apps/website/src/components/github-link.tsx @@ -4,7 +4,6 @@ import useSWR from "swr"; import { cn } from "@/utils/cn"; import { globals } from "@/globals"; -import { StarIcon } from "lucide-react"; import { GitHub } from "@/components/ui/svgs/github"; import { buttonVariants } from "@/components/ui/button"; diff --git a/apps/website/src/components/previews/copy-text-morph-example.tsx b/apps/website/src/components/previews/copy-text-morph-example.tsx new file mode 100644 index 0000000..a00b8a6 --- /dev/null +++ b/apps/website/src/components/previews/copy-text-morph-example.tsx @@ -0,0 +1,37 @@ +import { + CodeBlock, + CodeBlockContent, + CodeBlockGroup, + CodeBlockHeader, + CodeBlockIcon, +} from "@/components/code-block/code-block"; +import { CodeblockShiki } from "@/components/code-block/client/shiki"; +import { CopyTextMorph } from "@/components/code-block/blocks/copy-text-morph"; + +const code = `const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + console.log("Text copied to clipboard"); + } catch (err) { + console.error("Failed to copy text: ", err); + } +};`; + +const CopyMorphExample = () => { + return ( + + + + + Copy with Text Morph + + + + + + + + ); +}; + +export default CopyMorphExample; diff --git a/apps/website/src/components/registry/data.tsx b/apps/website/src/components/registry/data.tsx index 37f1da5..ea328d3 100644 --- a/apps/website/src/components/registry/data.tsx +++ b/apps/website/src/components/registry/data.tsx @@ -295,6 +295,23 @@ const Blocks: RegistryComponent[] = [ target: "src/components/code-block/blocks/inline-code.tsx", }, }, + { + title: "Blocks - Copy + Text Morph", + fileType: "tsx", + group: "blocks", + fileSource: `${codeblockComponent}/blocks/copy-text-morph.tsx`, + exampleFileSource: `${componentsFolder}/previews/copy-text-morph-example.tsx`, + reactComponent: lazy( + () => import("@/components/previews/copy-text-morph-example"), + ), + shadcnRegistry: { + name: "block-copy-text-morph", + type: "registry:block", + dependencies: ["motion"], + registryDependencies: ["shiki-highlighter", "code-block", "client-shiki"], + target: "src/components/code-block/blocks/copy-text-morph.tsx", + }, + }, { title: "Blocks - Select Package Manager", fileType: "tsx", diff --git a/apps/website/src/components/ui/sidebar.tsx b/apps/website/src/components/ui/sidebar.tsx index f12c798..aa848ad 100644 --- a/apps/website/src/components/ui/sidebar.tsx +++ b/apps/website/src/components/ui/sidebar.tsx @@ -26,7 +26,7 @@ const Sidebar = ({ children, position }: SidebarProps) => { return (