{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "text-scribble", "type": "registry:block", "title": "Text Scribble", "description": "Annotate React text with a hand-drawn scribble, a squiggly underline, a plain underline, a strike-through, or a circle or box that draws itself on. Lines follow each wrapped line of text; the circle and box loop the whole phrase in one stroke that runs past its start, the way a pen closes a shape.", "author": "Wensity ", "dependencies": [ "clsx", "tailwind-merge" ], "registryDependencies": [], "files": [ { "path": "registry/wensity/lib/utils.ts", "type": "registry:lib", "target": "@lib/utils.ts", "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n" }, { "path": "registry/wensity/text-scribble.tsx", "type": "registry:component", "target": "@components/wensity/text-scribble.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TextScribbleType = \"scribble\" | \"underline\" | \"strike-through\" | \"circle\" | \"box\";\nexport type TextScribbleTrigger = \"view\" | \"hover\" | \"mount\";\n\nexport interface TextScribbleProps {\n /** The inline content to annotate. */\n children: React.ReactNode;\n /** Annotation style. Default: `\"scribble\"`. */\n type?: TextScribbleType;\n /** Stroke color. Defaults to the Wensity brand (chili). */\n color?: string;\n /** Stroke width in CSS pixels. Default: 2. */\n strokeWidth?: number;\n /** Seconds for the draw-on. Default: 0.9. */\n duration?: number;\n /** Seconds to wait before drawing. Default: 0. */\n delay?: number;\n /** When to draw. Default: `\"view\"`. */\n trigger?: TextScribbleTrigger;\n /**\n * Sketch passes for the hand-drawn look. More = scratchier. Clamped 1–4.\n * Default: 2 for `scribble`, 1 otherwise.\n */\n iterations?: number;\n /** Extra px around the text before the mark is drawn. Default: 2. */\n padding?: number;\n /**\n * Force the draw state. `true` keeps it drawn, `false` retracted; when\n * omitted the `trigger` drives it. Reduced-motion always draws instantly.\n */\n drawn?: boolean;\n /** Outer element tag. Default: `\"span\"`. */\n as?: keyof React.JSX.IntrinsicElements;\n /** Tailwind class for the host (size / weight / color of the text). */\n className?: string;\n /** Optional accessible name override for the host. */\n \"aria-label\"?: string;\n}\n\ntype Mark = { d: string; delay: number };\n\n/** Wensity brand (chili). Falls back to the literal for copied-out source. */\nconst DEFAULT_COLOR = \"var(--color-chili-500, #cd1c18)\";\n\n/** Tiny deterministic PRNG so passes look sketchy but stay stable per size. */\nfunction mulberry32(seed: number) {\n return () => {\n seed |= 0;\n seed = (seed + 0x6d2b79f5) | 0;\n let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\n/** A near-horizontal line with light vertical jitter — a felt-tip stroke. */\nfunction jitterLine(x1: number, x2: number, y: number, amp: number, rng: () => number) {\n const span = x2 - x1;\n const steps = Math.max(2, Math.round(span / 34));\n let d = `M ${x1.toFixed(1)} ${(y + (rng() - 0.5) * amp).toFixed(1)}`;\n for (let i = 1; i <= steps; i++) {\n const x = x1 + (span * i) / steps;\n const yy = y + (rng() - 0.5) * amp;\n d += ` L ${x.toFixed(1)} ${yy.toFixed(1)}`;\n }\n return d;\n}\n\n/** A denser back-and-forth squiggle for the scribble underline. */\nfunction scribblePath(x1: number, x2: number, y: number, amp: number, rng: () => number) {\n const span = x2 - x1;\n const steps = Math.max(6, Math.round(span / 12));\n let d = `M ${x1.toFixed(1)} ${y.toFixed(1)}`;\n for (let i = 1; i <= steps; i++) {\n const x = x1 + (span * i) / steps;\n const dir = i % 2 === 0 ? -1 : 1;\n const yy = y + dir * amp * (0.6 + rng() * 0.4);\n const cx = x - span / steps / 2 + (rng() - 0.5) * 4;\n const cy = y + dir * -amp * (0.4 + rng() * 0.5);\n d += ` Q ${cx.toFixed(1)} ${cy.toFixed(1)} ${x.toFixed(1)} ${yy.toFixed(1)}`;\n }\n return d;\n}\n\n/**\n * A hand-drawn loop around the text. A pen does not close a circle exactly:\n * the stroke starts at the upper left, runs a little past a full turn, and\n * drifts outward as it goes, so the end overlaps the start just off-line.\n * Points are joined with a Catmull-Rom spline, so the loop stays smooth.\n */\nfunction circlePath(cx: number, cy: number, rx: number, ry: number, rng: () => number) {\n const turns = 1.06 + rng() * 0.06;\n const start = -Math.PI * 0.62 + (rng() - 0.5) * 0.4;\n const phase = rng() * Math.PI * 2;\n const count = 28;\n const points: Array<[number, number]> = [];\n for (let i = 0; i <= count; i++) {\n const t = i / count;\n const a = start + t * turns * Math.PI * 2;\n const drift = 1 + (t - 0.5) * 0.08;\n const wobble = 1 + Math.sin(a * 2 + phase) * 0.025 + (rng() - 0.5) * 0.015;\n points.push([cx + Math.cos(a) * rx * drift * wobble, cy + Math.sin(a) * ry * drift * wobble]);\n }\n let d = `M ${points[0][0].toFixed(1)} ${points[0][1].toFixed(1)}`;\n for (let i = 0; i < points.length - 1; i++) {\n const p0 = points[i - 1] ?? points[i];\n const p1 = points[i];\n const p2 = points[i + 1];\n const p3 = points[i + 2] ?? p2;\n const c1x = p1[0] + (p2[0] - p0[0]) / 6;\n const c1y = p1[1] + (p2[1] - p0[1]) / 6;\n const c2x = p2[0] - (p3[0] - p1[0]) / 6;\n const c2y = p2[1] - (p3[1] - p1[1]) / 6;\n d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)} ${c2x.toFixed(1)} ${c2y.toFixed(1)} ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;\n }\n return d;\n}\n\n/** The most a circle pass can pull inward: outward drift and wobble at their lowest. */\nconst CIRCLE_MIN_REACH = 0.92;\n\n/**\n * An ellipse that encloses every line of the text, not just its bounding box.\n * An ellipse narrows toward its ends, so half-extents plus padding leave the\n * corners of the outer lines outside it and the stroke cuts through words.\n *\n * Each line box is trimmed to roughly where the glyphs sit (the content box\n * includes empty space above caps and below descenders). The shape follows\n * the block: a single wide line gets a flat ellipse that hugs it horizontally,\n * a tall block of wrapped lines a rounder one. It is then scaled so the\n * farthest line corner sits on it, and widened by the wobble margin so no\n * hand-drawn pass can dip into the text.\n */\nfunction fitEllipse(lineRects: DOMRect[], hostBox: DOMRect, padding: number) {\n const boxes = lineRects.map((r) => {\n const trim = r.height * 0.12;\n return {\n left: r.left - hostBox.left,\n right: r.right - hostBox.left,\n top: r.top - hostBox.top + trim,\n bottom: r.bottom - hostBox.top - trim,\n };\n });\n const left = Math.min(...boxes.map((b) => b.left));\n const right = Math.max(...boxes.map((b) => b.right));\n const top = Math.min(...boxes.map((b) => b.top));\n const bottom = Math.max(...boxes.map((b) => b.bottom));\n const cx = (left + right) / 2;\n const cy = (top + bottom) / 2;\n const halfW = Math.max(1, (right - left) / 2);\n const halfH = Math.max(1, (bottom - top) / 2);\n\n // Stretch factors with 1/fx^2 + 1/fy^2 = 1, so the bounding box corners land\n // exactly on the ellipse. Equal factors (sqrt 2) are the roundest fit; wide\n // lines lean on fy instead, so the loop does not reach far into the words on\n // either side.\n const wide = Math.min(1, Math.max(0, (halfW / halfH - 1.5) / 4));\n const fx = Math.SQRT2 - wide * (Math.SQRT2 - 1.15);\n const fy = 1 / Math.sqrt(1 - 1 / (fx * fx));\n let rx = halfW * fx;\n let ry = halfH * fy;\n\n // Wrapped lines rarely fill the bounding box corners (a short last line),\n // so shrink to the farthest real corner when that is tighter.\n let reach = 0;\n for (const b of boxes) {\n for (const x of [b.left, b.right]) {\n for (const y of [b.top, b.bottom]) {\n reach = Math.max(reach, Math.hypot((x - cx) / rx, (y - cy) / ry));\n }\n }\n }\n rx = (rx * reach) / CIRCLE_MIN_REACH + padding;\n ry = (ry * reach) / CIRCLE_MIN_REACH + padding;\n return { cx, cy, rx, ry };\n}\n\n/**\n * A hand-drawn box: four slightly bowed sides with loose corners, drawn in\n * one stroke that runs past the first corner when it comes back round.\n */\nfunction boxPath(x1: number, y1: number, x2: number, y2: number, rng: () => number) {\n const j = () => (rng() - 0.5) * 3;\n const tl: [number, number] = [x1 + j(), y1 + j()];\n const tr: [number, number] = [x2 + j(), y1 + j()];\n const br: [number, number] = [x2 + j(), y2 + j()];\n const bl: [number, number] = [x1 + j(), y2 + j()];\n const end: [number, number] = [x1 + Math.min(14, (x2 - x1) * 0.12), y1 + j()];\n const side = (a: [number, number], b: [number, number]) => {\n const bow = (rng() - 0.5) * 3;\n const mx = (a[0] + b[0]) / 2 + (b[1] - a[1] === 0 ? 0 : bow);\n const my = (a[1] + b[1]) / 2 + (b[0] - a[0] === 0 ? 0 : bow);\n return ` Q ${mx.toFixed(1)} ${my.toFixed(1)} ${b[0].toFixed(1)} ${b[1].toFixed(1)}`;\n };\n const start: [number, number] = [x1 - 3 + j(), tl[1]];\n return (\n `M ${start[0].toFixed(1)} ${start[1].toFixed(1)}` +\n side(start, tr) +\n side(tr, br) +\n side(br, bl) +\n side(bl, tl) +\n side(tl, end)\n );\n}\n\nfunction marksEqual(a: Mark[], b: Mark[]) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].d !== b[i].d || a[i].delay !== b[i].delay) return false;\n }\n return true;\n}\n\n/**\n * TextScribble\n *\n * Hand-drawn annotations for React text that draw themselves on: scribble,\n * underline, strike-through, or a circle or box around the words. Lines track\n * wrapping per line; circle and box wrap the whole phrase. Re-fits on resize\n * and font load.\n *\n * Motion contract:\n * - Measures the live text via Range client rects (ResizeObserver, no polling).\n * - Marks paint client-side only (no SSR path → no hydration diff).\n * - Each stroke reveals via a single `stroke-dashoffset` transition\n * (pathLength normalised to 100).\n * - `trigger=\"view\"` draws once on-screen; `\"hover\"` draws and retracts.\n * - Honours `prefers-reduced-motion` by drawing instantly, no transition.\n */\nexport function TextScribble({\n children,\n type = \"scribble\",\n color,\n strokeWidth = 2,\n duration = 0.9,\n delay = 0,\n trigger = \"view\",\n iterations,\n padding = 2,\n drawn,\n as = \"span\",\n className,\n \"aria-label\": ariaLabel,\n}: TextScribbleProps) {\n const hostRef = React.useRef(null);\n const textRef = React.useRef(null);\n\n const [size, setSize] = React.useState({ w: 0, h: 0 });\n const [marks, setMarks] = React.useState([]);\n const [active, setActive] = React.useState(trigger === \"mount\");\n const [reduceMotion, setReduceMotion] = React.useState(false);\n\n const stroke = color ?? DEFAULT_COLOR;\n const passes = Math.max(1, Math.min(4, iterations ?? (type === \"scribble\" ? 2 : 1)));\n\n // --- Measure text geometry into annotation paths -------------------------\n const measure = React.useCallback(() => {\n const host = hostRef.current;\n const text = textRef.current;\n if (!host || !text || !text.firstChild) return;\n\n const hostBox = host.getBoundingClientRect();\n if (hostBox.width < 1) return;\n\n const range = document.createRange();\n range.selectNodeContents(text);\n const lineRects = Array.from(range.getClientRects()).filter((r) => r.width > 1);\n if (lineRects.length === 0) return;\n\n const next: Mark[] = [];\n const stagger = lineRects.length > 1 ? duration * 0.3 : duration * 0.12;\n\n // Circle and box wrap the whole phrase, across every wrapped line.\n if (type === \"circle\" || type === \"box\") {\n const left = Math.min(...lineRects.map((r) => r.left)) - hostBox.left;\n const right = Math.max(...lineRects.map((r) => r.right)) - hostBox.left;\n const top = Math.min(...lineRects.map((r) => r.top)) - hostBox.top;\n const bottom = Math.max(...lineRects.map((r) => r.bottom)) - hostBox.top;\n const lineHeight = Math.min(...lineRects.map((r) => r.height));\n const circle = type === \"circle\" ? fitEllipse(lineRects, hostBox, padding) : null;\n for (let p = 0; p < passes; p++) {\n const rng = mulberry32(4243 + p * 97 + Math.round(right - left));\n const d = circle\n ? circlePath(circle.cx, circle.cy, circle.rx, circle.ry, rng)\n : boxPath(\n left - padding - lineHeight * 0.18,\n top - padding * 0.5,\n right + padding + lineHeight * 0.18,\n bottom + padding * 0.5,\n rng,\n );\n next.push({ d, delay: delay + p * duration * 0.35 });\n }\n }\n\n if (type !== \"circle\" && type !== \"box\") lineRects.forEach((r, lineIdx) => {\n const x = r.left - hostBox.left;\n const y = r.top - hostBox.top;\n const baseY = type === \"strike-through\" ? y + r.height * 0.55 : y + r.height - strokeWidth;\n\n for (let p = 0; p < passes; p++) {\n const rng = mulberry32(7919 + lineIdx * 131 + p * 53 + Math.round(r.width));\n const d =\n type === \"scribble\"\n ? scribblePath(x - padding, x + r.width + padding, baseY, Math.min(r.height * 0.16, 5) + strokeWidth, rng)\n : jitterLine(x - padding, x + r.width + padding, baseY, 1.6, rng);\n next.push({ d, delay: delay + (lineIdx * passes + p) * (stagger / passes) });\n }\n });\n\n setSize((prev) =>\n prev.w === hostBox.width && prev.h === hostBox.height\n ? prev\n : { w: hostBox.width, h: hostBox.height },\n );\n setMarks((prev) => (marksEqual(prev, next) ? prev : next));\n }, [type, strokeWidth, passes, padding, delay, duration]);\n\n React.useLayoutEffect(() => {\n let cancelled = false;\n const run = () => {\n if (!cancelled) measure();\n };\n\n run();\n const host = hostRef.current;\n if (!host) return;\n\n const ro = new ResizeObserver(run);\n ro.observe(host);\n\n const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;\n fonts?.ready?.then(run).catch(() => {});\n\n return () => {\n cancelled = true;\n ro.disconnect();\n };\n }, [measure, children]);\n\n // --- Reduced motion ------------------------------------------------------\n React.useEffect(() => {\n if (typeof window.matchMedia !== \"function\") return;\n const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const sync = () => setReduceMotion(mq.matches);\n sync();\n mq.addEventListener(\"change\", sync);\n return () => mq.removeEventListener(\"change\", sync);\n }, []);\n\n // --- Trigger -------------------------------------------------------------\n React.useEffect(() => {\n if (drawn !== undefined) {\n setActive(drawn);\n return;\n }\n if (trigger === \"mount\") {\n setActive(true);\n return;\n }\n if (trigger === \"view\") {\n const host = hostRef.current;\n if (!host) return;\n const io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n setActive(true);\n io.disconnect();\n }\n },\n { threshold: 0.35 },\n );\n io.observe(host);\n return () => io.disconnect();\n }\n }, [trigger, drawn]);\n\n const isHover = trigger === \"hover\" && drawn === undefined;\n const hoverProps = isHover\n ? {\n tabIndex: 0 as const,\n onPointerEnter: () => setActive(true),\n onPointerLeave: () => setActive(false),\n onFocus: () => setActive(true),\n onBlur: () => setActive(false),\n }\n : {};\n\n const Tag = as as unknown as React.ComponentType<\n React.HTMLAttributes & { ref?: React.RefCallback }\n >;\n\n const show = active || reduceMotion;\n const ease = \"cubic-bezier(.65,0,.35,1)\";\n\n return (\n {\n hostRef.current = node;\n }}\n aria-label={ariaLabel}\n className={cn(\"relative inline-block\", className)}\n {...hoverProps}\n >\n \n {children}\n \n {size.w > 0 && marks.length > 0 ? (\n \n {marks.map((m, i) => (\n \n ))}\n \n ) : null}\n \n );\n}\n" } ], "cssVars": { "theme": { "font-sans": "var(--font-satoshi, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif)", "font-display": "var(--font-satoshi, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif)", "font-heading": "var(--font-cabinet, ui-sans-serif, system-ui, sans-serif)", "color-background": "var(--background)", "color-foreground": "var(--foreground)", "color-surface": "var(--surface)", "color-surface-muted": "var(--surface-muted)", "color-border": "var(--border)", "color-border-strong": "var(--border-strong)", "color-muted": "var(--muted)", "color-muted-foreground": "var(--muted-foreground)", "color-ring": "var(--ring)", "color-chili-50": "#fff1ee", "color-chili-100": "#ffe2db", "color-chili-200": "#ffa896", "color-chili-300": "#ff8a73", "color-chili-400": "#f15a45", "color-chili-500": "#cd1c18", "color-chili-600": "#b31614", "color-chili-700": "#9b1313", "color-chili-800": "#6a0d0e", "color-chili-900": "#38000a", "color-chili-950": "#1f0006", "color-primitive-surface-elevated": "var(--primitive-surface-elevated)", "color-primitive-surface-overlay": "var(--primitive-surface-overlay)", "color-primitive-surface-hover": "var(--primitive-surface-hover)", "color-primitive-surface-active": "var(--primitive-surface-active)", "color-primitive-surface-selected": "var(--primitive-surface-selected)", "color-primitive-border-subtle": "var(--primitive-border-subtle)", "color-primitive-ring": "var(--primitive-ring)", "color-primitive-text-secondary": "var(--primitive-text-secondary)", "color-primitive-text-placeholder": "var(--primitive-text-placeholder)", "color-primitive-text-inverse": "var(--primitive-text-inverse)", "color-primitive-destructive": "var(--primitive-destructive)", "color-primitive-destructive-hover": "var(--primitive-destructive-hover)", "color-primitive-destructive-active": "var(--primitive-destructive-active)", "color-primitive-destructive-foreground": "var(--primitive-destructive-foreground)", "color-primitive-destructive-surface": "var(--primitive-destructive-surface)", "color-primitive-destructive-border": "var(--primitive-destructive-border)", "color-primitive-success": "var(--primitive-success)", "color-primitive-warning": "var(--primitive-warning)", "color-primitive-info": "var(--primitive-info)", "color-primitive-control-solid": "var(--primitive-control-solid)", "color-primitive-control-solid-hover": "var(--primitive-control-solid-hover)", "color-primitive-control-solid-active": "var(--primitive-control-solid-active)", "color-primitive-control-solid-foreground": "var(--primitive-control-solid-foreground)", "color-primitive-chart-1": "var(--primitive-chart-1)", "color-primitive-chart-2": "var(--primitive-chart-2)", "color-primitive-chart-3": "var(--primitive-chart-3)", "color-primitive-chart-4": "var(--primitive-chart-4)", "color-primitive-chart-5": "var(--primitive-chart-5)", "color-primitive-chart-6": "var(--primitive-chart-6)", "radius-primitive": "var(--primitive-radius)", "radius-primitive-control": "var(--primitive-radius-control)", "radius-primitive-control-sm": "var(--primitive-radius-control-sm)", "radius-primitive-surface": "var(--primitive-radius-surface)", "radius-primitive-popup": "var(--primitive-radius-popup)", "radius-primitive-item": "var(--primitive-radius-item)", "font-primitive-sans": "var(--primitive-font-sans)", "font-primitive-display": "var(--primitive-font-display)", "font-primitive-mono": "var(--primitive-font-mono)", "spacing-primitive-control-height-sm": "var(--primitive-control-height-sm)", "spacing-primitive-control-height-md": "var(--primitive-control-height-md)", "spacing-primitive-control-height-lg": "var(--primitive-control-height-lg)" }, "light": { "background": "#fafafa", "foreground": "#0a0a0a", "surface": "#ffffff", "surface-muted": "#f4f4f5", "border": "rgba(10, 10, 10, 0.08)", "border-strong": "rgba(10, 10, 10, 0.16)", "muted": "#f4f4f5", "muted-foreground": "#52525b", "ring": "#cd1c18", "pattern-fg": "rgba(10, 10, 10, 0.07)", "llb-primary": "#18181b", "llb-primary-fg": "#ffffff", "llb-success": "#1f883d", "llb-error": "#cf222e", "primitive-surface-elevated": "#ffffff", "primitive-surface-overlay": "#ffffff", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 4%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 7%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 6%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.75rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#24292d", "primitive-control-solid-hover": "#2c3237", "primitive-control-solid-active": "#1e2326", "primitive-control-solid-foreground": "#ffffff", "primitive-chart-1": "#2a78d6", "primitive-chart-2": "#1baf7a", "primitive-chart-3": "#eda100", "primitive-chart-4": "#008300", "primitive-chart-5": "#4a3aa7", "primitive-chart-6": "#e34948", "primitive-text-inverse": "#ffffff", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#dc2626", "primitive-destructive-hover": "#b91c1c", "primitive-destructive-active": "#991b1b", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(220, 38, 38, 0.10)", "primitive-destructive-border": "rgba(220, 38, 38, 0.45)", "primitive-success": "#047857", "primitive-success-surface": "rgba(4, 120, 87, 0.10)", "primitive-success-border": "rgba(4, 120, 87, 0.45)", "primitive-warning": "#b45309", "primitive-warning-surface": "rgba(180, 83, 9, 0.10)", "primitive-warning-border": "rgba(180, 83, 9, 0.45)", "primitive-info": "#0369a1", "primitive-info-surface": "rgba(3, 105, 161, 0.10)", "primitive-info-border": "rgba(3, 105, 161, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "min(calc(var(--primitive-radius) * 0.8), 12px)", "primitive-radius-surface": "calc(var(--primitive-radius) * 1.4)", "primitive-radius-popup": "var(--primitive-radius)", "primitive-radius-item": "calc(var(--primitive-radius) * 0.8)", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "0 1px 1px -0.5px rgba(10, 10, 10, 0.08), 0 2px 6px -2px rgba(10, 10, 10, 0.08)", "primitive-shadow-overlay": "0 2px 4px -1px rgba(10, 10, 10, 0.06), 0 8px 16px -4px rgba(10, 10, 10, 0.08), 0 16px 32px -8px rgba(10, 10, 10, 0.14)", "primitive-shadow-modal": "0 4px 8px -2px rgba(10, 10, 10, 0.06), 0 16px 32px -8px rgba(10, 10, 10, 0.12), 0 32px 64px -16px rgba(10, 10, 10, 0.2)", "primitive-edge": "rgba(10, 10, 10, 0.1)", "primitive-catchlight": "rgba(10, 10, 10, 0.06)", "primitive-groove-shadow": "rgba(10, 10, 10, 0.08)", "primitive-groove-highlight": "rgba(255, 255, 255, 0.85)", "primitive-shadow-drop": "0 1px 1px -0.5px rgba(10, 10, 10, 0.1), 0 2px 4px -1px rgba(10, 10, 10, 0.06)", "primitive-shadow-recess": "inset 0 2px 3px -1px rgba(10, 10, 10, 0.1)", "primitive-control-face": "var(--primitive-surface-elevated)", "primitive-field-face": "var(--primitive-surface-elevated)", "primitive-well": "rgba(10, 10, 10, 0.045)", "primitive-shadow-well": "inset 0 1px 2px 0 rgba(10, 10, 10, 0.06)", "primitive-tint-hover": "rgba(10, 10, 10, 0.045)", "primitive-tint-press": "rgba(10, 10, 10, 0.06)", "primitive-footer": "color-mix(in srgb, var(--primitive-surface-elevated) 97%, black)", "primitive-overlay-face": "var(--primitive-surface-overlay)", "primitive-frame-face": "color-mix(in srgb, var(--primitive-surface-elevated) 97%, black)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-motion-fast": "80ms", "primitive-motion-fast-exit": "60ms", "primitive-motion-moderate": "160ms", "primitive-motion-moderate-exit": "120ms", "primitive-motion-slow": "240ms", "primitive-motion-slow-exit": "160ms", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" }, "dark": { "background": "#0a0a0b", "foreground": "#f5f5f6", "surface": "#111113", "surface-muted": "#18181b", "border": "rgba(255, 255, 255, 0.08)", "border-strong": "rgba(255, 255, 255, 0.14)", "muted": "#1c1c1f", "muted-foreground": "#a1a1aa", "ring": "#cd1c18", "pattern-fg": "rgba(255, 255, 255, 0.06)", "llb-primary": "#f5f5f6", "llb-primary-fg": "#0a0a0b", "llb-success": "#2da44e", "llb-error": "#f85149", "primitive-surface-elevated": "#0f0f10", "primitive-surface-overlay": "#141415", "primitive-surface-hover": "color-mix(in srgb, var(--foreground) 5%, transparent)", "primitive-surface-active": "color-mix(in srgb, var(--foreground) 10%, transparent)", "primitive-surface-selected": "color-mix(in srgb, var(--foreground) 8%, transparent)", "primitive-border-subtle": "color-mix(in srgb, var(--border) 60%, transparent)", "primitive-ring": "color-mix(in srgb, var(--foreground) 45%, transparent)", "primitive-text-secondary": "color-mix(in srgb, var(--foreground) 72%, transparent)", "primitive-text-placeholder": "color-mix(in srgb, var(--muted-foreground) 85%, transparent)", "primitive-radius": "0.75rem", "primitive-font-sans": "var(--font-sans)", "primitive-font-display": "var(--font-display)", "primitive-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", "primitive-control-solid": "#f5f3ee", "primitive-control-solid-hover": "#ffffff", "primitive-control-solid-active": "#e8e4da", "primitive-control-solid-foreground": "#151719", "primitive-chart-1": "#3987e5", "primitive-chart-2": "#199e70", "primitive-chart-3": "#c98500", "primitive-chart-4": "#008300", "primitive-chart-5": "#9085e9", "primitive-chart-6": "#e66767", "primitive-text-inverse": "#151719", "primitive-text-hint": "0.6875rem", "primitive-destructive": "#e11d2e", "primitive-destructive-hover": "#c1121f", "primitive-destructive-active": "#a4161a", "primitive-destructive-foreground": "#ffffff", "primitive-destructive-surface": "rgba(225, 29, 46, 0.12)", "primitive-destructive-border": "rgba(225, 29, 46, 0.52)", "primitive-success": "#34d399", "primitive-success-surface": "rgba(52, 211, 153, 0.12)", "primitive-success-border": "rgba(52, 211, 153, 0.45)", "primitive-warning": "#fbbf24", "primitive-warning-surface": "rgba(251, 191, 36, 0.12)", "primitive-warning-border": "rgba(251, 191, 36, 0.45)", "primitive-info": "#38bdf8", "primitive-info-surface": "rgba(56, 189, 248, 0.12)", "primitive-info-border": "rgba(56, 189, 248, 0.45)", "primitive-radius-control": "var(--primitive-radius)", "primitive-radius-control-sm": "min(calc(var(--primitive-radius) * 0.8), 12px)", "primitive-radius-surface": "calc(var(--primitive-radius) * 1.4)", "primitive-radius-popup": "var(--primitive-radius)", "primitive-radius-item": "calc(var(--primitive-radius) * 0.8)", "primitive-control-height-sm": "2rem", "primitive-control-height-md": "2.25rem", "primitive-control-height-lg": "2.5rem", "primitive-shadow-raised": "inset 0 1px 0 0 rgba(255, 255, 255, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.6), 0 2px 6px -2px rgba(0, 0, 0, 0.5)", "primitive-shadow-overlay": "inset 0 1px 0 0 rgba(255, 255, 255, 0.06), 0 2px 4px -1px rgba(0, 0, 0, 0.4), 0 8px 16px -4px rgba(0, 0, 0, 0.45), 0 16px 32px -8px rgba(0, 0, 0, 0.65)", "primitive-shadow-modal": "inset 0 1px 0 0 rgba(255, 255, 255, 0.06), 0 4px 8px -2px rgba(0, 0, 0, 0.4), 0 16px 32px -8px rgba(0, 0, 0, 0.55), 0 32px 64px -16px rgba(0, 0, 0, 0.75)", "primitive-edge": "rgba(255, 255, 255, 0.09)", "primitive-catchlight": "rgba(255, 255, 255, 0.16)", "primitive-groove-shadow": "rgba(0, 0, 0, 0.55)", "primitive-groove-highlight": "rgba(255, 255, 255, 0.05)", "primitive-shadow-drop": "0 1px 1px -0.5px rgba(0, 0, 0, 0.6), 0 2px 4px -1px rgba(0, 0, 0, 0.4)", "primitive-shadow-recess": "inset 0 2px 3px -1px rgba(0, 0, 0, 0.5)", "primitive-control-face": "var(--primitive-frame-face)", "primitive-field-face": "var(--primitive-frame-face)", "primitive-well": "rgba(0, 0, 0, 0.35)", "primitive-shadow-well": "inset 0 1px 2px 0 rgba(0, 0, 0, 0.45)", "primitive-tint-hover": "rgba(255, 255, 255, 0.06)", "primitive-tint-press": "rgba(0, 0, 0, 0.2)", "primitive-footer": "color-mix(in srgb, var(--primitive-surface-elevated) 97%, white)", "primitive-overlay-face": "color-mix(in srgb, var(--primitive-surface-overlay) 94%, white)", "primitive-frame-face": "color-mix(in srgb, var(--primitive-surface-elevated) 97%, white)", "primitive-z-overlay": "100", "primitive-z-popover": "130", "primitive-z-toast": "140", "primitive-backdrop": "rgba(0, 0, 0, 0.6)", "primitive-motion-fast": "80ms", "primitive-motion-fast-exit": "60ms", "primitive-motion-moderate": "160ms", "primitive-motion-moderate-exit": "120ms", "primitive-motion-slow": "240ms", "primitive-motion-slow-exit": "160ms", "primitive-ease": "cubic-bezier(0.23, 1, 0.32, 1)" } }, "css": { "@layer components": { "[data-surface]": { "--surface-tint": "transparent", "border-width": "1px", "border-style": "solid", "border-color": "var(--surface-rim)", "border-top-color": "var(--surface-rim-top)", "background-color": "var(--surface-face)", "background-image": "linear-gradient(var(--surface-tint), var(--surface-tint))", "--tw-shadow": "var(--surface-depth)", "box-shadow": "var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)" }, "[data-surface=\"control\"]": { "--surface-face": "var(--primitive-control-face)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-catchlight)", "--surface-depth": "var(--primitive-shadow-drop)" }, "[data-surface=\"field\"]": { "--surface-face": "var(--primitive-field-face)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-catchlight)", "--surface-depth": "var(--primitive-shadow-drop)" }, "[data-surface=\"fill\"]": { "--surface-face": "var(--primitive-control-solid)", "--surface-rim": "color-mix(in srgb, var(--surface-face) 88%, black)", "--surface-rim-top": "color-mix(in srgb, var(--surface-face) 80%, white)", "--surface-depth": "var(--primitive-shadow-drop)" }, "[data-surface=\"flat\"], [data-surface=\"toggle\"], [data-surface=\"toggle-fill\"]": { "--surface-face": "transparent", "--surface-rim": "transparent", "--surface-rim-top": "transparent", "--surface-depth": "0 0 #0000" }, "[data-surface=\"toggle\"]:is([data-pressed]:not([data-pressed=\"false\"]), [aria-pressed=\"true\"])": { "--surface-face": "var(--primitive-control-face)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-catchlight)", "--surface-depth": "var(--primitive-shadow-drop)" }, "[data-surface=\"toggle-fill\"]:is([data-pressed]:not([data-pressed=\"false\"]), [aria-pressed=\"true\"])": { "--surface-face": "var(--primitive-control-solid)", "--surface-rim": "color-mix(in srgb, var(--surface-face) 88%, black)", "--surface-rim-top": "color-mix(in srgb, var(--surface-face) 80%, white)", "--surface-depth": "var(--primitive-shadow-drop)" }, "[data-surface=\"well\"]": { "--surface-face": "var(--primitive-well)", "--surface-rim": "transparent", "--surface-rim-top": "transparent", "--surface-depth": "var(--primitive-shadow-well)" }, "[data-surface=\"frame\"]": { "--surface-face": "var(--primitive-frame-face)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-catchlight)", "--surface-depth": "0 0 #0000" }, "[data-surface=\"panel\"]": { "--surface-face": "var(--primitive-surface-elevated)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-edge)", "--surface-depth": "var(--primitive-shadow-raised)" }, "[data-surface=\"overlay\"]": { "--surface-face": "var(--primitive-overlay-face)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-edge)", "--surface-depth": "var(--primitive-shadow-overlay)" }, "[data-surface=\"modal\"]": { "--surface-face": "var(--primitive-surface-elevated)", "--surface-rim": "var(--primitive-edge)", "--surface-rim-top": "var(--primitive-edge)", "--surface-depth": "var(--primitive-shadow-modal)" }, "[data-surface=\"footer\"]": { "--surface-face": "var(--primitive-footer)", "--surface-rim": "transparent", "--surface-rim-top": "transparent", "--surface-depth": "0 0 #0000", "border-width": "0", "background-image": "linear-gradient(to bottom, var(--primitive-groove-shadow) 0 1px, var(--primitive-groove-highlight) 1px 2px)", "background-size": "100% 2px", "background-repeat": "no-repeat", "background-position": "top" }, "@media (hover: hover)": { "[data-surface]:where(button, a[href], summary, label, [role=\"button\"], [role=\"tab\"], [role=\"combobox\"], [role=\"option\"], [role=\"menuitem\"]):where(:not(:disabled, [data-disabled], [aria-disabled=\"true\"])):hover": { "--surface-tint": "var(--primitive-tint-hover)" }, ":is([data-surface=\"control\"], [data-surface=\"field\"]):is([data-pressed]:not([data-pressed=\"false\"]), [aria-pressed=\"true\"]):where(:not(:disabled, [data-disabled], [aria-disabled=\"true\"])):hover": { "--surface-tint": "var(--primitive-tint-hover)" } }, ":is([data-surface=\"control\"], [data-surface=\"field\"]):is([data-pressed]:not([data-pressed=\"false\"]), [aria-pressed=\"true\"])": { "--surface-tint": "var(--primitive-tint-press)", "--surface-rim-top": "var(--surface-rim)", "--surface-depth": "var(--primitive-shadow-recess)" }, ":is([data-surface=\"control\"], [data-surface=\"field\"], [data-surface=\"fill\"], [data-surface=\"toggle\"], [data-surface=\"toggle-fill\"]):is([aria-expanded=\"true\"], [data-popup-open]):not(input, textarea)": { "--surface-tint": "var(--primitive-tint-press)", "--surface-rim-top": "var(--surface-rim)", "--surface-depth": "var(--primitive-shadow-recess)" }, ":is([data-surface=\"control\"], [data-surface=\"field\"], [data-surface=\"fill\"], [data-surface=\"toggle\"], [data-surface=\"toggle-fill\"]):where(button, a[href], summary, [role=\"button\"], [role=\"tab\"]):where(:not(:disabled, [data-disabled], [aria-disabled=\"true\"])):active": { "--surface-tint": "var(--primitive-tint-press)", "--surface-rim-top": "var(--surface-rim)", "--surface-depth": "var(--primitive-shadow-recess)" }, "[data-surface=\"flat\"]:where(button, a[href], summary, [role=\"button\"], [role=\"tab\"]):where(:not(:disabled, [data-disabled], [aria-disabled=\"true\"])):active": { "--surface-tint": "var(--primitive-tint-press)" }, "[data-surface]:is([aria-invalid=\"true\"], :has(> :is(input, textarea)[aria-invalid=\"true\"]))": { "--surface-rim": "var(--primitive-destructive-border)", "--surface-rim-top": "var(--primitive-destructive-border)" }, "[data-surface][data-surface]:is(:disabled, [data-disabled], [aria-disabled=\"true\"])": { "--surface-tint": "transparent", "--surface-rim-top": "var(--surface-rim)", "--surface-depth": "0 0 #0000" }, "[data-groove]": { "flex-shrink": "0", "border-width": "0", "background-image": "linear-gradient(var(--groove-direction, to bottom), var(--primitive-groove-shadow) 0 1px, var(--primitive-groove-highlight) 1px 2px)" }, "[data-groove=\"horizontal\"]": { "height": "2px" }, "[data-groove=\"vertical\"]": { "--groove-direction": "to right", "width": "2px", "align-self": "stretch" }, "[data-carve]": { "background-image": "linear-gradient(to bottom, var(--primitive-groove-shadow) 0 1px, var(--primitive-groove-highlight) 1px 2px)", "background-repeat": "no-repeat", "background-size": "100% 2px" }, "[data-carve=\"top\"]": { "background-position": "top" }, "[data-carve=\"bottom\"]": { "background-position": "bottom" } }, "@layer base": { ":where([data-wensity-primitive])": { "font-family": "var(--primitive-font-sans)" } }, "@utility scrollbar-hidden": { "scrollbar-width": "none", "-ms-overflow-style": "none", "&::-webkit-scrollbar": { "display": "none" } }, "@keyframes wensity-marquee-x": { "from": { "transform": "translate3d(0, 0, 0)" }, "to": { "transform": "translate3d(-50%, 0, 0)" } }, "@keyframes wensity-marquee-x-reverse": { "from": { "transform": "translate3d(-50%, 0, 0)" }, "to": { "transform": "translate3d(0, 0, 0)" } }, "@keyframes wensity-morph-rot-cw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(360deg)" } }, "@keyframes wensity-morph-rot-ccw": { "from": { "transform": "rotate(0deg)" }, "to": { "transform": "rotate(-360deg)" } } }, "docs": "Free Wensity component. Installs to @components/wensity/text-scribble.tsx. For Pro components and updates, use pnpm dlx wensity@latest add text-scribble.", "categories": [ "Text Animations", "wensity", "free" ], "meta": { "wensity": { "slug": "text-scribble", "access": "free", "category": "Text Animations", "kind": "component", "componentUrl": "https://ui.wensity.com/components/text-scribble", "shadcnUrl": "https://ui.wensity.com/r/text-scribble", "cliInstall": "pnpm dlx wensity@latest add text-scribble" } } }