From 2386e45cc7ea030aa1d762b576a1fe2b4e0f6021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80ngel=20Arreola?= Date: Wed, 25 Feb 2026 02:21:38 -0600 Subject: [PATCH 1/4] feat(decrypted-text): add 'click' animationOn mode; rename "both" mode to "InViewHover" mode --- public/r/DecryptedText-JS-CSS.json | 2 +- public/r/DecryptedText-JS-TW.json | 2 +- public/r/DecryptedText-TS-CSS.json | 2 +- public/r/DecryptedText-TS-TW.json | 2 +- .../DecryptedText/DecryptedText.jsx | 25 +++++++++++++---- src/demo/TextAnimations/DecryptedTextDemo.jsx | 5 ++-- .../DecryptedText/DecryptedText.jsx | 25 +++++++++++++---- .../DecryptedText/DecryptedText.tsx | 27 ++++++++++++++----- .../DecryptedText/DecryptedText.tsx | 27 ++++++++++++++----- 9 files changed, 89 insertions(+), 28 deletions(-) diff --git a/public/r/DecryptedText-JS-CSS.json b/public/r/DecryptedText-JS-CSS.json index 69b4880d..95dbfdd3 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 } 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 ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText, currentRevealed) => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'both') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n setIsHovering(true);\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]);\n\n const hoverProps =\n animateOn === 'hover' || animateOn === 'both'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n" + "content": "import { useEffect, useState, useRef } 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 ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [clickCount, setClickCount] = useState(0);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText, currentRevealed) => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'click') return;\n if (clickCount === 0) return;\n\n setIsHovering(false);\n const timeout = setTimeout(() => setIsHovering(true), 0);\n\n return () => clearTimeout(timeout);\n }, [clickCount, animateOn]);\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 setIsHovering(true);\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]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : animateOn === 'click'\n ? {\n onClick: () => setClickCount(c => c + 1)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\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 059722bc..7a50c52f 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 } 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 ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText, currentRevealed) => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'both') return;\n\n const observerCallback = entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n setIsHovering(true);\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]);\n\n const hoverProps =\n animateOn === 'hover' || animateOn === 'both'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n" + "content": "import { useEffect, useState, useRef } 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 ...props\n}) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [clickCount, setClickCount] = useState(0);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText, currentRevealed) => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'click') return;\n if (clickCount === 0) return;\n\n setIsHovering(false);\n const timeout = setTimeout(() => setIsHovering(true), 0);\n\n return () => clearTimeout(timeout);\n }, [clickCount, animateOn]);\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 setIsHovering(true);\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]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : animateOn === 'click'\n ? {\n onClick: () => setClickCount(c => c + 1)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\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 04c072ce..90a8bbf1 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 } 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' | 'both';\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 ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText: string, currentRevealed: Set): string => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'both') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n setIsHovering(true);\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]);\n\n const hoverProps =\n animateOn === 'hover' || animateOn === 'both'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n" + "content": "import { useEffect, useState, useRef } 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}\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 ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [clickCount, setClickCount] = useState(0);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText: string, currentRevealed: Set): string => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'click') return;\n if (clickCount === 0) return;\n\n setIsHovering(false);\n const timeout = setTimeout(() => setIsHovering(true), 0);\n\n return () => clearTimeout(timeout);\n }, [clickCount, animateOn]);\n\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 setIsHovering(true);\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]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : animateOn === 'click'\n ? {\n onClick: () => setClickCount(c => c + 1)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\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 ff30e4be..62fc1871 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 } 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' | 'both';\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 ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText: string, currentRevealed: Set): string => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'view' && animateOn !== 'both') return;\n\n const observerCallback = (entries: IntersectionObserverEntry[]) => {\n entries.forEach(entry => {\n if (entry.isIntersecting && !hasAnimated) {\n setIsHovering(true);\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]);\n\n const hoverProps =\n animateOn === 'hover' || animateOn === 'both'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\n\n return (\n \n {char}\n \n );\n })}\n \n \n );\n}\n" + "content": "import { useEffect, useState, useRef } 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}\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 ...props\n}: DecryptedTextProps) {\n const [displayText, setDisplayText] = useState(text);\n const [isHovering, setIsHovering] = useState(false);\n const [clickCount, setClickCount] = useState(0);\n const [isScrambling, setIsScrambling] = useState(false);\n const [revealedIndices, setRevealedIndices] = useState>(new Set());\n const [hasAnimated, setHasAnimated] = useState(false);\n const containerRef = useRef(null);\n\n useEffect(() => {\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 const availableChars = useOriginalCharsOnly\n ? Array.from(new Set(text.split(''))).filter(char => char !== ' ')\n : characters.split('');\n\n const shuffleText = (originalText: string, currentRevealed: Set): string => {\n if (useOriginalCharsOnly) {\n const positions = originalText.split('').map((char, i) => ({\n char,\n isSpace: char === ' ',\n index: i,\n isRevealed: currentRevealed.has(i)\n }));\n\n const nonSpaceChars = positions.filter(p => !p.isSpace && !p.isRevealed).map(p => p.char);\n\n for (let i = nonSpaceChars.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [nonSpaceChars[i], nonSpaceChars[j]] = [nonSpaceChars[j], nonSpaceChars[i]];\n }\n\n let charIndex = 0;\n return positions\n .map(p => {\n if (p.isSpace) return ' ';\n if (p.isRevealed) return originalText[p.index];\n return nonSpaceChars[charIndex++];\n })\n .join('');\n } else {\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 };\n\n if (isHovering) {\n setIsScrambling(true);\n interval = setInterval(() => {\n setRevealedIndices(prevRevealed => {\n if (sequential) {\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 setIsScrambling(false);\n return prevRevealed;\n }\n } else {\n setDisplayText(shuffleText(text, prevRevealed));\n currentIteration++;\n if (currentIteration >= maxIterations) {\n clearInterval(interval);\n setIsScrambling(false);\n setDisplayText(text);\n }\n return prevRevealed;\n }\n });\n }, speed);\n } else {\n setDisplayText(text);\n setRevealedIndices(new Set());\n setIsScrambling(false);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]);\n\n useEffect(() => {\n if (animateOn !== 'click') return;\n if (clickCount === 0) return;\n\n setIsHovering(false);\n const timeout = setTimeout(() => setIsHovering(true), 0);\n\n return () => clearTimeout(timeout);\n }, [clickCount, animateOn]);\n\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 setIsHovering(true);\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]);\n\n const animateProps =\n animateOn === 'hover' || animateOn === 'inViewHover'\n ? {\n onMouseEnter: () => setIsHovering(true),\n onMouseLeave: () => setIsHovering(false)\n }\n : animateOn === 'click'\n ? {\n onClick: () => setClickCount(c => c + 1)\n }\n : {};\n\n return (\n \n {displayText}\n\n \n {displayText.split('').map((char, index) => {\n const isRevealedOrDone = revealedIndices.has(index) || !isScrambling || !isHovering;\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 bc611dac..6bc30091 100644 --- a/src/content/TextAnimations/DecryptedText/DecryptedText.jsx +++ b/src/content/TextAnimations/DecryptedText/DecryptedText.jsx @@ -34,6 +34,7 @@ export default function DecryptedText({ }) { const [displayText, setDisplayText] = useState(text); const [isHovering, setIsHovering] = useState(false); + const [clickCount, setClickCount] = useState(0); const [isScrambling, setIsScrambling] = useState(false); const [revealedIndices, setRevealedIndices] = useState(new Set()); const [hasAnimated, setHasAnimated] = useState(false); @@ -149,7 +150,17 @@ export default function DecryptedText({ }, [isHovering, text, speed, maxIterations, sequential, revealDirection, characters, useOriginalCharsOnly]); useEffect(() => { - if (animateOn !== 'view' && animateOn !== 'both') return; + if (animateOn !== 'click') return; + if (clickCount === 0) return; + + setIsHovering(false); + const timeout = setTimeout(() => setIsHovering(true), 0); + + return () => clearTimeout(timeout); + }, [clickCount, animateOn]); + + useEffect(() => { + if (animateOn !== 'view' && animateOn !== 'inViewHover') return; const observerCallback = entries => { entries.forEach(entry => { @@ -179,16 +190,20 @@ export default function DecryptedText({ }; }, [animateOn, hasAnimated]); - const hoverProps = - animateOn === 'hover' || animateOn === 'both' + const animateProps = + animateOn === 'hover' || animateOn === 'inViewHover' ? { onMouseEnter: () => setIsHovering(true), onMouseLeave: () => setIsHovering(false) } - : {}; + : animateOn === 'click' + ? { + onClick: () => setClickCount(c => c + 1) + } + : {}; return ( - + {displayText}