{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "rough-highlight", "title": "RoughHighlight", "description": "Hand-drawn text annotation — underline, box, circle, highlight, strike-through, bracket.", "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/primitives/rough-highlight.tsx", "content": "\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { type CrumbleTheme } from \"@/lib/rough\";\nimport { useRough } from \"@/hooks/use-rough\";\n\nexport type HighlightType =\n | \"underline\"\n | \"box\"\n | \"circle\"\n | \"highlight\"\n | \"strike-through\"\n | \"bracket\";\n\nexport interface RoughHighlightProps {\n animate?: boolean;\n animationDuration?: number;\n children: ReactNode;\n className?: string;\n color?: string;\n id?: string;\n opacity?: number;\n style?: CSSProperties;\n theme?: CrumbleTheme;\n type?: HighlightType;\n}\n\nfunction animateGroup(node: SVGGElement, duration: number) {\n const paths = node.querySelectorAll(\"path\");\n\n paths.forEach((path) => {\n const length = path.getTotalLength();\n path.style.strokeDasharray = String(length);\n path.style.strokeDashoffset = String(length);\n path.style.transition = `stroke-dashoffset ${duration}ms ease-out`;\n\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n path.style.strokeDashoffset = \"0\";\n });\n });\n });\n}\n\nexport function RoughHighlight({\n animate = true,\n animationDuration,\n children,\n className,\n color = \"currentColor\",\n id,\n opacity = 0.3,\n style,\n theme: themeProp,\n type = \"underline\",\n}: RoughHighlightProps) {\n const containerRef = useRef(null);\n const externalSvgRef = useRef(null);\n const { animateOnMount, drawPath, getOptions, rc, svgRef, theme } = useRough({\n stableId: id,\n svgRef: externalSvgRef,\n theme: themeProp,\n variant: \"border\",\n });\n const shouldAnimate = animate && animateOnMount;\n const duration =\n animationDuration ??\n (theme === \"ink\" ? 400 : theme === \"crayon\" ? 600 : 800);\n\n const draw = useCallback(() => {\n const container = containerRef.current;\n const svg = svgRef.current;\n\n if (!container || !svg) return;\n\n svg.replaceChildren();\n\n const width = container.offsetWidth;\n const height = container.offsetHeight;\n const pad = 4;\n const borderOptions = {\n stroke: color,\n strokeWidth: theme === \"crayon\" ? 3 : theme === \"ink\" ? 2 : 1,\n };\n\n svg.setAttribute(\"width\", String(width));\n svg.setAttribute(\"height\", String(height + 8));\n svg.setAttribute(\"viewBox\", `0 0 ${width} ${height + 8}`);\n\n let node: SVGGElement | null = null;\n\n switch (type) {\n case \"underline\":\n node = drawPath(\n `M ${-pad} ${height + 2} L ${width + pad} ${height + 2}`,\n borderOptions,\n ) as SVGGElement;\n break;\n case \"box\":\n node = drawPath(\n `M ${-pad} ${-pad} L ${width + pad} ${-pad} L ${width + pad} ${height + pad} L ${-pad} ${height + pad} Z`,\n { ...borderOptions, fill: \"none\" },\n ) as SVGGElement;\n break;\n case \"circle\":\n node = rc.current?.ellipse(\n width / 2,\n height / 2,\n width + pad * 3,\n height + pad * 3,\n getOptions({ ...borderOptions, fill: \"none\" }),\n ) as SVGGElement | null;\n break;\n case \"highlight\":\n node = drawPath(\n `M ${-pad} 0 L ${width + pad} 0 L ${width + pad} ${height} L ${-pad} ${height} Z`,\n {\n ...borderOptions,\n fill: color,\n fillStyle: \"solid\",\n fillWeight: 1,\n roughness: 1.5,\n stroke: \"none\",\n },\n ) as SVGGElement;\n if (node) node.style.opacity = String(opacity);\n break;\n case \"strike-through\":\n node = drawPath(\n `M ${-pad} ${height / 2} L ${width + pad} ${height / 2}`,\n borderOptions,\n ) as SVGGElement;\n break;\n case \"bracket\": {\n const left = drawPath(\n `M ${-pad} 0 L ${-pad} ${height}`,\n borderOptions,\n ) as SVGGElement;\n const right = drawPath(\n `M ${width + pad} 0 L ${width + pad} ${height}`,\n borderOptions,\n ) as SVGGElement;\n\n if (!left || !right) return;\n svg.append(left, right);\n\n if (shouldAnimate) {\n animateGroup(left, duration);\n animateGroup(right, duration);\n }\n return;\n }\n default:\n return;\n }\n\n if (!node) return;\n svg.appendChild(node);\n\n if (shouldAnimate) {\n animateGroup(node, duration);\n }\n }, [\n color,\n drawPath,\n duration,\n getOptions,\n opacity,\n rc,\n shouldAnimate,\n svgRef,\n theme,\n type,\n ]);\n\n useEffect(() => {\n draw();\n }, [draw]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const observer = new ResizeObserver(() => draw());\n observer.observe(container);\n\n return () => observer.disconnect();\n }, [draw]);\n\n return (\n \n {children}\n \n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/primitives/rough-highlight.tsx" } ], "type": "registry:ui" }