{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "redaction-hold-reveal",
  "title": "Redaction Hold Reveal",
  "description": "Inline redaction bar sized by the real text beneath it: hold to peek and the ink flows back after release, click to latch it open; the lifted bar stays hovering as a thin overline so the redaction never disappears.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/redaction-hold-reveal/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface UnderInkProps {\n  /** The sensitive text under the bar. Sized by the real text, so the bar never lies about length. */\n  children: string;\n  /** Tiny mono tag on the bar naming WHAT is hidden without revealing it — \"email\", \"amount\". */\n  label?: string;\n  /** ms after a hold-peek release before the ink flows back. */\n  resealMs?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst HOLD_THRESHOLD_MS = 280; // press shorter than this is a click-toggle, longer is a peek\n\n/**\n * Inline redaction. The bar is drawn over the real text (which sets the\n * width — a redaction that hides length hides less than people think, and a\n * fixed-width bar that lies about it reads as fake). Two ways in:\n * press-and-hold lifts the ink for a peek and it flows back shortly after\n * release; a plain click (or Enter/Space) latches it open until clicked\n * again. aria-pressed tracks whether the text is currently exposed.\n */\nexport function UnderInk({ children, label, resealMs = 900, className = \"\" }: UnderInkProps) {\n  const [latched, setLatched] = useState(false);\n  const [peeking, setPeeking] = useState(false);\n  const pressStartRef = useRef(0);\n  const resealRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const suppressClickRef = useRef(false);\n\n  const revealed = latched || peeking;\n\n  useEffect(() => () => window.clearTimeout(resealRef.current), []);\n\n  // Escape is the panic key: seal immediately, latched or mid-peek.\n  useEffect(() => {\n    if (!revealed) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        window.clearTimeout(resealRef.current);\n        setLatched(false);\n        setPeeking(false);\n      }\n    };\n    document.addEventListener(\"keydown\", onKey);\n    return () => document.removeEventListener(\"keydown\", onKey);\n  }, [revealed]);\n\n  const onPointerDown = (e: React.PointerEvent) => {\n    if (e.button !== 0 || latched) return;\n    window.clearTimeout(resealRef.current);\n    suppressClickRef.current = false; // a drag-out could leave a stale suppress behind\n    pressStartRef.current = performance.now();\n    setPeeking(true);\n  };\n\n  const endPress = () => {\n    if (!peeking) return;\n    const held = performance.now() - pressStartRef.current;\n    if (held >= HOLD_THRESHOLD_MS) {\n      // it was a peek: the ink takes a moment to flow back\n      suppressClickRef.current = true; // the click that follows pointerup must not latch\n      resealRef.current = setTimeout(() => setPeeking(false), resealMs);\n    } else {\n      // short press: let the click handler latch it\n      setPeeking(false);\n    }\n  };\n\n  const onClick = (e: React.MouseEvent) => {\n    if (suppressClickRef.current) {\n      suppressClickRef.current = false;\n      return;\n    }\n    // e.detail === 0 → keyboard activation (Enter/Space): always a toggle\n    window.clearTimeout(resealRef.current);\n    setPeeking(false);\n    setLatched((l) => !l);\n  };\n\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={revealed}\n      aria-label={\n        revealed\n          ? `Hide redacted ${label ?? \"content\"}`\n          : `Reveal redacted ${label ?? \"content\"} — hold to peek, click to keep open`\n      }\n      onPointerDown={onPointerDown}\n      onPointerUp={endPress}\n      onPointerCancel={endPress}\n      onPointerLeave={endPress}\n      onClick={onClick}\n      className={`ns-underink group relative inline-block cursor-pointer whitespace-nowrap rounded-[3px] px-[3px] align-baseline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent ${className}`}\n    >\n      <style>{`\n        .ns-underink .ns-underink-bar {\n          transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1), opacity 200ms ease-out;\n          transform-origin: top;\n        }\n        .ns-underink[aria-pressed=\"true\"] .ns-underink-bar {\n          transform: scaleY(0.14) translateY(-2px);\n          opacity: 0.85;\n        }\n        .ns-underink .ns-underink-text {\n          transition: opacity 180ms ease-out 60ms;\n        }\n        .ns-underink:hover .ns-underink-bar { opacity: 0.82; }\n        @media (prefers-reduced-motion: reduce) {\n          .ns-underink .ns-underink-bar, .ns-underink .ns-underink-text { transition: none; }\n        }\n      `}</style>\n      {/* the real text sets the width; hidden from everyone (visually + AT) while sealed */}\n      <span\n        data-underink-text\n        aria-hidden={!revealed}\n        className={`ns-underink-text relative z-0 ${revealed ? \"opacity-100\" : \"opacity-0\"}`}\n      >\n        {children}\n      </span>\n      {/* the ink: lifts to a thin overline on reveal, so the redaction never disappears — it hovers over what it was hiding */}\n      <span\n        aria-hidden\n        className=\"ns-underink-bar absolute inset-0 z-10 rounded-[3px] bg-foreground\"\n      >\n        {label && (\n          // stays mounted; hidden by opacity when revealed. Unmounting it on\n          // reveal removed the exact node under a centred click between\n          // pointerdown and pointerup, so the browser fired no `click` and the\n          // latch never happened.\n          <span\n            className={`flex h-full items-center justify-center overflow-hidden px-1 font-mono text-[9px] uppercase tracking-[0.18em] text-background/80 ${revealed ? \"opacity-0\" : \"opacity-100\"}`}\n          >\n            {label}\n          </span>\n        )}\n      </span>\n    </button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/redaction-hold-reveal.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-accent": "#006bff"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "redaction",
      "typography",
      "privacy",
      "reveal",
      "hold",
      "inline",
      "accessibility"
    ],
    "instruction": "An inline redaction primitive: `<UnderInk label=\"email\">p.raghavan@…</UnderInk>` drops into running prose. The bar is drawn OVER the real text, which stays in flow and sets the width — a fixed-width bar lies about the hidden content's length and reads as fake; this one cannot. An optional `label` renders as a tiny uppercase mono tag centered on the ink ('NAME', 'AMOUNT'), naming what is hidden without revealing it, so a redacted document self-explains at rest. TWO WAYS IN, one control: press-and-hold (≥280ms) lifts the ink for a peek — on release the text stays exposed for `resealMs` (default 900) and then the ink flows back, because real ink takes a moment to pool; a short press, plain click, or keyboard Enter/Space latches the reveal open until activated again (e.detail===0 keyboard clicks always toggle). The pointer paths are disambiguated by press duration with a suppressed synthetic click after a genuine hold, and pointercancel/pointerleave end a peek safely, so a drag off the bar never wedges it open. THE LIFT: revealing doesn't delete the bar — it scales to a 14%-height overline hovering above the text (scaleY transform, transform-origin top, 240ms cubic-bezier(0.22,1,0.36,1)), so what was redacted remains visibly marked as redacted even while exposed, and re-sealing is the same motion reversed. The text beneath fades in 60ms behind the lift so ink and ink-shadow never show doubled. ACCESSIBILITY: the whole thing is one real <button> with aria-pressed tracking exposure and an accessible name that states both the label and the interaction contract ('Reveal redacted email — hold to peek, click to keep open'); the hidden text is aria-hidden while sealed so assistive tech cannot read through the ink, and un-hidden the moment it is exposed. Focus shows a visible accent outline ring; --ns-accent appears nowhere else. Escape is the panic key: while anything is exposed a document-level listener re-seals it instantly, latched or mid-peek. REDUCED MOTION: lift and fade transitions drop to instant toggles — every state still reachable, nothing hidden. Colors are tokens only: the ink is --foreground, the tag text is --background over it, so the bar is true black-on-white and white-on-black in the two themes with zero hue. Pure DOM/CSS, zero dependencies, no canvas.",
    "rank": 289
  },
  "type": "registry:ui"
}