{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-modifier","type":"registry:ui","title":"Text Modifier","description":"Hand-drawn pen annotations (circles, underlines, boxes, strikes and crosses) that sketch themselves around words.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/text-modifier.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\nconst css = `@keyframes text-modifier-draw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}`;\n\nexport type TextModifierAction = \"underline\" | \"circle\" | \"box\" | \"strike\" | \"cross\";\n\nexport interface TextModifierProps {\n  children: ReactNode;\n  /** Annotation to draw. */\n  action?: TextModifierAction;\n  /** Pen color. */\n  color?: string;\n  /** Pen width, in px. */\n  strokeWidth?: number;\n  /** Space between the text and the annotation, in px. */\n  padding?: number;\n  /** Draw duration, in ms. */\n  duration?: number;\n  /** Delay after entering view, in ms. */\n  delay?: number;\n  /** Changes the hand-drawn wobble. */\n  seed?: number;\n  className?: string;\n}\n\ntype Pt = [number, number];\n\nfunction rng(seed: number) {\n  let s = seed * 2654435761;\n  return () => {\n    s = (s + 0x6d2b79f5) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296 - 0.5;\n  };\n}\n\n// Smooth curve through points using midpoint quadratics.\nconst smooth = (p: Pt[]) =>\n  p.reduce((d, [x, y], i) => {\n    if (i === 0) return `M${x.toFixed(1)} ${y.toFixed(1)}`;\n    const [nx, ny] = p[i + 1] ?? [x, y];\n    return `${d} Q${x.toFixed(1)} ${y.toFixed(1)} ${((x + nx) / 2).toFixed(1)} ${((y + ny) / 2).toFixed(1)}`;\n  }, \"\");\n\nfunction annotate(action: TextModifierAction, w: number, h: number, pad: number, seed: number) {\n  const r = rng(seed);\n  const j = (n: number) => r() * n;\n  switch (action) {\n    case \"underline\": {\n      const y = h + pad * 0.3;\n      return smooth([\n        [-pad * 0.6, y + j(3)],\n        [w * 0.5, y + 2 + j(4)],\n        [w + pad * 0.6, y - 2 + j(3)],\n        [w * 0.55, y + 5 + j(3)],\n        [w * 0.08, y + 8 + j(3)],\n      ]);\n    }\n    case \"strike\": {\n      const y = h * 0.55;\n      return smooth([[-pad * 0.6, y + j(4)], [w * 0.5, y - 3 + j(4)], [w + pad * 0.6, y + j(4)]]);\n    }\n    case \"cross\": {\n      const a = smooth([[-pad * 0.4, -pad * 0.3 + j(4)], [w * 0.5 + j(6), h * 0.5], [w + pad * 0.4, h + pad * 0.3 + j(4)]]);\n      const b = smooth([[w + pad * 0.4, -pad * 0.3 + j(4)], [w * 0.5 + j(6), h * 0.5], [-pad * 0.4, h + pad * 0.3 + j(4)]]);\n      return `${a} ${b}`;\n    }\n    case \"box\": {\n      const x0 = -pad, y0 = -pad * 0.6, x1 = w + pad, y1 = h + pad * 0.6;\n      const c = (x: number, y: number): Pt => [x + j(pad * 0.5), y + j(pad * 0.5)];\n      return smooth([\n        c(x0 + pad, y0), c(x1, y0 - 1), c(x1, y0), c(x1 + 1, y1), c(x1, y1),\n        c(x0, y1 + 1), c(x0, y1), c(x0 - 1, y0), c(x0, y0), c(x0 + pad * 2.5, y0 - 2),\n      ]);\n    }\n    default: {\n      const cx = w / 2, cy = h / 2, rx = w / 2 + pad * 1.3, ry = h / 2 + pad;\n      const start = -Math.PI * 0.62 + j(0.3);\n      const steps = 44;\n      const pts: Pt[] = [];\n      for (let i = 0; i <= steps; i++) {\n        const t = i / steps;\n        const a = start + t * Math.PI * 2 * 1.12;\n        const k = 1 + 0.07 * t + 0.03 * Math.sin(t * 9 + seed);\n        pts.push([cx + Math.cos(a) * rx * k, cy + Math.sin(a) * ry * k * 0.98]);\n      }\n      return smooth(pts);\n    }\n  }\n}\n\nexport function TextModifier({\n  children,\n  action = \"circle\",\n  color = \"#fb7185\",\n  strokeWidth = 3,\n  padding = 8,\n  duration = 900,\n  delay = 0,\n  seed = 3,\n  className,\n}: TextModifierProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [size, setSize] = useState<[number, number] | null>(null);\n  const [on, setOn] = useState(false);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setSize([el.offsetWidth, el.offsetHeight]));\n    const io = new IntersectionObserver(([e]) => {\n      if (e.isIntersecting) {\n        setOn(true);\n        io.disconnect();\n      }\n    }, { threshold: 0.6 });\n    ro.observe(el);\n    io.observe(el);\n    return () => {\n      ro.disconnect();\n      io.disconnect();\n    };\n  }, []);\n\n  const d = size ? annotate(action, size[0], size[1], padding, seed) : \"\";\n\n  return (\n    <span ref={ref} className={className} style={{ position: \"relative\", display: \"inline-block\" }}>\n      <style href=\"text-modifier\" precedence=\"default\">\n        {css}\n      </style>\n      {children}\n      {size && on && (\n        <svg\n          aria-hidden\n          width={size[0]}\n          height={size[1]}\n          style={{ position: \"absolute\", inset: 0, overflow: \"visible\", pointerEvents: \"none\" }}\n        >\n          <path\n            key={action}\n            d={d}\n            pathLength={1}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={strokeWidth}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeDasharray=\"1 1\"\n            style={{ animation: `text-modifier-draw ${duration}ms cubic-bezier(.55,.1,.3,1) ${delay}ms both` }}\n          />\n        </svg>\n      )}\n    </span>\n  );\n}\n"}]}