{
  "name": "encrypted-text",
  "type": "registry:ui",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/encrypted-text.tsx",
      "content": "\"use client\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { motion, useInView } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\ntype EncryptedTextProps = {\n  text: string;\n  className?: string;\n  /**\n   * Time in milliseconds between revealing each subsequent real character.\n   * Lower is faster. Defaults to 50ms per character.\n   */\n  revealDelayMs?: number;\n  /** Optional custom character set to use for the gibberish effect. */\n  charset?: string;\n  /**\n   * Time in milliseconds between gibberish flips for unrevealed characters.\n   * Lower is more jittery. Defaults to 50ms.\n   */\n  flipDelayMs?: number;\n  /** CSS class for styling the encrypted/scrambled characters */\n  encryptedClassName?: string;\n  /** CSS class for styling the revealed characters */\n  revealedClassName?: string;\n};\n\nconst DEFAULT_CHARSET =\n  \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-={}[];:,.<>/?\";\n\nfunction generateRandomCharacter(charset: string): string {\n  const index = Math.floor(Math.random() * charset.length);\n  return charset.charAt(index);\n}\n\nfunction generateGibberishPreservingSpaces(\n  original: string,\n  charset: string,\n): string {\n  if (!original) return \"\";\n  let result = \"\";\n  for (let i = 0; i < original.length; i += 1) {\n    const ch = original[i];\n    result += ch === \" \" ? \" \" : generateRandomCharacter(charset);\n  }\n  return result;\n}\n\nexport const EncryptedText: React.FC<EncryptedTextProps> = ({\n  text,\n  className,\n  revealDelayMs = 50,\n  charset = DEFAULT_CHARSET,\n  flipDelayMs = 50,\n  encryptedClassName,\n  revealedClassName,\n}) => {\n  const ref = useRef<HTMLSpanElement>(null);\n  const isInView = useInView(ref, { once: true });\n\n  const [revealCount, setRevealCount] = useState<number>(0);\n  const animationFrameRef = useRef<number | null>(null);\n  const startTimeRef = useRef<number>(0);\n  const lastFlipTimeRef = useRef<number>(0);\n  const scrambleCharsRef = useRef<string[]>(\n    text ? generateGibberishPreservingSpaces(text, charset).split(\"\") : [],\n  );\n\n  useEffect(() => {\n    if (!isInView) return;\n\n    // Reset state for a fresh animation whenever dependencies change\n    const initial = text\n      ? generateGibberishPreservingSpaces(text, charset)\n      : \"\";\n    scrambleCharsRef.current = initial.split(\"\");\n    startTimeRef.current = performance.now();\n    lastFlipTimeRef.current = startTimeRef.current;\n    setRevealCount(0);\n\n    let isCancelled = false;\n\n    const update = (now: number) => {\n      if (isCancelled) return;\n\n      const elapsedMs = now - startTimeRef.current;\n      const totalLength = text.length;\n      const currentRevealCount = Math.min(\n        totalLength,\n        Math.floor(elapsedMs / Math.max(1, revealDelayMs)),\n      );\n\n      setRevealCount(currentRevealCount);\n\n      if (currentRevealCount >= totalLength) {\n        return;\n      }\n\n      // Re-randomize unrevealed scramble characters on an interval\n      const timeSinceLastFlip = now - lastFlipTimeRef.current;\n      if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {\n        for (let index = 0; index < totalLength; index += 1) {\n          if (index >= currentRevealCount) {\n            if (text[index] !== \" \") {\n              scrambleCharsRef.current[index] =\n                generateRandomCharacter(charset);\n            } else {\n              scrambleCharsRef.current[index] = \" \";\n            }\n          }\n        }\n        lastFlipTimeRef.current = now;\n      }\n\n      animationFrameRef.current = requestAnimationFrame(update);\n    };\n\n    animationFrameRef.current = requestAnimationFrame(update);\n\n    return () => {\n      isCancelled = true;\n      if (animationFrameRef.current !== null) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [isInView, text, revealDelayMs, charset, flipDelayMs]);\n\n  if (!text) return null;\n\n  return (\n    <motion.span\n      ref={ref}\n      className={cn(className)}\n      aria-label={text}\n      role=\"text\"\n    >\n      {text.split(\"\").map((char, index) => {\n        const isRevealed = index < revealCount;\n        const displayChar = isRevealed\n          ? char\n          : char === \" \"\n            ? \" \"\n            : (scrambleCharsRef.current[index] ??\n              generateRandomCharacter(charset));\n\n        return (\n          <span\n            key={index}\n            className={cn(isRevealed ? revealedClassName : encryptedClassName)}\n          >\n            {displayChar}\n          </span>\n        );\n      })}\n    </motion.span>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/encrypted-text.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Encrypted Text"
}