{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "redaction",
  "title": "Redaction",
  "description": "Mark regions to black out, with a reveal that states it is only a preview.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/redaction/redaction.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Eye, EyeOff, Trash2, TriangleAlert } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type RedactionRegion = {\n  id: string\n  x: number\n  y: number\n  width: number\n  height: number\n  reason?: string\n}\n\nexport type RedactionRect = Pick<\n  RedactionRegion,\n  \"x\" | \"y\" | \"width\" | \"height\"\n>\n\nexport type RedactionProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"children\"\n> & {\n  src: string\n  alt: string\n  regions: RedactionRegion[]\n  onCreate?: (rect: RedactionRect) => void\n  onDelete?: (id: string) => void\n  revealed?: boolean\n  defaultRevealed?: boolean\n  onRevealedChange?: (revealed: boolean) => void\n  readOnly?: boolean\n  minSize?: number\n  label?: string\n  renderImage?: (props: {\n    src: string\n    alt: string\n    className: string\n  }) => React.ReactNode\n}\n\ntype Draft = { startX: number; startY: number; x: number; y: number }\n\nfunction clamp(value: number) {\n  return Math.max(0, Math.min(1, value))\n}\n\nfunction toPercent(value: number) {\n  return `${clamp(value) * 100}%`\n}\n\nfunction rectOf(draft: Draft): RedactionRect {\n  return {\n    x: Math.min(draft.startX, draft.x),\n    y: Math.min(draft.startY, draft.y),\n    width: Math.abs(draft.x - draft.startX),\n    height: Math.abs(draft.y - draft.startY),\n  }\n}\n\nexport function Redaction({\n  src,\n  alt,\n  regions,\n  onCreate,\n  onDelete,\n  revealed,\n  defaultRevealed = false,\n  onRevealedChange,\n  readOnly = false,\n  minSize = 0.01,\n  label = \"Redactions\",\n  renderImage,\n  className,\n  ...rootProps\n}: RedactionProps) {\n  const surfaceRef = React.useRef<HTMLDivElement>(null)\n  const [draft, setDraft] = React.useState<Draft | null>(null)\n  const [uncontrolledRevealed, setUncontrolledRevealed] =\n    React.useState(defaultRevealed)\n\n  const showing = revealed ?? uncontrolledRevealed\n  const drawable = !readOnly && Boolean(onCreate)\n\n  const toggleReveal = () => {\n    const next = !showing\n    if (revealed === undefined) setUncontrolledRevealed(next)\n    onRevealedChange?.(next)\n  }\n\n  const pointFrom = (event: React.PointerEvent) => {\n    const bounds = surfaceRef.current?.getBoundingClientRect()\n    if (!bounds || bounds.width === 0) return null\n\n    return {\n      x: clamp((event.clientX - bounds.left) / bounds.width),\n      y: clamp((event.clientY - bounds.top) / bounds.height),\n    }\n  }\n\n  const imageClassName = \"block h-auto w-full select-none\"\n\n  return (\n    <section\n      data-slot=\"redaction\"\n      data-revealed={showing || undefined}\n      aria-label={label}\n      className={cn(\"grid gap-3\", className)}\n      {...rootProps}\n    >\n      <div\n        ref={surfaceRef}\n        data-slot=\"redaction-surface\"\n        className={cn(\n          \"border-border bg-muted relative overflow-hidden rounded-[var(--radius)] border\",\n          drawable && \"cursor-crosshair\"\n        )}\n        onPointerDown={(event) => {\n          if (!drawable || event.button !== 0) return\n          const point = pointFrom(event)\n          if (!point) return\n\n          event.currentTarget.setPointerCapture(event.pointerId)\n          setDraft({ startX: point.x, startY: point.y, ...point })\n        }}\n        onPointerMove={(event) => {\n          if (!draft) return\n          const point = pointFrom(event)\n          if (!point) return\n\n          setDraft((current) => (current ? { ...current, ...point } : current))\n        }}\n        onPointerUp={() => {\n          if (!draft) return\n\n          const rect = rectOf(draft)\n          setDraft(null)\n          if (rect.width < minSize || rect.height < minSize) return\n\n          onCreate?.(rect)\n        }}\n        onPointerCancel={() => setDraft(null)}\n      >\n        {renderImage ? (\n          renderImage({ src, alt, className: imageClassName })\n        ) : (\n          // eslint-disable-next-line @next/next/no-img-element\n          <img\n            alt={alt}\n            className={imageClassName}\n            src={src}\n            draggable={false}\n          />\n        )}\n\n        <ul className=\"absolute inset-0 m-0 list-none p-0\">\n          {regions.map((region, index) => (\n            <li\n              key={region.id}\n              data-slot=\"redaction-region\"\n              data-revealed={showing || undefined}\n              className={cn(\n                \"absolute\",\n                showing\n                  ? \"border-destructive bg-destructive/15 border-2 border-dashed\"\n                  : \"bg-foreground\"\n              )}\n              style={{\n                left: toPercent(region.x),\n                top: toPercent(region.y),\n                width: toPercent(region.width),\n                height: toPercent(region.height),\n              }}\n            >\n              <span className=\"sr-only\">\n                Redaction {index + 1}\n                {region.reason ? `: ${region.reason}` : \"\"}\n                {showing ? \", currently revealed for review\" : \"\"}\n              </span>\n            </li>\n          ))}\n        </ul>\n\n        {draft ? (\n          <div\n            aria-hidden=\"true\"\n            className=\"border-foreground bg-foreground/20 pointer-events-none absolute border-2 border-dashed\"\n            style={{\n              left: toPercent(rectOf(draft).x),\n              top: toPercent(rectOf(draft).y),\n              width: toPercent(rectOf(draft).width),\n              height: toPercent(rectOf(draft).height),\n            }}\n          />\n        ) : null}\n      </div>\n\n      <div className=\"flex flex-wrap items-center gap-3\">\n        <p\n          data-slot=\"redaction-count\"\n          className=\"text-muted-foreground text-sm\"\n        >\n          {regions.length} {regions.length === 1 ? \"redaction\" : \"redactions\"}\n        </p>\n\n        <button\n          type=\"button\"\n          data-slot=\"redaction-reveal\"\n          aria-pressed={showing}\n          className=\"border-border hover:bg-muted focus-visible:ring-ring ml-auto inline-flex min-h-9 items-center gap-1.5 rounded-full border px-3 text-xs font-semibold transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n          onClick={toggleReveal}\n        >\n          {showing ? (\n            <EyeOff aria-hidden=\"true\" size={13} />\n          ) : (\n            <Eye aria-hidden=\"true\" size={13} />\n          )}\n          {showing ? \"Hide again\" : \"Reveal for review\"}\n        </button>\n      </div>\n\n      {showing ? (\n        <p\n          role=\"status\"\n          className=\"text-destructive flex items-center gap-2 text-xs\"\n        >\n          <TriangleAlert aria-hidden=\"true\" size={13} className=\"shrink-0\" />\n          Revealed for review. These regions are only covered visually here, so\n          redact the source file before sharing it.\n        </p>\n      ) : null}\n\n      {regions.length > 0 && onDelete && !readOnly ? (\n        <ul data-slot=\"redaction-list\" className=\"grid gap-1\">\n          {regions.map((region, index) => (\n            <li\n              key={region.id}\n              className=\"border-border flex items-center gap-2 rounded-[calc(var(--radius)-0.25rem)] border px-3 py-1.5 text-sm\"\n            >\n              <span className=\"text-muted-foreground font-[family-name:var(--font-mono),monospace] text-xs tabular-nums\">\n                {index + 1}\n              </span>\n              <span className=\"min-w-0 flex-1 truncate\">\n                {region.reason ?? \"No reason given\"}\n              </span>\n              <button\n                type=\"button\"\n                className=\"text-muted-foreground hover:text-destructive focus-visible:ring-ring -my-1 inline-flex size-9 shrink-0 items-center justify-center rounded-full transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n                onClick={() => onDelete(region.id)}\n              >\n                <span className=\"sr-only\">Remove redaction {index + 1}</span>\n                <Trash2 aria-hidden=\"true\" size={14} />\n              </button>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </section>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "document",
    "redaction",
    "privacy"
  ],
  "type": "registry:ui"
}