{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stagger-button",
  "title": "Stagger Button",
  "description": "A button that staggers per character or slides the whole label on hover, with a scaling background layer.",
  "dependencies": ["class-variance-authority"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/primitives/buttons/stagger-button/index.tsx",
      "content": "/** biome-ignore-all lint/suspicious/noArrayIndexKey: Character order is stable for static button labels. */\n\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  type ComponentPropsWithoutRef,\n  cloneElement,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type Ref,\n  type RefCallback,\n  type RefObject,\n} from \"react\";\n\nconst buttonBaseVariants = cva(\n  \"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        accent: \"bg-accent text-accent-foreground hover:bg-accent/90\",\n        neutral:\n          \"bg-neutral-100 text-neutral-700 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-9 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n        md: \"h-10 rounded-md px-5 has-[>svg]:px-4\",\n        lg: \"h-11 rounded-lg px-6 text-[15px] has-[>svg]:px-6\",\n        icon: \"size-10\",\n        \"icon-sm\": \"size-9\",\n        \"icon-xs\": \"size-8\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nconst staggerButtonVariants = cva(\n  \"group relative bg-transparent! no-underline\",\n  {\n    variants: {\n      withBg: {\n        true: \"z-[1] transition-[scale,color] duration-500 ease-[cubic-bezier(0.165,0.84,0.44,1)] active:scale-[0.98] motion-reduce:transition-none motion-reduce:active:scale-100\",\n        false: \"\",\n      },\n    },\n    defaultVariants: {\n      withBg: true,\n    },\n  }\n);\n\nconst staggerButtonBgVariants = cva(\n  \"pointer-events-none absolute inset-0 rounded-[inherit] transition-[scale] duration-500 ease-[cubic-bezier(0.165,0.84,0.44,1)] group-hover:scale-[0.97] motion-reduce:transition-none motion-reduce:group-hover:scale-100\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary\",\n        accent: \"bg-accent\",\n        neutral: \"bg-neutral-100 dark:bg-neutral-900\",\n        destructive: \"bg-destructive dark:bg-destructive/60\",\n        outline: \"bg-background dark:bg-input/30\",\n        secondary: \"bg-secondary\",\n        ghost: \"bg-transparent\",\n        link: \"bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n);\n\nconst staggerButtonTextVariants = cva(\"flex items-center whitespace-nowrap\");\n\nconst staggerButtonCharsVariants = cva(\n  \"relative inline-block overflow-hidden whitespace-nowrap\"\n);\n\nconst staggerButtonCharVariants = cva(\n  \"relative inline-block transition-transform duration-500 ease-[cubic-bezier(0.165,0.84,0.44,1)] [text-shadow:0_2em_0_currentColor] group-hover:-translate-y-[2em] motion-reduce:transition-none motion-reduce:group-hover:translate-y-0\"\n);\n\nconst WHITESPACE_SPLIT_REGEX = /\\s+/;\n\n/** Variants rendered without the scaling background layer. */\nconst FLAT_VARIANTS = new Set<string>([\"ghost\", \"link\"]);\n\ntype WithAsChild<Base extends object> =\n  | (Base & { asChild: true; children: ReactElement })\n  | (Base & { asChild?: false | undefined; children?: string });\n\ntype StaggerButtonBaseProps = Omit<ComponentPropsWithoutRef<\"a\">, \"children\"> &\n  VariantProps<typeof buttonBaseVariants> & {\n    /** Override the scaling background layer (solid variants only). */\n    bgClassName?: string;\n    /** Override each animated character span. */\n    charClassName?: string;\n    /** Override each per-word overflow wrapper. */\n    charsClassName?: string;\n    /** Alternative to string `children` — required when `asChild` child has no string content. */\n    label?: string;\n    /**\n     * Animate per character with stagger, or slide the whole label as one block.\n     * @default \"char\"\n     */\n    stagger?: \"char\" | \"text\";\n    /**\n     * Per-character transition delay increment in seconds. Ignored when `stagger` is `\"text\"`.\n     * @default 0.01\n     */\n    staggerDelay?: number;\n    /** Override the text wrapper. */\n    textClassName?: string;\n  };\n\nexport type StaggerButtonProps = WithAsChild<StaggerButtonBaseProps>;\n\ninterface StaggerAnimatedLabelProps {\n  bgClassName?: string;\n  charClassName?: string;\n  charsClassName?: string;\n  stagger: NonNullable<StaggerButtonProps[\"stagger\"]>;\n  staggerDelay: number;\n  text: string;\n  textClassName?: string;\n  variant: NonNullable<StaggerButtonProps[\"variant\"]>;\n}\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {\n  return (node) => {\n    for (const ref of refs) {\n      if (!ref) {\n        continue;\n      }\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else {\n        (ref as RefObject<T | null>).current = node;\n      }\n    }\n  };\n}\n\nfunction getButtonLabel(\n  asChild: boolean,\n  children: string | ReactElement | undefined,\n  label: string | undefined\n): string {\n  if (asChild && isValidElement(children)) {\n    const childText = (children.props as { children?: unknown }).children;\n\n    if (typeof childText === \"string\") {\n      return childText;\n    }\n\n    return label ?? \"\";\n  }\n\n  if (typeof children === \"string\") {\n    return children;\n  }\n\n  return label ?? \"\";\n}\n\nfunction StaggerAnimatedLabel({\n  variant,\n  text,\n  stagger,\n  staggerDelay,\n  bgClassName,\n  textClassName,\n  charsClassName,\n  charClassName,\n}: StaggerAnimatedLabelProps) {\n  const words =\n    stagger === \"text\"\n      ? [text]\n      : text.trim().split(WHITESPACE_SPLIT_REGEX).filter(Boolean);\n  let charIndex = 0;\n\n  return (\n    <>\n      {FLAT_VARIANTS.has(variant) ? null : (\n        <div\n          aria-hidden=\"true\"\n          className={cn(staggerButtonBgVariants({ variant }), bgClassName)}\n        />\n      )}\n      <span\n        aria-hidden={text ? true : undefined}\n        className={cn(staggerButtonTextVariants(), textClassName)}\n      >\n        {words.map((word, wordIndex) => (\n          <Fragment key={`${word}-${wordIndex}`}>\n            {wordIndex > 0 ? (\n              <span className=\"shrink-0 whitespace-pre\"> </span>\n            ) : null}\n            <span\n              className={cn(staggerButtonCharsVariants(), charsClassName)}\n              data-button-animate-chars=\"\"\n            >\n              {stagger === \"text\" ? (\n                <span\n                  className={cn(staggerButtonCharVariants(), charClassName)}\n                >\n                  {word}\n                </span>\n              ) : (\n                [...word].map((char, index) => (\n                  <span\n                    className={cn(staggerButtonCharVariants(), charClassName)}\n                    key={`${char}-${index}`}\n                    style={{\n                      transitionDelay: `${charIndex++ * staggerDelay}s`,\n                    }}\n                  >\n                    {char}\n                  </span>\n                ))\n              )}\n            </span>\n          </Fragment>\n        ))}\n      </span>\n    </>\n  );\n}\n\nfunction StaggerButton({\n  asChild = false,\n  variant = \"default\",\n  size = \"default\",\n  stagger = \"char\",\n  staggerDelay = 0.01,\n  label,\n  children,\n  className,\n  bgClassName,\n  textClassName,\n  charsClassName,\n  charClassName,\n  href,\n  ref,\n  \"aria-label\": ariaLabel,\n  ...props\n}: StaggerButtonProps & {\n  ref?: Ref<HTMLAnchorElement>;\n}) {\n  const text = getButtonLabel(asChild, children, label);\n  const resolvedVariant = variant ?? \"default\";\n  const rootClassName = cn(\n    buttonBaseVariants({ variant, size }),\n    staggerButtonVariants({ withBg: !FLAT_VARIANTS.has(resolvedVariant) }),\n    className\n  );\n  const animatedLabel = (\n    <StaggerAnimatedLabel\n      bgClassName={bgClassName}\n      charClassName={charClassName}\n      charsClassName={charsClassName}\n      stagger={stagger}\n      staggerDelay={staggerDelay}\n      text={text}\n      textClassName={textClassName}\n      variant={resolvedVariant}\n    />\n  );\n\n  if (asChild) {\n    if (!isValidElement(children)) {\n      return null;\n    }\n\n    const {\n      ref: childRef,\n      className: childClassName,\n      \"aria-label\": childAriaLabel,\n      ...childProps\n    } = children.props as {\n      ref?: Ref<HTMLElement>;\n      className?: string;\n      \"aria-label\"?: string;\n    };\n\n    return cloneElement(\n      children,\n      {\n        ...childProps,\n        ...props,\n        \"aria-label\": ariaLabel ?? childAriaLabel ?? (text || undefined),\n        className: cn(childClassName, rootClassName),\n        ref: mergeRefs(childRef, ref),\n      } as Record<string, unknown>,\n      animatedLabel\n    );\n  }\n\n  return (\n    <a\n      aria-label={ariaLabel ?? (text || undefined)}\n      className={rootClassName}\n      href={href}\n      ref={ref}\n      {...props}\n    >\n      {animatedLabel}\n    </a>\n  );\n}\n\nexport {\n  StaggerButton,\n  staggerButtonBgVariants,\n  staggerButtonCharsVariants,\n  staggerButtonCharVariants,\n  staggerButtonTextVariants,\n  staggerButtonVariants,\n};\n\nexport type StaggerButtonVariant = NonNullable<StaggerButtonProps[\"variant\"]>;\nexport type StaggerButtonSize = NonNullable<StaggerButtonProps[\"size\"]>;\n",
      "type": "registry:ui",
      "target": "components/sora-ui/buttons/stagger-button.tsx"
    }
  ],
  "meta": {
    "demoProps": {
      "StaggerButton": {
        "label": {
          "value": "Staggering Button"
        },
        "variant": {
          "value": "default",
          "options": {
            "Default": "default",
            "Accent": "accent",
            "Neutral": "neutral",
            "Destructive": "destructive",
            "Outline": "outline",
            "Secondary": "secondary",
            "Ghost": "ghost",
            "Link": "link"
          }
        },
        "size": {
          "value": "default",
          "options": {
            "Small": "sm",
            "Default": "default",
            "Medium": "md",
            "Large": "lg"
          }
        },
        "stagger": {
          "value": "char",
          "options": {
            "Char": "char",
            "Text": "text"
          }
        }
      }
    }
  },
  "type": "registry:ui"
}
