{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-along-path",
  "type": "registry:block",
  "title": "Text along path",
  "description": "Text along path",
  "files": [
    {
      "path": "components/usages/textalongpathusage.tsx",
      "content": "import { useCallback, useState } from \"react\";\n\nimport AnimatedPathText from \"@/registry/open-source/text-along-path\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nimport { Button } from \"../ui/button\";\n\nexport default function TextAlongPathUsage() {\n\t// Rounded rectangle path\n\tconst rectPath =\n\t\t\"M 20,20 L 180,20 A 20,20 0 0,1 200,40 L 200,160 A 20,20 0 0,1 180,180 L 20,180 A 20,20 0 0,1 0,160 L 0,40 A 20,20 0 0,1 20,20\";\n\tconst [buttonState, setButtonState] = useState<\n\t\t\"idle\" | \"loading\" | \"success\"\n\t>(\"idle\");\n\tconst [email, setEmail] = useState(\"\");\n\n\tconst buttonCopy = {\n\t\tidle: \"Subscribe\",\n\t\tloading: (\n\t\t\t<motion.div className=\"h-2 w-2 sm:h-4 sm:w-4 animate-spin rounded-full border-2 border-white border-t-transparent\" />\n\t\t),\n\t\tsuccess: \"Done ✓\",\n\t} as const;\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (buttonState === \"success\") return;\n\n\t\tsetButtonState(\"loading\");\n\n\t\tsetTimeout(() => {\n\t\t\tsetButtonState(\"success\");\n\t\t}, 1750);\n\n\t\tsetTimeout(() => {\n\t\t\tsetButtonState(\"idle\");\n\t\t\tsetEmail(\"\");\n\t\t}, 3500);\n\t}, [buttonState]);\n\n\treturn (\n\t\t<div className=\"w-dvw h-dvh flex justify-center items-center text-secondary relative bg-background\">\n\t\t\t<AnimatedPathText\n\t\t\t\tpath={rectPath}\n\t\t\t\tsvgClassName=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 py-2 sm:py-8\"\n\t\t\t\tviewBox=\"-20 10 240 180\"\n\t\t\t\ttext=\"JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ \"\n\t\t\t\ttextClassName=\"text-[10.6px] lowercase font-azeret-mono text-secondary\"\n\t\t\t\tduration={20}\n\t\t\t\tpreserveAspectRatio=\"none\"\n\t\t\t\ttextAnchor=\"start\"\n\t\t\t/>\n\n\t\t\t{/* This is just fluff for the demo */}\n\t\t\t<div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-56 sm:w-80 p-6 \">\n\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\tplaceholder=\"Enter your email\"\n\t\t\t\t\t\tvalue={email}\n\t\t\t\t\t\tonChange={(e) => setEmail(e.target.value)}\n\t\t\t\t\t\tclassName=\"w-full px-3 py-2 sm:px-4 sm:py-2 border border-[#0015ff] focus:outline-hidden focus:ring-primary-blue/50 font-azeret-mono text-xs sm:text-base placeholder:text-secondary rounded-lg bg-background\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Button\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\tonClick={handleSubmit}\n\t\t\t\t\t\tdisabled={buttonState === \"loading\"}\n\t\t\t\t\t\tclassName=\"w-full px-3 py-2 h-9 sm:h-11 sm:px-8 sm:py-2 bg-background text-secondary hover:bg-background/90 transition-colors font-azeret-mono text-xs sm:text-base rounded-lg\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AnimatePresence mode=\"popLayout\" initial={false}>\n\t\t\t\t\t\t\t<motion.span\n\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\ttype: \"spring\",\n\t\t\t\t\t\t\t\t\tduration: 0.3,\n\t\t\t\t\t\t\t\t\tbounce: 0,\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tinitial={{ opacity: 0, y: -25 }}\n\t\t\t\t\t\t\t\tanimate={{ opacity: 1, y: 0 }}\n\t\t\t\t\t\t\t\texit={{ opacity: 0, y: 25 }}\n\t\t\t\t\t\t\t\tkey={buttonState}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{buttonCopy[buttonState]}\n\t\t\t\t\t\t\t</motion.span>\n\t\t\t\t\t\t</AnimatePresence>\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/textalongpathusage.tsx",
      "content": "import { useCallback, useState } from \"react\";\n\nimport AnimatedPathText from \"@/registry/open-source/text-along-path\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nimport { Button } from \"../ui/button\";\n\nexport default function TextAlongPathUsage() {\n\t// Rounded rectangle path\n\tconst rectPath =\n\t\t\"M 20,20 L 180,20 A 20,20 0 0,1 200,40 L 200,160 A 20,20 0 0,1 180,180 L 20,180 A 20,20 0 0,1 0,160 L 0,40 A 20,20 0 0,1 20,20\";\n\tconst [buttonState, setButtonState] = useState<\n\t\t\"idle\" | \"loading\" | \"success\"\n\t>(\"idle\");\n\tconst [email, setEmail] = useState(\"\");\n\n\tconst buttonCopy = {\n\t\tidle: \"Subscribe\",\n\t\tloading: (\n\t\t\t<motion.div className=\"h-2 w-2 sm:h-4 sm:w-4 animate-spin rounded-full border-2 border-white border-t-transparent\" />\n\t\t),\n\t\tsuccess: \"Done ✓\",\n\t} as const;\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (buttonState === \"success\") return;\n\n\t\tsetButtonState(\"loading\");\n\n\t\tsetTimeout(() => {\n\t\t\tsetButtonState(\"success\");\n\t\t}, 1750);\n\n\t\tsetTimeout(() => {\n\t\t\tsetButtonState(\"idle\");\n\t\t\tsetEmail(\"\");\n\t\t}, 3500);\n\t}, [buttonState]);\n\n\treturn (\n\t\t<div className=\"w-dvw h-dvh flex justify-center items-center text-secondary relative bg-background\">\n\t\t\t<AnimatedPathText\n\t\t\t\tpath={rectPath}\n\t\t\t\tsvgClassName=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 py-2 sm:py-8\"\n\t\t\t\tviewBox=\"-20 10 240 180\"\n\t\t\t\ttext=\"JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ JOIN THE WAITLIST ✉ \"\n\t\t\t\ttextClassName=\"text-[10.6px] lowercase font-azeret-mono text-secondary\"\n\t\t\t\tduration={20}\n\t\t\t\tpreserveAspectRatio=\"none\"\n\t\t\t\ttextAnchor=\"start\"\n\t\t\t/>\n\n\t\t\t{/* This is just fluff for the demo */}\n\t\t\t<div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-56 sm:w-80 p-6 \">\n\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\tplaceholder=\"Enter your email\"\n\t\t\t\t\t\tvalue={email}\n\t\t\t\t\t\tonChange={(e) => setEmail(e.target.value)}\n\t\t\t\t\t\tclassName=\"w-full px-3 py-2 sm:px-4 sm:py-2 border border-[#0015ff] focus:outline-hidden focus:ring-primary-blue/50 font-azeret-mono text-xs sm:text-base placeholder:text-secondary rounded-lg bg-background\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Button\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\tonClick={handleSubmit}\n\t\t\t\t\t\tdisabled={buttonState === \"loading\"}\n\t\t\t\t\t\tclassName=\"w-full px-3 py-2 h-9 sm:h-11 sm:px-8 sm:py-2 bg-background text-secondary hover:bg-background/90 transition-colors font-azeret-mono text-xs sm:text-base rounded-lg\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AnimatePresence mode=\"popLayout\" initial={false}>\n\t\t\t\t\t\t\t<motion.span\n\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\ttype: \"spring\",\n\t\t\t\t\t\t\t\t\tduration: 0.3,\n\t\t\t\t\t\t\t\t\tbounce: 0,\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tinitial={{ opacity: 0, y: -25 }}\n\t\t\t\t\t\t\t\tanimate={{ opacity: 1, y: 0 }}\n\t\t\t\t\t\t\t\texit={{ opacity: 0, y: 25 }}\n\t\t\t\t\t\t\t\tkey={buttonState}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{buttonCopy[buttonState]}\n\t\t\t\t\t\t\t</motion.span>\n\t\t\t\t\t\t</AnimatePresence>\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/text-along-path.tsx",
      "content": "import { RefObject, useEffect, useRef } from \"react\";\n\nimport { useScroll, useTransform } from \"motion/react\";\n\n// Credit:\n// https://www.fancycomponents.dev/docs/components/text/text-along-path\n\ntype PreserveAspectRatioAlign =\n\t| \"none\"\n\t| \"xMinYMin\"\n\t| \"xMidYMin\"\n\t| \"xMaxYMin\"\n\t| \"xMinYMid\"\n\t| \"xMidYMid\"\n\t| \"xMaxYMid\"\n\t| \"xMinYMax\"\n\t| \"xMidYMax\"\n\t| \"xMaxYMax\";\n\ntype PreserveAspectRatioMeetOrSlice = \"meet\" | \"slice\";\n\ntype PreserveAspectRatio =\n\t| PreserveAspectRatioAlign\n\t| `${Exclude<\n\t\t\tPreserveAspectRatioAlign,\n\t\t\t\"none\"\n\t  >} ${PreserveAspectRatioMeetOrSlice}`;\n\ninterface AnimatedPathTextProps {\n\t// Path properties\n\tpath: string;\n\tpathId?: string;\n\tpathClassName?: string;\n\tpreserveAspectRatio?: PreserveAspectRatio;\n\tshowPath?: boolean;\n\n\t// SVG properties\n\twidth?: string | number;\n\theight?: string | number;\n\tviewBox?: string;\n\tsvgClassName?: string;\n\n\t// Text properties\n\ttext: string;\n\ttextClassName?: string;\n\ttextAnchor?: \"start\" | \"middle\" | \"end\";\n\n\t// Animation properties\n\tanimationType?: \"auto\" | \"scroll\";\n\n\t// Animation properties if animationType is auto\n\tduration?: number;\n\trepeatCount?: number | \"indefinite\";\n\teasingFunction?: {\n\t\tcalcMode?: string;\n\t\tkeyTimes?: string;\n\t\tkeySplines?: string;\n\t};\n\n\t// Scroll animation properties if animationType is scroll\n\tscrollContainer?: RefObject<HTMLElement>;\n\tscrollOffset?: any[\"offset\"];\n\tscrollTransformValues?: [number, number];\n}\n\nconst AnimatedPathText = ({\n\t// Path defaults\n\tpath,\n\tpathId,\n\tpathClassName,\n\tpreserveAspectRatio = \"xMidYMid meet\",\n\tshowPath = false,\n\n\t// SVG defaults\n\twidth = \"100%\",\n\theight = \"100%\",\n\tviewBox = \"0 0 100 100\",\n\tsvgClassName,\n\n\t// Text defaults\n\ttext,\n\ttextClassName,\n\ttextAnchor = \"start\",\n\n\t// Animation type\n\tanimationType = \"auto\",\n\n\t// Animation defaults\n\tduration = 4,\n\trepeatCount = \"indefinite\",\n\n\teasingFunction = {},\n\n\t// Scroll animation defaults\n\tscrollContainer,\n\tscrollOffset = [\"start end\", \"end end\"],\n\tscrollTransformValues = [0, 100],\n}: AnimatedPathTextProps) => {\n\tconst container = useRef<HTMLDivElement>(null);\n\tconst textPathRefs = useRef<SVGTextPathElement[]>([]);\n\n\t// naive id for the path. you should rather use yours :)\n\tconst id =\n\t\tpathId || `animated-path-${Math.random().toString(36).substring(7)}`;\n\n\tconst { scrollYProgress } = useScroll({\n\t\tcontainer: scrollContainer || container,\n\t\toffset: scrollOffset,\n\t});\n\n\tconst t = useTransform(scrollYProgress, [0, 1], scrollTransformValues);\n\n\tuseEffect(() => {\n\t\t// Re-initialize scroll handler when container ref changes\n\t\tconst handleChange = (e: number) => {\n\t\t\ttextPathRefs.current.forEach((textPath) => {\n\t\t\t\tif (textPath) {\n\t\t\t\t\ttextPath.setAttribute(\"startOffset\", `${t.get()}%`);\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tscrollYProgress.on(\"change\", handleChange);\n\n\t\treturn () => {\n\t\t\tscrollYProgress.clearListeners();\n\t\t};\n\t}, [scrollYProgress, t]);\n\n\tconst animationProps =\n\t\tanimationType === \"auto\"\n\t\t\t? {\n\t\t\t\t\tfrom: \"0%\",\n\t\t\t\t\tto: \"100%\",\n\t\t\t\t\tbegin: \"0s\",\n\t\t\t\t\tdur: `${duration}s`,\n\t\t\t\t\trepeatCount: repeatCount,\n\t\t\t\t\t...(easingFunction && easingFunction),\n\t\t\t\t}\n\t\t\t: null;\n\n\treturn (\n\t\t<svg\n\t\t\tclassName={svgClassName}\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth={width}\n\t\t\theight={height}\n\t\t\tviewBox={viewBox}\n\t\t\tpreserveAspectRatio={preserveAspectRatio}\n\t\t>\n\t\t\t<path\n\t\t\t\tid={id}\n\t\t\t\tclassName={pathClassName}\n\t\t\t\td={path}\n\t\t\t\tstroke={showPath ? \"currentColor\" : \"none\"}\n\t\t\t\tfill=\"none\"\n\t\t\t/>\n\n\t\t\t{/* First text element */}\n\t\t\t<text textAnchor={textAnchor} fill=\"currentColor\">\n\t\t\t\t<textPath\n\t\t\t\t\tclassName={textClassName}\n\t\t\t\t\thref={`#${id}`}\n\t\t\t\t\tstartOffset={\"0%\"}\n\t\t\t\t\tref={(ref) => {\n\t\t\t\t\t\tif (ref) textPathRefs.current[0] = ref;\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{animationType === \"auto\" && (\n\t\t\t\t\t\t<animate attributeName=\"startOffset\" {...animationProps} />\n\t\t\t\t\t)}\n\t\t\t\t\t{text}\n\t\t\t\t</textPath>\n\t\t\t</text>\n\n\t\t\t{/* Second text element (offset to hide the jump) */}\n\t\t\t{animationType === \"auto\" && (\n\t\t\t\t<text textAnchor={textAnchor} fill=\"currentColor\">\n\t\t\t\t\t<textPath\n\t\t\t\t\t\tclassName={textClassName}\n\t\t\t\t\t\thref={`#${id}`}\n\t\t\t\t\t\tstartOffset={\"-100%\"}\n\t\t\t\t\t\tref={(ref) => {\n\t\t\t\t\t\t\tif (ref) textPathRefs.current[1] = ref;\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{animationType === \"auto\" && (\n\t\t\t\t\t\t\t<animate\n\t\t\t\t\t\t\t\tattributeName=\"startOffset\"\n\t\t\t\t\t\t\t\t{...animationProps}\n\t\t\t\t\t\t\t\tfrom=\"-100%\"\n\t\t\t\t\t\t\t\tto=\"0%\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{text}\n\t\t\t\t\t</textPath>\n\t\t\t\t</text>\n\t\t\t)}\n\t\t</svg>\n\t);\n};\n\nexport default AnimatedPathText;\n",
      "type": "registry:ui"
    },
    {
      "path": "components/ui/button.tsx",
      "content": "import * as React from \"react\";\r\n\r\nimport { cn } from \"@/registry/utilities/cn\";\r\nimport { Slot } from \"@radix-ui/react-slot\";\r\nimport { cva, type VariantProps } from \"class-variance-authority\";\r\n\r\nconst buttonVariants = cva(\r\n\t\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm text-white hover:text-gray-400 font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\r\n\t{\r\n\t\tvariants: {\r\n\t\t\tvariant: {\r\n\t\t\t\tsimple:\r\n\t\t\t\t\t\"bg-secondary relative z-10 bg-transparent hover:border-secondary hover:bg-secondary/50  border border-transparent dark:text-white text-sm md:text-sm transition font-medium duration-200  rounded-md px-4 py-2  flex items-center justify-center\",\r\n\t\t\t\tdefault: \"bg-primary text-primary-foreground hover:bg-primary/90\",\r\n\t\t\t\tdestructive:\r\n\t\t\t\t\t\"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\r\n\t\t\t\toutline:\r\n\t\t\t\t\t\"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\r\n\t\t\t\tsecondary:\r\n\t\t\t\t\t\"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\r\n\t\t\t\tghost: \"hover:bg-accent hover:text-black hover:stroke-black dark:text-white text-black\",\r\n\t\t\t\tlink: \"text-primary underline-offset-4 hover:underline\",\r\n\t\t\t},\r\n\t\t\tsize: {\r\n\t\t\t\tdefault: \"h-10 px-4 py-2\",\r\n\t\t\t\tsm: \"h-9 rounded-md px-3\",\r\n\t\t\t\tlg: \"h-11 rounded-md px-8\",\r\n\t\t\t\ticon: \"h-10 w-10\",\r\n\t\t\t},\r\n\t\t},\r\n\t\tdefaultVariants: {\r\n\t\t\tvariant: \"default\",\r\n\t\t\tsize: \"default\",\r\n\t\t},\r\n\t}\r\n);\r\n\r\nexport interface ButtonProps\r\n\textends React.ButtonHTMLAttributes<HTMLButtonElement>,\r\n\t\tVariantProps<typeof buttonVariants> {\r\n\tasChild?: boolean;\r\n}\r\n\r\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\r\n\t({ className, variant, size, asChild = false, ...props }, ref) => {\r\n\t\tconst Comp = asChild ? Slot : \"button\";\r\n\t\treturn (\r\n\t\t\t<Comp\r\n\t\t\t\tclassName={cn(buttonVariants({ variant, size, className }))}\r\n\t\t\t\tref={ref}\r\n\t\t\t\t{...props}\r\n\t\t\t/>\r\n\t\t);\r\n\t}\r\n);\r\nButton.displayName = \"Button\";\r\n\r\nexport { Button, buttonVariants };\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/cn.ts",
      "content": "import { ClassValue, clsx } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n\treturn twMerge(clsx(inputs));\r\n}\r\n",
      "type": "registry:ui"
    }
  ]
}