{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "button", "title": "Button", "description": "Hand-drawn button with hover redraw animation. Supports default, ghost, and destructive variants.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json", "utils" ], "files": [ { "path": "registry/new-york/ui/button.tsx", "content": "\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n type ButtonHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n randomSeed,\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\nimport { useRough } from \"@/hooks/use-rough\";\n\nexport type ButtonVariant = \"default\" | \"ghost\" | \"destructive\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes,\n CrumbleColorProps {\n children: ReactNode;\n size?: ButtonSize;\n theme?: CrumbleTheme;\n variant?: ButtonVariant;\n}\n\nconst sizeClasses: Record = {\n lg: \"h-12 px-5 text-base\",\n md: \"h-10 px-4 text-sm\",\n sm: \"h-8 px-3 text-[13px]\",\n};\n\nconst sizeHeights: Record = {\n lg: 48,\n md: 40,\n sm: 32,\n};\n\nconst variantStroke: Record = {\n default: \"currentColor\",\n destructive: \"#dc2626\",\n ghost: \"currentColor\",\n};\n\nexport function Button({\n children,\n className,\n disabled,\n fill,\n onClick,\n size = \"md\",\n stroke,\n strokeMuted,\n theme: themeProp,\n variant = \"default\",\n ...props\n}: ButtonProps) {\n const btnRef = useRef(null);\n const svgRef = useRef(null);\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const { animateOnHover, drawRect, theme } = useRough({\n stableId: `btn-${typeof children === \"string\" ? children : \"content\"}`,\n svgRef,\n theme: themeProp,\n variant: \"interactive\",\n });\n const h = sizeHeights[size];\n\n const draw = useCallback(\n (reseed = false) => {\n const btn = btnRef.current;\n const svg = svgRef.current;\n\n if (!btn || !svg) return;\n\n svg.replaceChildren();\n\n const width = btn.offsetWidth;\n svg.setAttribute(\"width\", String(width));\n svg.setAttribute(\"height\", String(h));\n svg.setAttribute(\"viewBox\", `0 0 ${width} ${h}`);\n\n const node = drawRect(1, 1, width - 2, h - 2, {\n fill: \"none\",\n seed: reseed ? randomSeed() : undefined,\n stroke: disabled ? \"#9ca3af\" : variantStroke[variant],\n });\n\n if (node) {\n svg.appendChild(node);\n }\n },\n [disabled, drawRect, h, variant],\n );\n\n useEffect(() => {\n draw();\n }, [draw, theme]);\n\n useEffect(() => {\n const button = btnRef.current;\n if (!button) return;\n\n const observer = new ResizeObserver(() => draw());\n observer.observe(button);\n\n return () => observer.disconnect();\n }, [draw]);\n\n return (\n {\n if (!disabled) draw(true);\n }}\n onMouseEnter={() => {\n if (!disabled && animateOnHover) draw(true);\n }}\n onMouseLeave={() => {\n if (!disabled && animateOnHover) draw(false);\n }}\n {...props}\n >\n \n {children}\n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/button.tsx" } ], "type": "registry:ui" }