{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "toggle", "title": "Toggle", "description": "A hand-drawn toggle switch with a sliding circle thumb.", "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/toggle.tsx", "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport {\n resolveRoughVars,\n stableSeed,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToggleVariant = \"filled\" | \"hybrid\" | \"ball\";\n\nexport interface ToggleProps extends CrumbleColorProps {\n checked?: boolean;\n className?: string;\n defaultChecked?: boolean;\n disabled?: boolean;\n id?: string;\n label?: string;\n onChange?: (checked: boolean) => void;\n theme?: CrumbleTheme;\n variant?: ToggleVariant;\n}\n\nconst HEIGHT = 24;\nconst WIDTH = 44;\nconst THUMB_R = 8;\nconst THUMB_OFF_X = 2 + THUMB_R;\nconst THUMB_ON_X = WIDTH - 2 - THUMB_R;\n\nexport function Toggle({\n checked,\n className,\n defaultChecked = false,\n disabled = false,\n fill,\n id,\n label,\n onChange,\n stroke,\n strokeMuted,\n theme: themeProp,\n variant = \"ball\",\n}: ToggleProps) {\n const isControlled = checked !== undefined;\n const [internalValue, setInternalValue] = useState(defaultChecked);\n const svgRef = useRef(null);\n const toggleId =\n id ?? `toggle-${label?.toLowerCase().replace(/\\s+/g, \"-\") ?? \"field\"}`;\n const currentValue = isControlled ? checked : internalValue;\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const { drawCircle, drawRect, theme } = useRough({\n stableId: toggleId,\n svgRef,\n theme: themeProp,\n variant: \"interactive\",\n });\n\n const draw = useCallback(\n (isOn: boolean) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n svg.replaceChildren();\n svg.setAttribute(\"width\", String(WIDTH));\n svg.setAttribute(\"height\", String(HEIGHT));\n svg.setAttribute(\"viewBox\", `0 0 ${WIDTH} ${HEIGHT}`);\n\n const strokeColor = disabled ? \"var(--cr-stroke-muted)\" : \"var(--cr-stroke)\";\n let trackFill = \"none\";\n let trackFillStyle: \"solid\" | \"hachure\" = \"hachure\";\n\n if (variant === \"filled\" && isOn) {\n trackFill = strokeColor;\n trackFillStyle = \"solid\";\n } else if (variant === \"hybrid\" && isOn) {\n trackFill = strokeColor;\n trackFillStyle = \"hachure\";\n }\n\n const track = drawRect(1, 1, WIDTH - 2, HEIGHT - 2, {\n seed: stableSeed(`${toggleId}-track`),\n fill: trackFill,\n fillStyle: trackFillStyle,\n fillWeight: theme === \"crayon\" ? 2.2 : theme === \"ink\" ? 1.8 : 1.6,\n hachureGap: theme === \"crayon\" ? 3 : theme === \"ink\" ? 2 : 2.5,\n hachureAngle: -41,\n stroke: strokeColor,\n roughness: theme === \"crayon\" ? 2.0 : theme === \"ink\" ? 0.5 : 0.9,\n bowing: theme === \"crayon\" ? 1.5 : theme === \"ink\" ? 0.3 : 0.8,\n strokeWidth: theme === \"crayon\" ? 2.0 : theme === \"ink\" ? 1.5 : 1.2,\n });\n if (track) svg.appendChild(track);\n\n const thumbX = isOn ? THUMB_ON_X : THUMB_OFF_X;\n const thumbY = HEIGHT / 2;\n const bgDisc = document.createElementNS(\"http://www.w3.org/2000/svg\", \"circle\");\n bgDisc.setAttribute(\"cx\", String(thumbX));\n bgDisc.setAttribute(\"cy\", String(thumbY));\n bgDisc.setAttribute(\"r\", String(THUMB_R));\n bgDisc.setAttribute(\"fill\", \"var(--background, white)\");\n bgDisc.setAttribute(\"stroke\", \"none\");\n svg.appendChild(bgDisc);\n\n const thumbStroke = variant === \"hybrid\" && isOn ? \"none\" : strokeColor;\n const useThumbFill = variant === \"hybrid\" || variant === \"ball\";\n const thumb = drawCircle(thumbX, thumbY, THUMB_R * 2, {\n seed: stableSeed(`${toggleId}-thumb`),\n stroke: thumbStroke,\n fill: useThumbFill ? strokeColor : \"none\",\n fillStyle: \"hachure\",\n fillWeight: theme === \"crayon\" ? 2.0 : theme === \"ink\" ? 1.6 : 1.4,\n hachureGap: theme === \"crayon\" ? 3 : theme === \"ink\" ? 2.5 : 3,\n hachureAngle: 41,\n roughness: theme === \"crayon\" ? 2.2 : theme === \"ink\" ? 0.4 : 1.0,\n bowing: theme === \"crayon\" ? 1.8 : theme === \"ink\" ? 0.3 : 1.0,\n strokeWidth: theme === \"crayon\" ? 1.8 : theme === \"ink\" ? 1.2 : 1.0,\n });\n if (thumb) svg.appendChild(thumb);\n },\n [disabled, drawCircle, drawRect, theme, toggleId, variant],\n );\n\n useEffect(() => {\n draw(currentValue);\n }, [currentValue, draw]);\n\n const handleChange = () => {\n if (disabled) return;\n const next = !currentValue;\n if (!isControlled) setInternalValue(next);\n onChange?.(next);\n };\n\n return (\n \n
\n \n \n
\n {label ? {label} : null}\n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/toggle.tsx" } ], "type": "registry:ui" }