{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-roll-button",
  "type": "registry:ui",
  "title": "Text Roll Button",
  "description": "A pill button whose label rolls up character by character on hover — the current text lifts out the top as a fresh copy rolls in from below, on a fine per-character stagger.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/text-roll-button.tsx",
      "type": "registry:component",
      "target": "components/tweenui/text-roll-button.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { SplitText } from 'gsap/SplitText';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, SplitText);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface TextRollButtonProps extends ComponentPropsWithRef<'button'> {\n  /** Button label. Must be a string — it is duplicated and split per character. */\n  children: string;\n}\n\nconst TextRollButton = ({\n  children,\n  className,\n  type = 'button',\n  ref,\n  ...props\n}: TextRollButtonProps) => {\n  const btnRef = useRef<HTMLButtonElement>(null);\n\n  // Merge the internal ref (needed for GSAP) with any forwarded ref.\n  const setRef = (node: HTMLButtonElement | null) => {\n    btnRef.current = node;\n    if (typeof ref === 'function') ref(node);\n    else if (ref) ref.current = node;\n  };\n\n  useGSAP(\n    () => {\n      if (prefersReducedMotion()) return;\n      const btn = btnRef.current;\n      const upperEl = btn?.querySelector<HTMLElement>('[data-roll-upper]');\n      const lowerEl = btn?.querySelector<HTMLElement>('[data-roll-lower]');\n      if (!btn || !upperEl || !lowerEl) return;\n\n      const upper = new SplitText(upperEl, { type: 'chars', tag: 'span' });\n      const lower = new SplitText(lowerEl, { type: 'chars', tag: 'span' });\n      // Both copies overlap; the lower one sits one line below (clipped) until\n      // hover rolls it up while the upper rolls out the top. Reveal the lower\n      // wrapper — CSS keeps it hidden until GSAP is ready.\n      gsap.set(lowerEl, { opacity: 1 });\n      gsap.set(upper.chars, { display: 'inline-block', yPercent: 0, opacity: 1 });\n      gsap.set(lower.chars, { display: 'inline-block', yPercent: 100, opacity: 0 });\n\n      const anim = {\n        duration: 0.4,\n        ease: 'power2.inOut',\n        stagger: 0.0125,\n        overwrite: 'auto' as const,\n      };\n      const roll = () => {\n        gsap.to(upper.chars, { yPercent: -100, opacity: 0, ...anim });\n        gsap.to(lower.chars, { yPercent: 0, opacity: 1, ...anim });\n      };\n      const reset = () => {\n        gsap.to(upper.chars, { yPercent: 0, opacity: 1, ...anim });\n        gsap.to(lower.chars, { yPercent: 100, opacity: 0, ...anim });\n      };\n\n      btn.addEventListener('mouseenter', roll);\n      btn.addEventListener('mouseleave', reset);\n      btn.addEventListener('focus', roll);\n      btn.addEventListener('blur', reset);\n\n      return () => {\n        btn.removeEventListener('mouseenter', roll);\n        btn.removeEventListener('mouseleave', reset);\n        btn.removeEventListener('focus', roll);\n        btn.removeEventListener('blur', reset);\n        upper.revert();\n        lower.revert();\n      };\n    },\n    { scope: btnRef }\n  );\n\n  return (\n    <button\n      ref={setRef}\n      type={type}\n      className={cn(\n        'inline-flex h-12 cursor-pointer items-center justify-center rounded-full border border-transparent bg-[#045f64] px-6 text-sm font-medium text-[#c6f56f] shadow-[0_1px_1px_rgba(16,24,40,0.16)] transition-colors duration-300 hover:bg-[#045f64]/95 focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 dark:ring-offset-[#12161F]',\n        className\n      )}\n      {...props}\n    >\n      <span className=\"relative inline-block overflow-hidden leading-[1.4]\">\n        <span data-roll-upper className=\"block will-change-transform\">\n          {children}\n        </span>\n        <span\n          data-roll-lower\n          aria-hidden=\"true\"\n          className=\"absolute top-0 left-0 block opacity-0 will-change-transform motion-reduce:hidden\"\n        >\n          {children}\n        </span>\n      </span>\n    </button>\n  );\n};\n\nexport default TextRollButton;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/text-roll-button\nGSAP plugins (SplitText) are registered inside the component, so there is nothing to set up."
}