{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-title-animation",
  "type": "registry:ui",
  "title": "Hero Title Animation",
  "description": "An animated hero title with phased brand reveal, staggered word blur-in, and highlight box sweep effect.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/hero-title-animation.tsx",
      "content": "\"use client\";\n\nimport {\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n  useLayoutEffect,\n} from \"react\";\n\ninterface HeroTitleAnimationProps {\n  className?: string;\n}\n\nexport function HeroTitleAnimation({ className }: HeroTitleAnimationProps) {\n  const [phase, setPhase] = useState<\n    \"measuring\" | \"fade-in\" | \"move\" | \"reveal\" | \"highlight\" | \"done\"\n  >(\"measuring\");\n\n  const [brandOffsetX, setBrandOffsetX] = useState(0);\n  const [titleOffsetY, setTitleOffsetY] = useState(0);\n  const [ready, setReady] = useState(false);\n\n  const measureBrandRef = useRef<HTMLSpanElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const timersRef = useRef<NodeJS.Timeout[]>([]);\n\n  const brandWords = [\"Make\", \"your\", \"websites\"];\n  const highlightWords = [\"feel\", \"premium\", \"instantly\"];\n\n  const titleCx =\n    \"flex flex-wrap items-baseline justify-center gap-x-[0.1em] gap-y-2 md:gap-y-3 text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-semibold tracking-tight text-foreground leading-[1.15]\";\n\n  /* ─── Measure BEFORE first paint ─── */\n  useLayoutEffect(() => {\n    const brand = measureBrandRef.current;\n    const container = containerRef.current;\n    if (!brand || !container) return;\n\n    const brandRect = brand.getBoundingClientRect();\n    const containerRect = container.getBoundingClientRect();\n\n    const brandCenterX = brandRect.left + brandRect.width / 2;\n    const containerCenterX = containerRect.left + containerRect.width / 2;\n    setBrandOffsetX(containerCenterX - brandCenterX);\n\n    const brandMidY = brandRect.top - containerRect.top + brandRect.height / 2;\n    const containerMidY = containerRect.height / 2;\n    setTitleOffsetY(containerMidY - brandMidY);\n\n    setReady(true);\n  }, []);\n\n  const clearTimers = useCallback(() => {\n    timersRef.current.forEach(clearTimeout);\n    timersRef.current = [];\n  }, []);\n\n  const startSequence = useCallback(() => {\n    clearTimers();\n    setPhase(\"fade-in\");\n    timersRef.current.push(setTimeout(() => setPhase(\"move\"), 1200));\n    timersRef.current.push(setTimeout(() => setPhase(\"reveal\"), 1800));\n    timersRef.current.push(setTimeout(() => setPhase(\"highlight\"), 2500));\n    timersRef.current.push(setTimeout(() => setPhase(\"done\"), 3100));\n  }, [clearTimers]);\n\n  useEffect(() => {\n    if (!ready) return;\n    startSequence();\n    return clearTimers;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [ready]);\n\n  useEffect(() => clearTimers, [clearTimers]);\n\n  /* ─── Derived flags ─── */\n  const brandVisible = phase !== \"measuring\";\n  const brandMoved =\n    phase === \"move\" ||\n    phase === \"reveal\" ||\n    phase === \"highlight\" ||\n    phase === \"done\";\n  const showReveal =\n    phase === \"reveal\" || phase === \"highlight\" || phase === \"done\";\n  const showHighlight = phase === \"highlight\" || phase === \"done\";\n\n  return (\n    <div\n      ref={containerRef}\n      className={`relative w-full pb-3 md:pb-4 ${className ?? \"\"}`}\n    >\n      {/* ── Hidden sizing layer ── */}\n      <h1\n        className={`${titleCx} invisible pointer-events-none`}\n        aria-hidden=\"true\"\n      >\n        <span\n          ref={measureBrandRef}\n          className=\"inline-flex flex-wrap items-baseline justify-center gap-x-[0.3em] font-medium\"\n        >\n          {brandWords.map((w, i) => (\n            <span key={i} className=\"inline-block\">\n              {w}\n            </span>\n          ))}\n        </span>\n        <span className=\"inline-flex items-center gap-x-[0.3em] whitespace-nowrap mt-1 md:mt-2 ml-2 sm:ml-3 md:ml-4\">\n          <span className=\"relative inline-flex items-center gap-x-[0.3em]\">\n            {highlightWords.map((w, i) => (\n              <span key={i} className=\"font-bold\">\n                {w}\n              </span>\n            ))}\n          </span>\n        </span>\n      </h1>\n\n      {/* ── Visible animated title ── */}\n      <h1\n        className={`${titleCx} absolute inset-x-0 top-0`}\n        style={{\n          visibility: ready ? \"visible\" : \"hidden\",\n          transform: `translateY(${brandMoved ? 0 : titleOffsetY}px)`,\n          transition: brandMoved\n            ? \"transform 0.75s cubic-bezier(0.22, 1, 0.36, 1)\"\n            : \"none\",\n        }}\n      >\n        {/* \"Built for Your Foundation\" — fades in centered, then slides to position */}\n        <span\n          className=\"inline-flex flex-wrap items-baseline justify-center gap-x-[0.3em] font-medium text-foreground/85\"\n          style={{\n            opacity: brandVisible ? 1 : 0,\n            transform: `translateX(${brandMoved ? 0 : brandOffsetX}px)`,\n            transition: brandMoved\n              ? \"opacity 0.55s ease-out, transform 0.75s cubic-bezier(0.22, 1, 0.36, 1)\"\n              : \"opacity 0.55s ease-out\",\n          }}\n        >\n          {brandWords.map((w, i) => (\n            <span key={i} className=\"inline-block\">\n              {w}\n            </span>\n          ))}\n        </span>\n\n        {/* \"of Design Engineering\" — reveals together, only \"Design Engineering\" gets highlight */}\n        <span\n          className=\"inline-flex items-center gap-x-[0.3em] whitespace-nowrap mt-1 md:mt-2 ml-2 sm:ml-3 md:ml-4\"\n          style={{\n            opacity: showReveal ? 1 : 0,\n            transform: `translateY(${showReveal ? 0 : 14}px)`,\n            filter: `blur(${showReveal ? 0 : 8}px)`,\n            transition:\n              \"opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94), transform 0.5s cubic-bezier(0.25,0.46,0.45,0.94), filter 0.5s cubic-bezier(0.25,0.46,0.45,0.94)\",\n          }}\n        >\n          {/* Highlight sub-group */}\n          <span className=\"relative inline-flex items-center gap-x-[0.3em]\">\n            {/* Highlight box sweeps left → right */}\n            <span\n              className=\"absolute -inset-x-1.5 -inset-y-0.5 sm:-inset-x-2 sm:-inset-y-1 md:-inset-x-2.5 md:-inset-y-1 rounded-xl sm:rounded-2xl bg-foreground pointer-events-none\"\n              style={{\n                transformOrigin: \"left center\",\n                transform: `scaleX(${showHighlight ? 1 : 0})`,\n                transition: \"transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)\",\n              }}\n            />\n            {highlightWords.map((word, i) => (\n              <span\n                key={`hl-${i}`}\n                className={`relative z-10 font-bold transition-colors duration-300 ${\n                  showHighlight\n                    ? \"text-background delay-200\"\n                    : \"text-foreground/85\"\n                }`}\n              >\n                {word}\n              </span>\n            ))}\n          </span>\n        </span>\n      </h1>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/hero-title-animation.tsx"
    }
  ]
}