{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tabs", "title": "Tabs", "description": "Hand-drawn tabs with rough active-tab underline indicator.", "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/tabs.tsx", "content": "\"use client\";\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n type HTMLAttributes,\n} from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CrumbleContext,\n randomSeed,\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\n\ninterface TabsContextValue {\n activeTab: string;\n animateOnHover: boolean;\n setActiveTab: (value: string) => void;\n theme: CrumbleTheme;\n}\n\nconst TabsContext = createContext({\n activeTab: \"\",\n animateOnHover: true,\n setActiveTab: () => {},\n theme: \"pencil\",\n});\n\nexport interface TabsProps\n extends HTMLAttributes,\n CrumbleColorProps {\n animateOnHover?: boolean;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n theme?: CrumbleTheme;\n value?: string;\n}\n\nexport function Tabs({\n animateOnHover = true,\n children,\n className,\n defaultValue = \"\",\n fill,\n onValueChange,\n stroke,\n strokeMuted,\n theme: themeProp,\n value: controlledValue,\n ...props\n}: TabsProps) {\n const { theme: contextTheme } = useContext(CrumbleContext);\n const theme = themeProp ?? contextTheme;\n const [internalValue, setInternalValue] = useState(defaultValue);\n const activeTab = controlledValue ?? internalValue;\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n\n const setActiveTab = (next: string) => {\n setInternalValue(next);\n onValueChange?.(next);\n };\n\n return (\n \n \n {children}\n \n \n );\n}\n\nexport interface TabsListProps extends HTMLAttributes {}\n\nexport function TabsList({ children, className, ...props }: TabsListProps) {\n return (\n \n {children}\n \n );\n}\n\nexport interface TabsTriggerProps extends HTMLAttributes {\n disabled?: boolean;\n value: string;\n}\n\nexport function TabsTrigger({\n children,\n className,\n disabled,\n value,\n ...props\n}: TabsTriggerProps) {\n const { activeTab, animateOnHover, setActiveTab, theme } =\n useContext(TabsContext);\n const active = activeTab === value;\n const btnRef = useRef(null);\n const externalSvgRef = useRef(null);\n const { drawLine, svgRef } = useRough({\n stableId: `tab-trigger-${value}`,\n svgRef: externalSvgRef,\n theme,\n variant: \"border\",\n });\n\n const draw = useCallback(\n (reseed = false) => {\n const btn = btnRef.current;\n const svg = svgRef.current;\n if (!btn || !svg) return;\n\n svg.replaceChildren();\n\n const w = btn.offsetWidth;\n const h = btn.offsetHeight;\n svg.setAttribute(\"width\", String(w));\n svg.setAttribute(\"height\", String(h));\n svg.setAttribute(\"viewBox\", `0 0 ${w} ${h}`);\n\n if (active) {\n const line = drawLine(2, h - 1, w - 2, h - 1, {\n seed: reseed ? randomSeed() : undefined,\n stroke: \"currentColor\",\n strokeWidth: theme === \"crayon\" ? 3 : theme === \"ink\" ? 2 : 1.5,\n });\n if (line) svg.appendChild(line);\n }\n },\n [active, drawLine, svgRef, theme],\n );\n\n useEffect(() => {\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw]);\n\n useEffect(() => {\n const btn = btnRef.current;\n if (!btn) return;\n const ro = new ResizeObserver(() => draw());\n ro.observe(btn);\n return () => ro.disconnect();\n }, [draw]);\n\n return (\n {\n if (!disabled) setActiveTab(value);\n }}\n onMouseEnter={() => {\n if (!disabled && animateOnHover && active) draw(true);\n }}\n onMouseLeave={() => {\n if (!disabled && animateOnHover && active) draw(false);\n }}\n {...(props as HTMLAttributes)}\n >\n \n {children}\n \n );\n}\n\nexport interface TabsContentProps extends HTMLAttributes {\n value: string;\n}\n\nexport function TabsContent({\n children,\n className,\n value,\n ...props\n}: TabsContentProps) {\n const { activeTab } = useContext(TabsContext);\n if (activeTab !== value) return null;\n\n return (\n
\n {children}\n
\n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/tabs.tsx" } ], "type": "registry:ui" }