{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stagger-button",
  "type": "registry:ui",
  "title": "Stagger Button",
  "description": "Interactive button with animated stagger text and customizable styles for modern uis.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": [
    "motion",
    "@radix-ui/react-slot",
    "class-variance-authority",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/stagger-button.tsx",
      "content": "'use client';\nimport * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n  AnimatePresence,\n  motion,\n  type Variants,\n  useReducedMotion,\n  type Easing,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'relative text-primary underline-offset-4 hover:text-primary/90 group',\n        success: 'bg-green-600 text-white hover:bg-green-700',\n        warning: 'bg-yellow-500 text-black hover:bg-yellow-600',\n        info: 'bg-blue-500 text-white hover:bg-blue-600',\n        dark: 'bg-gray-800 text-white hover:bg-gray-700',\n        light: 'bg-gray-100 text-gray-800 hover:bg-gray-200',\n        gradient:\n          'bg-linear-to-r from-indigo-500 via-purple-500 to-pink-500 text-white hover:opacity-90',\n        glass:\n          'bg-white/10 backdrop-blur-md text-white border border-white/20 hover:bg-white/20',\n      },\n      size: {\n        default: 'h-9 px-4 py-2',\n        sm: 'h-8 rounded-md px-3 text-xs',\n        lg: 'h-10 rounded-md px-8',\n        icon: 'h-9 w-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\ninterface StaggerButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n  text?: string;\n  hoverText?: string;\n  staggerDelay?: number;\n  staggerDuration?: number;\n  direction?: 'up' | 'down' | 'alternate-even' | 'alternate-odd';\n  easing?: Easing;\n  disableStagger?: boolean;\n}\n\nconst useProcessedChars = (text: string, hoverText?: string) =>\n  React.useMemo(() => {\n    const base = Array.from(text);\n    const hover = Array.from(hoverText ?? text);\n    const max = Math.max(base.length, hover.length);\n    return {\n      safeBase: Array.from({ length: max }, (_, i) => base[i] ?? ' '),\n      safeHover: Array.from({ length: max }, (_, i) => hover[i] ?? ' '),\n    };\n  }, [text, hoverText]);\n\nconst useIsTouchDevice = () => {\n  const [isTouch, setIsTouch] = React.useState(false);\n  React.useEffect(() => {\n    const check = () =>\n      setIsTouch('ontouchstart' in window || navigator.maxTouchPoints > 0);\n    check();\n    window.addEventListener('resize', check);\n    return () => window.removeEventListener('resize', check);\n  }, []);\n  return isTouch;\n};\n\nconst getTargetY = (\n  direction: StaggerButtonProps['direction'],\n  isEven: boolean,\n) => {\n  switch (direction) {\n    case 'up':\n      return '-100%';\n    case 'down':\n      return '0%';\n    case 'alternate-even':\n      return isEven ? '-50%' : '0%';\n    case 'alternate-odd':\n      return isEven ? '0%' : '-50%';\n    default:\n      return '-50%';\n  }\n};\n\nconst StaggerButton = React.forwardRef<HTMLButtonElement, StaggerButtonProps>(\n  (\n    {\n      className,\n      variant,\n      size,\n      asChild = false,\n      text,\n      hoverText,\n      staggerDelay = 0.025,\n      staggerDuration = 0.4,\n      direction = 'down',\n      easing = [0.25, 1, 0.5, 1] as Easing,\n      disableStagger = false,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const [isHovered, setIsHovered] = React.useState(false);\n    const [isTapped, setIsTapped] = React.useState(false);\n    const displayText =\n      text ??\n      React.Children.toArray(children)\n        .map((c) => (typeof c === 'string' ? c : ''))\n        .join('') ??\n      '';\n    const { safeBase, safeHover } = useProcessedChars(displayText, hoverText);\n    const prefersReducedMotion = useReducedMotion();\n    const isTouchDevice = useIsTouchDevice();\n\n    const containerVariants: Variants = {\n      initial: {},\n      hover: {\n        transition: {\n          staggerChildren: prefersReducedMotion ? 0 : staggerDelay,\n        },\n      },\n      exit: {},\n    };\n\n    const stackVariants: Variants = {\n      initial: { y: direction === 'up' ? '0%' : '-50%' },\n      hover: ({ index, isEven }: { index: number; isEven: boolean }) =>\n        prefersReducedMotion\n          ? { y: direction === 'up' ? '0%' : '-50%' }\n          : {\n              y: getTargetY(direction, isEven),\n              transition: {\n                duration: staggerDuration,\n                delay: index * staggerDelay,\n                ease: easing,\n              },\n            },\n      exit: { y: direction === 'up' ? '0%' : '-50%' },\n    };\n\n    const handleTouchStart = (e: React.TouchEvent<HTMLButtonElement>) => {\n      if (isTouchDevice && !disableStagger) {\n        setIsTapped(true);\n        setTimeout(\n          () => setIsTapped(false),\n          staggerDuration * 1000 + safeBase.length * staggerDelay * 1000,\n        );\n      }\n      props.onTouchStart?.(e);\n    };\n\n    const shouldAnimate = disableStagger\n      ? false\n      : isTouchDevice\n        ? isTapped\n        : isHovered;\n\n    const Comp = asChild ? Slot : 'button';\n\n    return (\n      <Comp\n        className={cn(\n          buttonVariants({ variant, size }),\n          className,\n          'relative group',\n        )}\n        ref={ref}\n        onMouseEnter={() => !isTouchDevice && setIsHovered(true)}\n        onMouseLeave={() => !isTouchDevice && setIsHovered(false)}\n        onTouchStart={handleTouchStart}\n        {...props}\n      >\n        <AnimatePresence mode='wait'>\n          <motion.span\n            className='relative h-fit leading-none select-none transform-gpu will-change-transform inline-flex'\n            variants={containerVariants}\n            initial='initial'\n            exit='exit'\n            animate={shouldAnimate ? 'hover' : 'initial'}\n            style={{ perspective: 1000 }}\n          >\n            {safeBase.map((char, index) => {\n              const nextChar = safeHover[index];\n              const isSpace = char === ' ' && nextChar === ' ';\n              const visibleCharsBefore = safeBase\n                .slice(0, index)\n                .filter((c) => c !== ' ').length;\n              const isEven = visibleCharsBefore % 2 === 0;\n              const isUp = direction === 'up';\n\n              return (\n                <span\n                  key={index}\n                  className='inline-block h-[1em] align-baseline overflow-hidden relative'\n                  style={{ lineHeight: 1 }}\n                >\n                  <motion.span\n                    className='block relative'\n                    variants={stackVariants}\n                    custom={{ index, isEven }}\n                    style={{\n                      backfaceVisibility: 'hidden',\n                      transform: 'translateZ(0)',\n                      lineHeight: 1,\n                    }}\n                  >\n                    {isUp ? (\n                      <>\n                        <span className='block h-[1em] leading-none relative'>\n                          {isSpace ? '\\u00A0' : char}\n                        </span>\n                        <span className='block h-[1em] leading-none absolute top-full left-0'>\n                          {isSpace ? '\\u00A0' : nextChar}\n                        </span>\n                      </>\n                    ) : (\n                      <>\n                        {isEven && (\n                          <span className='block h-[1em] leading-none'>\n                            {isSpace ? '\\u00A0' : nextChar}\n                          </span>\n                        )}\n                        <span className='block h-[1em] leading-none'>\n                          {isSpace ? '\\u00A0' : char}\n                        </span>\n                        {!isEven && (\n                          <span className='block h-[1em] leading-none'>\n                            {isSpace ? '\\u00A0' : nextChar}\n                          </span>\n                        )}\n                      </>\n                    )}\n                  </motion.span>\n                </span>\n              );\n            })}\n          </motion.span>\n        </AnimatePresence>\n        {variant === 'link' && (\n          <span className='absolute left-1/2 bottom-2 h-px w-0 bg-current transition-all duration-300 group-hover:w-3/4 group-hover:-translate-x-1/2' />\n        )}\n      </Comp>\n    );\n  },\n);\n\nStaggerButton.displayName = 'StaggerButton';\n\nexport { StaggerButton, buttonVariants };\nexport type { StaggerButtonProps };\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
