diff --git a/public/r/DecryptedText-JS-CSS.json b/public/r/DecryptedText-JS-CSS.json
index 23cfe85a..cb94cd6b 100644
--- a/public/r/DecryptedText-JS-CSS.json
+++ b/public/r/DecryptedText-JS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "DecryptedText/DecryptedText.jsx",
- "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\n\nconst styles = {\n wrapper: {\n display: 'inline-block',\n whiteSpace: 'pre-wrap'\n },\n srOnly: {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n border: 0\n }\n};\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText, currentRevealed) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n len => {\n const order = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback(() => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set, count) => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let interval;\n let currentIteration = 0;\n\n const getNextIndex = revealedSet => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(interval);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n // Reset animation state cleanly\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) {\n observer.unobserve(currentRef);\n }\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
+ "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\n\nconst styles = {\n wrapper: {\n display: 'inline-block',\n whiteSpace: 'pre-wrap'\n },\n srOnly: {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n border: 0\n }\n};\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n const intervalRef = useRef(null);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText, currentRevealed) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n len => {\n const order = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback(() => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set, count) => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let currentIteration = 0;\n\n const getNextIndex = revealedSet => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n intervalRef.current = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(intervalRef.current);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) {\n observer.unobserve(currentRef);\n }\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/DecryptedText-JS-TW.json b/public/r/DecryptedText-JS-TW.json
index e360243e..69e6c105 100644
--- a/public/r/DecryptedText-JS-TW.json
+++ b/public/r/DecryptedText-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "DecryptedText/DecryptedText.jsx",
- "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText, currentRevealed) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n len => {\n const order = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback(() => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set, count) => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let interval;\n let currentIteration = 0;\n\n const getNextIndex = revealedSet => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(interval);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n // Reset animation state cleanly\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) observer.unobserve(currentRef);\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
+ "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n const intervalRef = useRef(null);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText, currentRevealed) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n len => {\n const order = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback(() => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set, count) => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let currentIteration = 0;\n\n const getNextIndex = revealedSet => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n intervalRef.current = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(intervalRef.current);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n clearInterval(intervalRef.current);\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) observer.unobserve(currentRef);\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/DecryptedText-TS-CSS.json b/public/r/DecryptedText-TS-CSS.json
index 47f4cd6a..60f45cf9 100644
--- a/public/r/DecryptedText-TS-CSS.json
+++ b/public/r/DecryptedText-TS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "DecryptedText/DecryptedText.tsx",
- "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\nimport type { HTMLMotionProps } from 'motion/react';\n\nconst styles = {\n wrapper: {\n display: 'inline-block',\n whiteSpace: 'pre-wrap'\n },\n srOnly: {\n position: 'absolute' as const,\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n border: 0\n }\n};\n\ninterface DecryptedTextProps extends HTMLMotionProps<'span'> {\n text: string;\n speed?: number;\n maxIterations?: number;\n sequential?: boolean;\n revealDirection?: 'start' | 'end' | 'center';\n useOriginalCharsOnly?: boolean;\n characters?: string;\n className?: string;\n parentClassName?: string;\n encryptedClassName?: string;\n animateOn?: 'view' | 'hover' | 'inViewHover' | 'click';\n clickMode?: 'once' | 'toggle';\n}\n\ntype Direction = 'forward' | 'reverse';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText: string, currentRevealed: Set) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n (len: number): number[] => {\n const order: number[] = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback((): Set => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set: Set, count: number): Set => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let interval: ReturnType;\n let currentIteration = 0;\n\n const getNextIndex = (revealedSet: Set): number => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(interval);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n // Reset animation state cleanly\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) {\n observer.unobserve(currentRef);\n }\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
+ "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\nimport type { HTMLMotionProps } from 'motion/react';\n\nconst styles = {\n wrapper: {\n display: 'inline-block',\n whiteSpace: 'pre-wrap'\n },\n srOnly: {\n position: 'absolute' as const,\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n border: 0\n }\n};\n\ninterface DecryptedTextProps extends HTMLMotionProps<'span'> {\n text: string;\n speed?: number;\n maxIterations?: number;\n sequential?: boolean;\n revealDirection?: 'start' | 'end' | 'center';\n useOriginalCharsOnly?: boolean;\n characters?: string;\n className?: string;\n parentClassName?: string;\n encryptedClassName?: string;\n animateOn?: 'view' | 'hover' | 'inViewHover' | 'click';\n clickMode?: 'once' | 'toggle';\n}\n\ntype Direction = 'forward' | 'reverse';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n const intervalRef = useRef | null>(null);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText: string, currentRevealed: Set) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n (len: number): number[] => {\n const order: number[] = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback((): Set => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set: Set, count: number): Set => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let currentIteration = 0;\n\n const getNextIndex = (revealedSet: Set): number => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n intervalRef.current = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n\n return () => clearInterval(intervalRef.current ?? undefined);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) {\n observer.unobserve(currentRef);\n }\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/DecryptedText-TS-TW.json b/public/r/DecryptedText-TS-TW.json
index 90646f75..7812dfa0 100644
--- a/public/r/DecryptedText-TS-TW.json
+++ b/public/r/DecryptedText-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "DecryptedText/DecryptedText.tsx",
- "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\nimport type { HTMLMotionProps } from 'motion/react';\n\ninterface DecryptedTextProps extends HTMLMotionProps<'span'> {\n text: string;\n speed?: number;\n maxIterations?: number;\n sequential?: boolean;\n revealDirection?: 'start' | 'end' | 'center';\n useOriginalCharsOnly?: boolean;\n characters?: string;\n className?: string;\n encryptedClassName?: string;\n parentClassName?: string;\n animateOn?: 'view' | 'hover' | 'inViewHover' | 'click';\n clickMode?: 'once' | 'toggle';\n}\n\ntype Direction = 'forward' | 'reverse';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText: string, currentRevealed: Set) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n (len: number): number[] => {\n const order: number[] = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback((): Set => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set: Set, count: number): Set => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let interval: ReturnType;\n let currentIteration = 0;\n\n const getNextIndex = (revealedSet: Set): number => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n return () => clearInterval(interval);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n // Reset animation state cleanly\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) observer.unobserve(currentRef);\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
+ "content": "import { useEffect, useState, useRef, useMemo, useCallback } from 'react';\nimport { motion } from 'motion/react';\nimport type { HTMLMotionProps } from 'motion/react';\n\ninterface DecryptedTextProps extends HTMLMotionProps<'span'> {\n text: string;\n speed?: number;\n maxIterations?: number;\n sequential?: boolean;\n revealDirection?: 'start' | 'end' | 'center';\n useOriginalCharsOnly?: boolean;\n characters?: string;\n className?: string;\n encryptedClassName?: string;\n parentClassName?: string;\n animateOn?: 'view' | 'hover' | 'inViewHover' | 'click';\n clickMode?: 'once' | 'toggle';\n}\n\ntype Direction = 'forward' | 'reverse';\n\nexport default function DecryptedText({\n text,\n speed = 50,\n maxIterations = 10,\n sequential = false,\n revealDirection = 'start',\n useOriginalCharsOnly = false,\n characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',\n className = '',\n parentClassName = '',\n encryptedClassName = '',\n animateOn = 'hover',\n clickMode = 'once',\n ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isAnimating, setIsAnimating] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const [isDecrypted, setIsDecrypted] = useState(animateOn !== 'click');\n const [direction, setDirection] = useState('forward');\n\n const containerRef = useRef(null);\n const orderRef = useRef([]);\n const pointerRef = useRef(0);\n const intervalRef = useRef | null>(null);\n\n const availableChars = useMemo(() => {\n return useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n }, [useOriginalCharsOnly, text, characters]);\n\n const shuffleText = useCallback(\n (originalText: string, currentRevealed: Set) => {\n return originalText\n .split('')\n .map((char, i) => {\n if (char === ' ') return ' ';\n if (currentRevealed.has(i)) return originalText[i];\n return availableChars[Math.floor(Math.random() * availableChars.length)];\n })\n .join('');\n },\n [availableChars]\n );\n\n const computeOrder = useCallback(\n (len: number): number[] => {\n const order: number[] = [];\n if (len <= 0) return order;\n if (revealDirection === 'start') {\n for (let i = 0; i < len; i++) order.push(i);\n return order;\n }\n if (revealDirection === 'end') {\n for (let i = len - 1; i >= 0; i--) order.push(i);\n return order;\n }\n // center\n const middle = Math.floor(len / 2);\n let offset = 0;\n while (order.length < len) {\n if (offset % 2 === 0) {\n const idx = middle + offset / 2;\n if (idx >= 0 && idx < len) order.push(idx);\n } else {\n const idx = middle - Math.ceil(offset / 2);\n if (idx >= 0 && idx < len) order.push(idx);\n }\n offset++;\n }\n return order.slice(0, len);\n },\n [revealDirection]\n );\n\n const fillAllIndices = useCallback((): Set => {\n const s = new Set();\n for (let i = 0; i < text.length; i++) s.add(i);\n return s;\n }, [text]);\n\n const removeRandomIndices = useCallback((set: Set, count: number): Set => {\n const arr = Array.from(set);\n for (let i = 0; i < count && arr.length > 0; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr.splice(idx, 1);\n }\n return new Set(arr);\n }, []);\n\n const encryptInstantly = useCallback(() => {\n const emptySet = new Set();\n setRevealedIndices(emptySet);\n setDisplayText(shuffleText(text, emptySet));\n setIsDecrypted(false);\n }, [text, shuffleText]);\n\n const triggerDecrypt = useCallback(() => {\n if (sequential) {\n orderRef.current = computeOrder(text.length);\n pointerRef.current = 0;\n setRevealedIndices(new Set());\n } else {\n setRevealedIndices(new Set());\n }\n setDirection('forward');\n setIsAnimating(true);\n }, [sequential, computeOrder, text.length]);\n\n const triggerReverse = useCallback(() => {\n if (sequential) {\n // compute forward order then reverse it: we'll remove indices in that order\n orderRef.current = computeOrder(text.length).slice().reverse();\n pointerRef.current = 0;\n setRevealedIndices(fillAllIndices()); // start fully revealed\n setDisplayText(shuffleText(text, fillAllIndices()));\n } else {\n // non-seq: start from fully revealed as well\n setRevealedIndices(fillAllIndices());\n setDisplayText(shuffleText(text, fillAllIndices()));\n }\n setDirection('reverse');\n setIsAnimating(true);\n }, [sequential, computeOrder, fillAllIndices, shuffleText, text]);\n\n useEffect(() => {\n if (!isAnimating) return;\n\n let currentIteration = 0;\n\n const getNextIndex = (revealedSet: Set): number => {\n const textLength = text.length;\n switch (revealDirection) {\n case 'start':\n return revealedSet.size;\n case 'end':\n return textLength - 1 - revealedSet.size;\n case 'center': {\n const middle = Math.floor(textLength / 2);\n const offset = Math.floor(revealedSet.size / 2);\n const nextIndex = revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;\n\n if (nextIndex >= 0 && nextIndex < textLength && !revealedSet.has(nextIndex)) {\n return nextIndex;\n }\n for (let i = 0; i < textLength; i++) {\n if (!revealedSet.has(i)) return i;\n }\n return 0;\n }\n default:\n return revealedSet.size;\n }\n };\n\n intervalRef.current = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\n // Forward\n if (direction === 'forward') {\n if (prevRevealed.size < text.length) {\n const nextIndex = getNextIndex(prevRevealed);\n const newRevealed = new Set(prevRevealed);\n newRevealed.add(nextIndex);\n setDisplayText(shuffleText(text, newRevealed));\n return newRevealed;\n } else {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(true);\n return prevRevealed;\n }\n }\n // Reverse\n if (direction === 'reverse') {\n if (pointerRef.current < orderRef.current.length) {\n const idxToRemove = orderRef.current[pointerRef.current++];\n const newRevealed = new Set(prevRevealed);\n newRevealed.delete(idxToRemove);\n setDisplayText(shuffleText(text, newRevealed));\n if (newRevealed.size === 0) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n }\n return newRevealed;\n } else {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n return prevRevealed;\n }\n }\n } else {\n // Non-Sequential\n if (direction === 'forward') {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setDisplayText(text);\n setIsDecrypted(true);\n }\n return prevRevealed;\n }\n\n // Non-Sequential Reverse\n if (direction === 'reverse') {\n let currentSet = prevRevealed;\n if (currentSet.size === 0) {\n currentSet = fillAllIndices();\n }\n const removeCount = Math.max(1, Math.ceil(text.length / Math.max(1, maxIterations)));\n const nextSet = removeRandomIndices(currentSet, removeCount);\n setDisplayText(shuffleText(text, nextSet));\n currentIteration++;\n if (nextSet.size === 0 || currentIteration >= maxIterations) {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setIsDecrypted(false);\n // ensure final scrambled state\n setDisplayText(shuffleText(text, new Set()));\n return new Set();\n }\n return nextSet;\n }\n }\n return prevRevealed;\n });\n }, speed);\n return () => clearInterval(intervalRef.current ?? undefined);\n }, [\n isAnimating,\n text,\n speed,\n maxIterations,\n sequential,\n revealDirection,\n shuffleText,\n direction,\n fillAllIndices,\n removeRandomIndices,\n characters,\n useOriginalCharsOnly\n ]);\n\n /* Click Behaviour */\n const handleClick = () => {\n if (animateOn !== 'click') return;\n\n if (clickMode === 'once') {\n if (isDecrypted) return;\n setDirection('forward');\n triggerDecrypt();\n }\n\n if (clickMode === 'toggle') {\n if (isDecrypted) {\n triggerReverse();\n } else {\n setDirection('forward');\n triggerDecrypt();\n }\n }\n };\n\n /* Hover Behaviour */\n const triggerHoverDecrypt = useCallback(() => {\n if (isAnimating) return;\n\n setRevealedIndices(new Set());\n setIsDecrypted(false);\n setDisplayText(text);\n setDirection('forward');\n setIsAnimating(true);\n }, [isAnimating, text]);\n\n const resetToPlainText = useCallback(() => {\n clearInterval(intervalRef.current ?? undefined);\n setIsAnimating(false);\n setRevealedIndices(new Set());\n setDisplayText(text);\n setIsDecrypted(true);\n setDirection('forward');\n }, [text]);\n\n /* View Observer */\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'inViewHover') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n triggerDecrypt();\n setHasAnimated(true);\n }\n });\n };\n\n const observerOptions = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n };\n\n const observer = new IntersectionObserver(observerCallback, observerOptions);\n const currentRef = containerRef.current;\n if (currentRef) {\n observer.observe(currentRef);\n }\n\n return () => {\n if (currentRef) observer.unobserve(currentRef);\n };\n }, [animateOn, hasAnimated, triggerDecrypt]);\n\n useEffect(() => {\n if (animateOn === 'click') {\n encryptInstantly();\n } else {\n setDisplayText(text);\n setIsDecrypted(true);\n }\n setRevealedIndices(new Set());\n setDirection('forward');\n }, [animateOn, text, encryptInstantly]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: triggerHoverDecrypt,\n onMouseLeave: resetToPlainText\n }\n : animateOn === 'click'\n ? {\n onClick: handleClick\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || (!isAnimating && isDecrypted);\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/src/content/TextAnimations/DecryptedText/DecryptedText.jsx b/src/content/TextAnimations/DecryptedText/DecryptedText.jsx
index 6eab473b..4aae5b1e 100644
--- a/src/content/TextAnimations/DecryptedText/DecryptedText.jsx
+++ b/src/content/TextAnimations/DecryptedText/DecryptedText.jsx
@@ -43,6 +43,7 @@ export default function DecryptedText({
const containerRef = useRef(null);
const orderRef = useRef([]);
const pointerRef = useRef(0);
+ const intervalRef = useRef(null);
const availableChars = useMemo(() => {
return useOriginalCharsOnly
@@ -147,7 +148,6 @@ export default function DecryptedText({
useEffect(() => {
if (!isAnimating) return;
- let interval;
let currentIteration = 0;
const getNextIndex = revealedSet => {
@@ -176,7 +176,7 @@ export default function DecryptedText({
}
};
- interval = setInterval(() => {
+ intervalRef.current = setInterval(() => {
setRevealedIndices(prevRevealed => {
if (sequential) {
// Forward
@@ -188,7 +188,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, newRevealed));
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(true);
return prevRevealed;
@@ -202,13 +202,13 @@ export default function DecryptedText({
newRevealed.delete(idxToRemove);
setDisplayText(shuffleText(text, newRevealed));
if (newRevealed.size === 0) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
}
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
return prevRevealed;
@@ -220,7 +220,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, prevRevealed));
currentIteration++;
if (currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setDisplayText(text);
setIsDecrypted(true);
@@ -239,7 +239,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, nextSet));
currentIteration++;
if (nextSet.size === 0 || currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
// ensure final scrambled state
@@ -253,7 +253,7 @@ export default function DecryptedText({
});
}, speed);
- return () => clearInterval(interval);
+ return () => clearInterval(intervalRef.current);
}, [
isAnimating,
text,
@@ -293,16 +293,15 @@ export default function DecryptedText({
const triggerHoverDecrypt = useCallback(() => {
if (isAnimating) return;
- // Reset animation state cleanly
setRevealedIndices(new Set());
setIsDecrypted(false);
setDisplayText(text);
-
setDirection('forward');
setIsAnimating(true);
}, [isAnimating, text]);
const resetToPlainText = useCallback(() => {
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setRevealedIndices(new Set());
setDisplayText(text);
diff --git a/src/tailwind/TextAnimations/DecryptedText/DecryptedText.jsx b/src/tailwind/TextAnimations/DecryptedText/DecryptedText.jsx
index 34b4f90c..49377e0e 100644
--- a/src/tailwind/TextAnimations/DecryptedText/DecryptedText.jsx
+++ b/src/tailwind/TextAnimations/DecryptedText/DecryptedText.jsx
@@ -26,6 +26,7 @@ export default function DecryptedText({
const containerRef = useRef(null);
const orderRef = useRef([]);
const pointerRef = useRef(0);
+ const intervalRef = useRef(null);
const availableChars = useMemo(() => {
return useOriginalCharsOnly
@@ -130,7 +131,6 @@ export default function DecryptedText({
useEffect(() => {
if (!isAnimating) return;
- let interval;
let currentIteration = 0;
const getNextIndex = revealedSet => {
@@ -158,7 +158,7 @@ export default function DecryptedText({
}
};
- interval = setInterval(() => {
+ intervalRef.current = setInterval(() => {
setRevealedIndices(prevRevealed => {
if (sequential) {
// Forward
@@ -170,7 +170,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, newRevealed));
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(true);
return prevRevealed;
@@ -185,13 +185,13 @@ export default function DecryptedText({
newRevealed.delete(idxToRemove);
setDisplayText(shuffleText(text, newRevealed));
if (newRevealed.size === 0) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
}
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
return prevRevealed;
@@ -203,7 +203,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, prevRevealed));
currentIteration++;
if (currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setDisplayText(text);
setIsDecrypted(true);
@@ -222,7 +222,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, nextSet));
currentIteration++;
if (nextSet.size === 0 || currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setIsDecrypted(false);
// ensure final scrambled state
@@ -236,7 +236,7 @@ export default function DecryptedText({
});
}, speed);
- return () => clearInterval(interval);
+ return () => clearInterval(intervalRef.current);
}, [
isAnimating,
text,
@@ -276,16 +276,15 @@ export default function DecryptedText({
const triggerHoverDecrypt = useCallback(() => {
if (isAnimating) return;
- // Reset animation state cleanly
setRevealedIndices(new Set());
setIsDecrypted(false);
setDisplayText(text);
-
setDirection('forward');
setIsAnimating(true);
}, [isAnimating, text]);
const resetToPlainText = useCallback(() => {
+ clearInterval(intervalRef.current);
setIsAnimating(false);
setRevealedIndices(new Set());
setDisplayText(text);
diff --git a/src/ts-default/TextAnimations/DecryptedText/DecryptedText.tsx b/src/ts-default/TextAnimations/DecryptedText/DecryptedText.tsx
index a46b3c04..9b1f4954 100644
--- a/src/ts-default/TextAnimations/DecryptedText/DecryptedText.tsx
+++ b/src/ts-default/TextAnimations/DecryptedText/DecryptedText.tsx
@@ -61,6 +61,7 @@ export default function DecryptedText({
const containerRef = useRef(null);
const orderRef = useRef([]);
const pointerRef = useRef(0);
+ const intervalRef = useRef | null>(null);
const availableChars = useMemo(() => {
return useOriginalCharsOnly
@@ -165,7 +166,6 @@ export default function DecryptedText({
useEffect(() => {
if (!isAnimating) return;
- let interval: ReturnType;
let currentIteration = 0;
const getNextIndex = (revealedSet: Set): number => {
@@ -194,7 +194,7 @@ export default function DecryptedText({
}
};
- interval = setInterval(() => {
+ intervalRef.current = setInterval(() => {
setRevealedIndices(prevRevealed => {
if (sequential) {
// Forward
@@ -206,7 +206,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, newRevealed));
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(true);
return prevRevealed;
@@ -220,13 +220,13 @@ export default function DecryptedText({
newRevealed.delete(idxToRemove);
setDisplayText(shuffleText(text, newRevealed));
if (newRevealed.size === 0) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
}
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
return prevRevealed;
@@ -238,7 +238,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, prevRevealed));
currentIteration++;
if (currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setDisplayText(text);
setIsDecrypted(true);
@@ -257,7 +257,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, nextSet));
currentIteration++;
if (nextSet.size === 0 || currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
// ensure final scrambled state
@@ -271,7 +271,7 @@ export default function DecryptedText({
});
}, speed);
- return () => clearInterval(interval);
+ return () => clearInterval(intervalRef.current ?? undefined);
}, [
isAnimating,
text,
@@ -311,16 +311,15 @@ export default function DecryptedText({
const triggerHoverDecrypt = useCallback(() => {
if (isAnimating) return;
- // Reset animation state cleanly
setRevealedIndices(new Set());
setIsDecrypted(false);
setDisplayText(text);
-
setDirection('forward');
setIsAnimating(true);
}, [isAnimating, text]);
const resetToPlainText = useCallback(() => {
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setRevealedIndices(new Set());
setDisplayText(text);
diff --git a/src/ts-tailwind/TextAnimations/DecryptedText/DecryptedText.tsx b/src/ts-tailwind/TextAnimations/DecryptedText/DecryptedText.tsx
index 09dac7d0..9e8767db 100644
--- a/src/ts-tailwind/TextAnimations/DecryptedText/DecryptedText.tsx
+++ b/src/ts-tailwind/TextAnimations/DecryptedText/DecryptedText.tsx
@@ -44,6 +44,7 @@ export default function DecryptedText({
const containerRef = useRef(null);
const orderRef = useRef([]);
const pointerRef = useRef(0);
+ const intervalRef = useRef | null>(null);
const availableChars = useMemo(() => {
return useOriginalCharsOnly
@@ -148,7 +149,6 @@ export default function DecryptedText({
useEffect(() => {
if (!isAnimating) return;
- let interval: ReturnType;
let currentIteration = 0;
const getNextIndex = (revealedSet: Set): number => {
@@ -176,7 +176,7 @@ export default function DecryptedText({
}
};
- interval = setInterval(() => {
+ intervalRef.current = setInterval(() => {
setRevealedIndices(prevRevealed => {
if (sequential) {
// Forward
@@ -188,7 +188,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, newRevealed));
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(true);
return prevRevealed;
@@ -202,13 +202,13 @@ export default function DecryptedText({
newRevealed.delete(idxToRemove);
setDisplayText(shuffleText(text, newRevealed));
if (newRevealed.size === 0) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
}
return newRevealed;
} else {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
return prevRevealed;
@@ -220,7 +220,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, prevRevealed));
currentIteration++;
if (currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setDisplayText(text);
setIsDecrypted(true);
@@ -239,7 +239,7 @@ export default function DecryptedText({
setDisplayText(shuffleText(text, nextSet));
currentIteration++;
if (nextSet.size === 0 || currentIteration >= maxIterations) {
- clearInterval(interval);
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setIsDecrypted(false);
// ensure final scrambled state
@@ -252,7 +252,7 @@ export default function DecryptedText({
return prevRevealed;
});
}, speed);
- return () => clearInterval(interval);
+ return () => clearInterval(intervalRef.current ?? undefined);
}, [
isAnimating,
text,
@@ -292,16 +292,15 @@ export default function DecryptedText({
const triggerHoverDecrypt = useCallback(() => {
if (isAnimating) return;
- // Reset animation state cleanly
setRevealedIndices(new Set());
setIsDecrypted(false);
setDisplayText(text);
-
setDirection('forward');
setIsAnimating(true);
}, [isAnimating, text]);
const resetToPlainText = useCallback(() => {
+ clearInterval(intervalRef.current ?? undefined);
setIsAnimating(false);
setRevealedIndices(new Set());
setDisplayText(text);