{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "A button component",
  "dependencies": [
    "motion",
    "@radix-ui/react-use-callback-ref"
  ],
  "registryDependencies": [
    "https://ui.ednesdayw.com/r/use-ripple.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/button.tsx",
      "content": "\"use client\";\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useRipple } from \"@/hooks/use-ripple\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { Loader2Icon } from \"lucide-react\";\nimport { AnimatePresence, LazyMotion, domAnimation, m } from \"motion/react\";\nimport { useCallback, useState } from \"react\";\n\nconst buttonVariants = cva(\n  cn(\n    \"inline-flex items-center justify-center gap-2 shrink-0 outline-none relative overflow-hidden\",\n    \"text-sm font-medium whitespace-nowrap rounded-lg transition-all cursor-pointer\",\n    \"active:scale-98 transition-all duration-100\",\n    \"disabled:pointer-events-none disabled:opacity-70\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4\",\n    \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n    \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-invalid:dark:ring-destructive/40\",\n  ),\n  {\n    variants: {\n      variant: {\n        primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive: cn(\n          \"bg-destructive text-white hover:bg-destructive/90\",\n          \"focus-visible:ring-destructive/20 focus-visible:dark:ring-destructive/40 dark:bg-destructive/60\",\n        ),\n        outline:\n          \"border bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground \",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        sm: \"h-8 gap-1.5 px-3 has-[>svg]:px-2.5\",\n        md: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        lg: \"h-10 px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"primary\",\n      size: \"md\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant,\n  size,\n  disabled = false,\n  asChild = false,\n  loading,\n  setLoading,\n  onClick,\n  children,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n    loading?: boolean;\n    setLoading?: (loading: boolean) => void;\n  }) {\n  const {\n    ripples,\n    onClick: handleRippleClick,\n    onClear: handleRippleClear,\n  } = useRipple();\n  const handleLoading = useCallbackRef(setLoading);\n\n  const [uncontrolledLoading, setUncontrolledLoading] = useState(\n    loading ?? false,\n  );\n  const isLoading = loading ?? uncontrolledLoading;\n  const isControlled = loading !== undefined;\n\n  const onLoadingChange = useCallback(\n    (loading: boolean) => {\n      if (!isControlled) {\n        setUncontrolledLoading(loading);\n      }\n      handleLoading?.(loading);\n    },\n    [isControlled, handleLoading],\n  );\n\n  const Comp = asChild ? Slot : \"button\";\n\n  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n    handleRippleClick(event);\n    if (!onClick) return;\n\n    if (\n      typeof onClick === \"function\" &&\n      onClick.constructor.name === \"AsyncFunction\"\n    ) {\n      onLoadingChange(true);\n      Promise.resolve(onClick(event)).finally(() => {\n        onLoadingChange(false);\n      });\n      return;\n    }\n\n    onClick(event);\n  };\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size }), className)}\n      disabled={disabled || isLoading}\n      onClick={handleClick}\n      {...props}\n    >\n      {isLoading && (\n        <>\n          <Loader2Icon\n            className=\"size-4 shrink-0 animate-spin\"\n            aria-hidden=\"true\"\n          />\n          <span className=\"sr-only\">Loading</span>\n        </>\n      )}\n      <Slottable>{children}</Slottable>\n      {ripples.map((ripple) => {\n        const duration = Math.min(\n          Math.max(0.01 * ripple.size, 0.2),\n          ripple.size > 100 ? 0.75 : 0.5,\n        );\n        return (\n          <LazyMotion key={ripple.id} features={domAnimation}>\n            <AnimatePresence mode=\"popLayout\">\n              <m.span\n                initial={{\n                  transform: \"scale(0)\",\n                  opacity: 0.35,\n                }}\n                animate={{ transform: \"scale(2)\", opacity: 0 }}\n                exit={{ opacity: 0 }}\n                style={{\n                  position: \"absolute\",\n                  backgroundColor: \"currentColor\",\n                  borderRadius: \"100%\",\n                  transformOrigin: \"center\",\n                  pointerEvents: \"none\",\n                  overflow: \"hidden\",\n                  inset: 0,\n                  zIndex: 0,\n                  top: ripple.y,\n                  left: ripple.x,\n                  width: `${ripple.size}px`,\n                  height: `${ripple.size}px`,\n                }}\n                transition={{ duration }}\n                onAnimationComplete={() => {\n                  handleRippleClear(ripple.id);\n                }}\n              />\n            </AnimatePresence>\n          </LazyMotion>\n        );\n      })}\n    </Comp>\n  );\n}\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui",
      "target": "components/wed/button.tsx"
    }
  ]
}