{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "button", "type": "registry:ui", "title": "TAC Button", "description": "Button primitive with brutalist offset shadow, premium focus bloom, lift-on-hover, and the TAC-specific `glow` variant. Replaces the bare shadcn 4.7.0 Button.", "dependencies": [ "class-variance-authority" ], "registryDependencies": [ "@radix-ui/react-slot" ], "files": [ { "path": "src/components/button.tsx", "type": "registry:ui", "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@workspace/ui/lib/utils\"\n\n/**\n * Button — Violet Grid v6 primitive.\n *\n * v6 changes:\n * - Modern function component (React 19 forwards refs natively — `forwardRef` removed).\n * - Multi-axis hover signal via `.tac-hover-lift` (background shift + border activate +\n * sub-pixel translate) for `default` / `destructive` / `outline` / `secondary`.\n * - Premium focus via `.tac-focus-premium` (1px outline + 8px primary bloom)\n * replaces the generic Tailwind `ring-[3px] ring-ring/50`.\n * - Preserves the brutalist offset: still uses `shadow-sm` (= 3px brutalist offset),\n * no soft drop shadows.\n * - Active state returns from the lift via `translate3d(0,0,0)` (managed by tac-hover-lift).\n *\n * See `docs/VIOLET-GRID-V6-EVOLUTION.md` § 2.1.\n */\nconst buttonVariants = cva(\n [\n \"group/button inline-flex shrink-0 items-center justify-center\",\n \"border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap\",\n \"rounded-[var(--radius-md)] outline-none select-none\",\n // v6: multi-axis transition handles bg + border + transform\n \"transition-[background-color,border-color,transform,box-shadow] duration-fast ease-linear will-change-transform\",\n // v6: premium focus signal\n \"focus-visible:outline-1 focus-visible:outline-primary focus-visible:[outline-offset:1px] focus-visible:[box-shadow:0_0_8px_color-mix(in_oklch,var(--primary)_40%,transparent)]\",\n // Disabled / aria-invalid carry over from v5 with the new bloom\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"aria-invalid:border-destructive aria-invalid:[box-shadow:0_0_8px_color-mix(in_oklch,var(--destructive)_40%,transparent)]\",\n // Icon sizing rules\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n ],\n {\n variants: {\n variant: {\n // Premium hover: bg shifts to primary/85 + sub-pixel lift on the offset shadow\n default: [\n \"bg-primary text-primary-foreground shadow-sm\",\n // Single hover bg — `hover:bg-[color:var(--overlay-primary-strong)]`\n // and `hover:bg-primary/90` both targeted the same CSS property\n // with different values, so Tailwind's class-ordering picked one\n // non-deterministically. Kept the primary/90 variant — closes\n // Macroscope finding on button.json.\n \"hover:bg-primary/90\",\n \"hover:-translate-x-px hover:-translate-y-px hover:shadow-[5px_5px_0_0_var(--border)]\",\n \"active:translate-x-0 active:translate-y-0 active:shadow-sm\",\n ],\n destructive: [\n \"bg-destructive text-destructive-foreground shadow-sm\",\n \"hover:bg-destructive/90\",\n \"hover:-translate-x-px hover:-translate-y-px hover:shadow-[5px_5px_0_0_var(--border)]\",\n \"active:translate-x-0 active:translate-y-0 active:shadow-sm\",\n ],\n // Outline button — surface lifts on hover, border activates to primary\n outline: [\n \"border-input bg-background text-foreground shadow-sm\",\n \"hover:bg-surface-hover hover:border-primary\",\n \"hover:-translate-x-px hover:-translate-y-px hover:shadow-[5px_5px_0_0_var(--border)]\",\n \"active:translate-x-0 active:translate-y-0 active:shadow-sm\",\n ],\n secondary: [\n \"bg-secondary text-secondary-foreground shadow-sm\",\n \"hover:bg-secondary/80\",\n \"hover:-translate-x-px hover:-translate-y-px hover:shadow-[5px_5px_0_0_var(--border)]\",\n \"active:translate-x-0 active:translate-y-0 active:shadow-sm\",\n ],\n // Ghost — no shadow, just hover-bg shift (lift would feel wrong without offset)\n ghost: \"text-foreground hover:bg-primary-subtle hover:text-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n // Glow — primary CTA used in hero contexts\n glow: [\n \"bg-primary text-primary-foreground shadow-sm\",\n \"hover:bg-primary/90 hover:shadow-[0_0_24px_color-mix(in_oklch,var(--primary)_45%,transparent)]\",\n \"hover:-translate-x-px hover:-translate-y-px\",\n \"active:translate-x-0 active:translate-y-0 active:shadow-sm\",\n ],\n },\n size: {\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-[var(--radius-sm)] px-3 text-xs\",\n lg: \"h-12 rounded-[var(--radius-lg)] px-8 text-base\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes,\n VariantProps {\n asChild?: boolean\n /** React 19 supports `ref` as a normal prop on function components. */\n ref?: React.Ref\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n ...props\n}: ButtonProps) {\n const Comp = asChild ? Slot : \"button\"\n return (\n \n )\n}\n\nexport { Button, buttonVariants }\n" } ] }