{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "button", "title": "Button (Base UI)", "description": "Animated button with variants (primary, neutral, destructive, secondary, tertiary, ghost), icon support, and loading spinner. Base UI flavor.", "dependencies": [ "framer-motion", "@base-ui/react", "class-variance-authority" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils", "https://zeron-ui.vercel.app/r/shape-context.json", "https://zeron-ui.vercel.app/r/icon-context.json" ], "files": [ { "path": "src/components/ui/button.tsx", "content": "\"use client\";\n\nimport {\n cloneElement,\n forwardRef,\n isValidElement,\n type ButtonHTMLAttributes,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { IconComponent } from \"@/lib/icon-context\";\nimport { cn } from \"@/lib/utils\";\nimport { useShape } from \"@/lib/shape-context\";\n\nconst buttonVariants = cva(\n [\n \"group relative isolate inline-flex items-center justify-center outline-none cursor-pointer\",\n \"transition-colors duration-fast\",\n \"disabled:opacity-50 disabled:pointer-events-none\",\n \"focus-visible:ring-1 focus-visible:ring-focus-ring\",\n ],\n {\n variants: {\n variant: {\n primary: \"text-fg-on-brand\",\n neutral: \"text-fg-on-inverse\",\n destructive: \"text-fg-on-danger\",\n secondary: \"text-fg-default\",\n tertiary: \"text-fg-default\",\n ghost: \"text-fg-muted hover:text-fg-default\",\n },\n size: {\n sm: \"h-control-xs px-3 text-label gap-1\",\n md: \"h-control-sm px-4 text-body gap-1.5\",\n lg: \"h-control-md px-5 text-body gap-1.5\",\n \"icon-sm\": \"h-control-sm w-8 p-0 [&_svg]:h-3.5 [&_svg]:w-3.5\",\n icon: \"h-control-md w-9 p-0 [&_svg]:h-4 [&_svg]:w-4\",\n \"icon-lg\": \"h-control-lg w-10 p-0 [&_svg]:h-5 [&_svg]:w-5\",\n },\n iconLeft: { true: \"\" },\n iconRight: { true: \"\" },\n },\n compoundVariants: [\n { size: \"sm\", iconLeft: true, className: \"pl-1.5\" },\n { size: \"md\", iconLeft: true, className: \"pl-2.5\" },\n { size: \"lg\", iconLeft: true, className: \"pl-3.5\" },\n { size: \"sm\", iconRight: true, className: \"pr-1.5\" },\n { size: \"md\", iconRight: true, className: \"pr-2.5\" },\n { size: \"lg\", iconRight: true, className: \"pr-3.5\" },\n ],\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n }\n);\n\ninterface ButtonProps\n extends ButtonHTMLAttributes,\n VariantProps {\n /** When true, the given single React-element child becomes the rendered element (slot-style). */\n asChild?: boolean;\n loading?: boolean;\n leadingIcon?: IconComponent;\n trailingIcon?: IconComponent;\n /** Force the visual pressed/held state. Useful when the button drives an\n * external open piece of UI (a popover, dropdown, etc.) so it reads as\n * engaged while the menu is showing. */\n active?: boolean;\n}\n\nconst bgVariants: Record = {\n primary: \"bg-brand group-hover:bg-brand-hover group-active:bg-brand-active\",\n neutral: \"bg-inverse-background group-hover:bg-inverse-background-hover group-active:bg-inverse-background-active\",\n destructive: \"bg-destructive group-hover:bg-destructive-hover group-active:bg-destructive-active\",\n secondary: \"bg-secondary-action group-hover:bg-secondary-action-hover group-active:bg-secondary-action-active\",\n tertiary: \"border border-border bg-transparent group-hover:bg-hover group-active:bg-active\",\n ghost: \"bg-transparent group-hover:bg-hover group-active:bg-active\",\n};\n\nconst activeBgVariants: Record = {\n primary: \"bg-brand-active\",\n neutral: \"bg-inverse-background-active\",\n destructive: \"bg-destructive-active\",\n secondary: \"bg-secondary-action-active\",\n tertiary: \"border border-border bg-active\",\n ghost: \"bg-active\",\n};\n\nconst Button = forwardRef(\n (\n {\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leadingIcon: LeadingIcon,\n trailingIcon: TrailingIcon,\n active = false,\n disabled,\n children,\n style,\n ...props\n },\n ref\n ) => {\n // asChild: the user's element becomes the root while the button's internal\n // structure (bg layer, content wrapper, spinner, icons) survives as its\n // children — the element's own children become the label. We clone the\n // element directly instead of routing through ButtonPrimitive's `render`:\n // Base UI would bolt button semantics (role=\"button\", Space activation)\n // onto e.g. a link, where plain-link output is wanted.\n const asChildElement =\n asChild && isValidElement(children)\n ? (children as ReactElement<{\n children?: ReactNode;\n className?: string;\n style?: React.CSSProperties;\n ref?: React.Ref;\n }>)\n : null;\n const label = asChildElement ? asChildElement.props.children : children;\n const isIconOnly = size === \"icon\" || size === \"icon-sm\" || size === \"icon-lg\";\n const iconSize = size === \"sm\" ? 14 : size === \"lg\" ? 20 : 16;\n // Spinner box tracks the button height (sm is h-control-xs, lg/icon are h-control-md, …) so\n // the loading glyph stays proportionate across sizes.\n const spinnerSizeClass =\n size === \"sm\"\n ? \"h-control-xs w-7\"\n : size === \"lg\" || size === \"icon\"\n ? \"h-control-md w-9\"\n : size === \"icon-lg\"\n ? \"h-control-lg w-10\"\n : \"h-control-sm w-8\";\n const shape = useShape();\n const bgClass = active\n ? activeBgVariants[variant ?? \"primary\"]\n : bgVariants[variant ?? \"primary\"];\n\n const internals = (\n <>\n \n \n {loading ? (\n <>\n \n {LeadingIcon && !isIconOnly && (\n \n )}\n {label}\n {TrailingIcon && !isIconOnly && (\n \n )}\n \n \n \n \n \n \n \n ) : isIconOnly ? (\n \n {label}\n \n ) : (\n <>\n {LeadingIcon && (\n \n )}\n {label}\n {TrailingIcon && (\n \n )}\n \n )}\n \n \n );\n\n const rootClassName = cn(\n buttonVariants({\n variant,\n size,\n iconLeft: !isIconOnly && !!LeadingIcon,\n iconRight: !isIconOnly && !!TrailingIcon,\n }),\n shape.button,\n className\n );\n\n if (asChildElement) {\n const childProps = asChildElement.props;\n return cloneElement(\n asChildElement,\n {\n ...props,\n ref,\n className: cn(rootClassName, childProps.className),\n style: { ...style, ...childProps.style },\n },\n internals\n );\n }\n\n return (\n }\n className={rootClassName}\n disabled={disabled || loading}\n style={style}\n {...props}\n >\n {internals}\n \n );\n }\n);\n\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\nexport type { ButtonProps };\n", "type": "registry:ui", "target": "components/ui/button.tsx" } ], "css": { "@keyframes spinner-move": { "to": { "stroke-dashoffset": "-100" } }, "@keyframes spinner-dash": { "0%, 100%": { "stroke-dasharray": "15 85" }, "50%": { "stroke-dasharray": "40 60" } } }, "type": "registry:ui" }