{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sensitive-text","type":"registry:ui","title":"Sensitive Text","description":"Spoiler text veiled by blur and shimmering noise particles until you hover, focus or click it.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/sensitive-text.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\nexport interface SensitiveTextProps {\n  children: ReactNode;\n  /** Reveal while hovered/focused, or toggle on click/Enter/Space. */\n  revealOn?: \"hover\" | \"click\";\n  /** Blur applied to the hidden text, in px. */\n  blur?: number;\n  /** Particle density multiplier for the noise veil. */\n  density?: number;\n  /** Particle drift speed multiplier. */\n  speed?: number;\n  /** Accessible label for the hidden state. */\n  label?: string;\n  className?: string;\n}\n\nexport function SensitiveText({\n  children,\n  revealOn = \"hover\",\n  blur = 8,\n  density = 1,\n  speed = 1,\n  label = \"Hidden text, activate to reveal\",\n  className,\n}: SensitiveTextProps) {\n  const root = useRef<HTMLSpanElement>(null);\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const [hover, setHover] = useState(false);\n  const [open, setOpen] = useState(false);\n  const revealed = revealOn === \"hover\" ? hover : open;\n  const visible = useRef(true);\n  useEffect(() => {\n    visible.current = !revealed;\n  }, [revealed]);\n\n  useEffect(() => {\n    const el = root.current;\n    const cv = canvas.current;\n    const ctx = cv?.getContext(\"2d\");\n    if (!el || !cv || !ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    let w = 0;\n    let h = 0;\n    let dots: { x: number; y: number; vx: number; vy: number; p: number }[] = [];\n    let color = \"#fff\";\n\n    const resize = () => {\n      w = el.offsetWidth + 8;\n      h = el.offsetHeight + 4;\n      cv.width = w * dpr;\n      cv.height = h * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      color = getComputedStyle(el).color;\n      const count = Math.round(((w * h) / 9) * density);\n      dots = Array.from({ length: count }, () => ({\n        x: Math.random() * w,\n        y: Math.random() * h,\n        vx: (Math.random() - 0.5) * 0.35,\n        vy: (Math.random() - 0.5) * 0.35,\n        p: Math.random() * Math.PI * 2,\n      }));\n    };\n    const draw = (t: number) => {\n      ctx.clearRect(0, 0, w, h);\n      ctx.fillStyle = color;\n      for (const d of dots) {\n        d.x = (d.x + d.vx * speed + w) % w;\n        d.y = (d.y + d.vy * speed + h) % h;\n        ctx.globalAlpha = 0.35 + 0.65 * Math.abs(Math.sin(d.p + t * 0.002 * speed));\n        ctx.fillRect(d.x, d.y, 1.2, 1.2);\n      }\n    };\n\n    let raf = 0;\n    let onScreen = false;\n    const loop = (t: number) => {\n      raf = requestAnimationFrame(loop);\n      if (visible.current) draw(t);\n    };\n    const ro = new ResizeObserver(() => {\n      resize();\n      draw(0);\n    });\n    const io = new IntersectionObserver(([e]) => {\n      onScreen = e.isIntersecting;\n      cancelAnimationFrame(raf);\n      if (onScreen && !reduced) raf = requestAnimationFrame(loop);\n    });\n    ro.observe(el);\n    io.observe(el);\n    return () => {\n      ro.disconnect();\n      io.disconnect();\n      cancelAnimationFrame(raf);\n    };\n  }, [density, speed]);\n\n  return (\n    <span\n      ref={root}\n      role=\"button\"\n      tabIndex={0}\n      aria-pressed={revealOn === \"click\" ? open : undefined}\n      aria-label={revealed ? undefined : label}\n      className={`relative inline-block cursor-pointer rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring ${className ?? \"\"}`}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n      onFocus={() => setHover(true)}\n      onBlur={() => setHover(false)}\n      onClick={() => setOpen((o) => !o)}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          setOpen((o) => !o);\n        }\n      }}\n    >\n      <span\n        aria-hidden={!revealed}\n        style={{\n          display: \"inline-block\",\n          filter: revealed ? \"blur(0)\" : `blur(${blur}px)`,\n          opacity: revealed ? 1 : 0.35,\n          transition: \"filter .5s ease, opacity .5s ease\",\n        }}\n      >\n        {children}\n      </span>\n      <canvas\n        ref={canvas}\n        aria-hidden\n        className=\"pointer-events-none absolute\"\n        style={{\n          left: -4,\n          top: -2,\n          width: \"calc(100% + 8px)\",\n          height: \"calc(100% + 4px)\",\n          opacity: revealed ? 0 : 1,\n          transform: revealed ? \"scale(1.08)\" : \"none\",\n          transition: \"opacity .45s ease, transform .6s ease\",\n        }}\n      />\n    </span>\n  );\n}\n"}]}