{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "button", "type": "registry:ui", "title": "Button", "description": "The action button. Labels wrap to two lines rather than overflowing, so long translated text stays inside the pill.", "categories": [ "actions" ], "dependencies": [ "class-variance-authority" ], "registryDependencies": [ "https://whiskeyjack.net/r/utils.json" ], "files": [ { "path": "components/ui/button.tsx", "type": "registry:ui", "target": "components/ui/button.tsx", "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n [\n // Labels wrap to two lines (no whitespace-nowrap) and the sizes below use\n // min-h, not a fixed height, so a long label -- common in translated UIs --\n // breaks inside the button instead of overflowing it. text-center keeps a\n // wrapped label centered.\n \"inline-flex items-center justify-center gap-2 text-center\",\n \"text-sm font-medium transition-all duration-200\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n ],\n {\n variants: {\n variant: {\n default: [\n \"bg-[var(--color-neutral-900)] text-white\",\n \"hover:bg-[var(--color-neutral-800)]\",\n \"focus-visible:ring-[var(--color-neutral-500)]\",\n \"dark:bg-white dark:text-[var(--color-neutral-900)]\",\n \"dark:hover:bg-[var(--color-neutral-100)]\",\n ],\n secondary: [\n \"bg-[var(--color-neutral-100)] text-[var(--color-neutral-900)]\",\n \"hover:bg-[var(--color-neutral-200)]\",\n // Was neutral-800, which is exactly surface-dark -- so a secondary\n // button inside a Card had no fill and no border in dark mode, and\n // read as bare text.\n \"dark:bg-[var(--color-surface-muted-dark)] dark:text-[var(--color-neutral-50)]\",\n \"dark:hover:bg-[var(--color-neutral-700)]\",\n ],\n outline: [\n \"border border-[var(--color-border-light)] bg-transparent\",\n \"hover:bg-[var(--color-neutral-100)]\",\n \"dark:border-[var(--color-border-dark)]\",\n // The border kept it visible; the hover did not exist on a Card.\n \"dark:hover:bg-[var(--color-surface-muted-dark)]\",\n ],\n ghost: [\n \"hover:bg-[var(--color-neutral-100)]\",\n // No border to fall back on, so on a Card this was a button with no\n // hover feedback at all in dark mode.\n \"dark:hover:bg-[var(--color-surface-muted-dark)]\",\n ],\n destructive: [\n \"bg-[var(--color-error-500)] text-white\",\n \"hover:bg-[var(--color-error-700)]\",\n \"focus-visible:ring-[var(--color-error-500)]\",\n ],\n accent: [\n \"bg-[var(--color-accent-500)] text-[var(--color-accent-foreground)]\",\n \"hover:bg-[var(--color-accent-400)]\",\n \"active:bg-[var(--color-accent-600)]\",\n \"focus-visible:ring-[var(--color-accent-500)]\",\n ],\n link: [\n \"text-[var(--color-neutral-900)] underline-offset-4\",\n \"hover:underline\",\n \"dark:text-white\",\n ],\n },\n size: {\n // min-h (not h) so the button grows to fit a wrapped two-line label;\n // icon is a fixed square (no text, never wraps).\n sm: \"min-h-8 rounded-full px-3 py-1 text-xs\",\n default: \"min-h-10 rounded-full px-6 py-2\",\n lg: \"min-h-12 rounded-full px-8 py-2 text-base\",\n icon: \"h-10 w-10 rounded-full\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes,\n VariantProps {\n /**\n * Render the button styling onto the single child element instead of a\n * ``. The child's\n * own props win on conflict; classNames merge.\n */\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef(\n ({ className, variant, size, asChild = false, children, ...props }, ref) => {\n const classes = cn(buttonVariants({ variant, size, className }));\n if (asChild) {\n // Minimal slot (no Radix dependency): clone the single child with the\n // button classes and remaining props merged in.\n const child = React.Children.only(children) as React.ReactElement<\n Record\n >;\n const mergedProps: Record = {\n ...props,\n ...child.props,\n className: cn(classes, child.props.className as string | undefined),\n ref,\n };\n return React.cloneElement(child, mergedProps);\n }\n return (\n \n );\n }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n" } ], "docs": "Use the default size (40px) for every action button in an app; it matches size=\"icon\" so text and icon buttons align. Reserve size=\"lg\" for hero and landing CTAs. Avoid size=\"sm\", which breaks rhythm against neighbouring default buttons. The accent variant is the primary action, outline the secondary one.", "meta": { "group": "actions", "related": [ "button-row", "confirm-button", "drawer-action" ], "exports": [ "Button" ], "siteSlug": "button" } }