{"version":3,"file":"animatedText.mjs","names":[],"sources":["../../../../package/common/animatedText/animatedText.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport { AnimatedTextVariant, AnimatedTextUnit } from './animatedText.types';\nimport type { AnimatedTextProps } from './animatedText.types';\nimport styles from './animatedText.module.pcss';\n\nfunction prefersReducedMotion(): boolean {\n  return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n    ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    : false;\n}\n\n// CSS-driven one-shot entrance variants: identical rendering, only the keyframe (via this class)\n// differs. RiseUp/BlurIn/Bounce/Glitch all animate opacity 0 -> 1 with `animation-fill-mode:\n// forwards`, staggered per unit via `animation-delay`.\nconst ENTRANCE_VARIANT_CLASS: Partial<Record<AnimatedTextVariant, string>> = {\n  [AnimatedTextVariant.RiseUp]: styles.animatedTextVariantRiseUp,\n  [AnimatedTextVariant.BlurIn]: styles.animatedTextVariantBlurIn,\n  [AnimatedTextVariant.Bounce]: styles.animatedTextVariantBounce,\n  [AnimatedTextVariant.Glitch]: styles.animatedTextVariantGlitch,\n};\n\nfunction splitUnits(text: string, unit: AnimatedTextUnit): string[] {\n  return unit === AnimatedTextUnit.Word ? text.split(' ').filter(Boolean) : Array.from(text);\n}\n\ninterface TypewriterProps {\n  units: string[];\n  unit: AnimatedTextUnit;\n  staggerDelay: number;\n  classNames: AnimatedTextProps['classNames'];\n  slotStyles: AnimatedTextProps['styles'];\n}\n\n// Split out so its wrapper (keyed by `replayKey`, see below) can be remounted on prop change to\n// reset this component's reveal state, the idiomatic React way to reset state on prop change.\n// It also keeps the reveal-count updates that happen over time (inside the interval callback) as\n// the only setState calls, none of them synchronous-in-effect.\nfunction Typewriter({ units, unit, staggerDelay, classNames, slotStyles }: TypewriterProps) {\n  const [revealCount, setRevealCount] = useState(() => {\n    if (units.length === 0) return 0;\n    return prefersReducedMotion() ? units.length : 1;\n  });\n\n  useEffect(() => {\n    if (units.length <= 1 || prefersReducedMotion()) return;\n    let revealed = 1;\n    const id = setInterval(() => {\n      revealed += 1;\n      setRevealCount(revealed);\n      if (revealed >= units.length) clearInterval(id);\n    }, staggerDelay);\n    return () => clearInterval(id);\n  }, [units, staggerDelay]);\n\n  const isDone = revealCount >= units.length;\n\n  return (\n    <>\n      {units.slice(0, revealCount).map((unitText, index) => (\n        <Fragment key={index}>\n          <span className={cx(styles.animatedTextTypewriterLetter, classNames?.letter)} style={slotStyles?.letter}>\n            {unitText}\n          </span>\n          {unit === AnimatedTextUnit.Word && index < revealCount - 1 ? ' ' : null}\n        </Fragment>\n      ))}\n      <span\n        className={cx(styles.animatedTextCursor, isDone && styles.animatedTextCursorBlinking, classNames?.cursor)}\n        style={slotStyles?.cursor}\n      />\n    </>\n  );\n}\n\nconst SCRAMBLE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*';\nconst SCRAMBLE_TICK_MS = 35;\n\nfunction randomScrambleText(length: number): string {\n  return Array.from({ length }, () => SCRAMBLE_CHARSET[Math.floor(Math.random() * SCRAMBLE_CHARSET.length)]).join('');\n}\n\ninterface ScrambleProps {\n  units: string[];\n  unit: AnimatedTextUnit;\n  staggerDelay: number;\n  classNames: AnimatedTextProps['classNames'];\n  slotStyles: AnimatedTextProps['styles'];\n}\n\n// Same keyed-remount-on-prop-change approach as Typewriter. Every unit starts scrambled and\n// \"locks in\" left to right; the scramble refresh (SCRAMBLE_TICK_MS) is a separate, faster cadence\n// than the per-unit settle pacing (staggerDelay) so still-scrambling units keep visibly flickering.\nfunction Scramble({ units, unit, staggerDelay, classNames, slotStyles }: ScrambleProps) {\n  const [reduceMotion] = useState(prefersReducedMotion);\n  const [tick, setTick] = useState(0);\n\n  useEffect(() => {\n    if (units.length === 0 || reduceMotion) return;\n    let t = 0;\n    const totalTicks = Math.ceil((units.length * staggerDelay) / SCRAMBLE_TICK_MS) + 1;\n    const id = setInterval(() => {\n      t += 1;\n      setTick(t);\n      if (t >= totalTicks) clearInterval(id);\n    }, SCRAMBLE_TICK_MS);\n    return () => clearInterval(id);\n  }, [units, staggerDelay, reduceMotion]);\n\n  const settledCount = Math.min(units.length, Math.floor((tick * SCRAMBLE_TICK_MS) / staggerDelay));\n\n  return (\n    <>\n      {units.map((unitText, index) => {\n        const isSettled = reduceMotion || index < settledCount;\n        return (\n          <Fragment key={index}>\n            <span className={cx(styles.animatedTextScrambleLetter, classNames?.letter)} style={slotStyles?.letter}>\n              {unitText === ' ' ? ' ' : isSettled ? unitText : randomScrambleText(unitText.length)}\n            </span>\n            {unit === AnimatedTextUnit.Word && index < units.length - 1 ? ' ' : null}\n          </Fragment>\n        );\n      })}\n    </>\n  );\n}\n\nfunction AnimatedText({\n  text,\n  variant = AnimatedTextVariant.RiseUp,\n  unit = AnimatedTextUnit.Letter,\n  staggerDelay = 40,\n  duration,\n  triggerOnView = true,\n  className,\n  style,\n  classNames,\n  styles: slotStyles,\n  ...rest\n}: AnimatedTextProps): ReactElement {\n  const units = useMemo(() => splitUnits(text, unit), [text, unit]);\n  const rootRef = useRef<HTMLSpanElement>(null);\n\n  // When `triggerOnView` is on, the animated subtree stays unmounted (see `isVisible` below)\n  // until the root first scrolls into the viewport, so the entrance/loop only plays once someone\n  // can actually see it instead of burning its one-shot animation off-screen on mount. Falls back\n  // to immediately visible when IntersectionObserver isn't available (SSR, old browsers, jsdom).\n  const [isVisible, setIsVisible] = useState(() => !triggerOnView || typeof IntersectionObserver === 'undefined');\n\n  useEffect(() => {\n    if (isVisible) return;\n    const node = rootRef.current;\n    if (!node) return;\n    const observer = new IntersectionObserver(([entry]) => {\n      if (entry?.isIntersecting) {\n        setIsVisible(true);\n        observer.disconnect();\n      }\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [isVisible]);\n\n  // Forces the animated subtree (not the whole component) to remount whenever anything that\n  // affects the animation changes, most commonly `text`. Without this, CSS entrance variants\n  // rely on `animation-fill-mode: forwards`, which just holds the prior end state instead of\n  // replaying when the letters underneath it change; keying on these values makes every variant\n  // (CSS-driven and JS-driven alike) replay consistently, matching what a component named\n  // AnimatedText should do when the text it's animating actually changes.\n  const replayKey = `${variant} ${text} ${unit} ${staggerDelay} ${duration ?? ''}`;\n\n  let content: ReactElement;\n\n  if (!isVisible) {\n    // Reserves the same layout space as the real content without playing anything yet.\n    content = (\n      <span aria-hidden='true' style={{ opacity: 0 }}>\n        {text}\n      </span>\n    );\n  } else if (variant === AnimatedTextVariant.Shine) {\n    content = (\n      <span\n        key={replayKey}\n        aria-hidden='true'\n        className={cx(styles.animatedTextShine, classNames?.letter)}\n        style={{ animationDuration: `${duration ?? 2000}ms`, ...slotStyles?.letter }}\n      >\n        {text}\n      </span>\n    );\n  } else if (variant === AnimatedTextVariant.Highlight) {\n    content = (\n      <span\n        key={replayKey}\n        aria-hidden='true'\n        className={cx(styles.animatedTextHighlight, classNames?.letter)}\n        style={{ animationDuration: `${duration ?? 600}ms`, ...slotStyles?.letter }}\n      >\n        {text}\n      </span>\n    );\n  } else if (variant === AnimatedTextVariant.Typewriter) {\n    content = (\n      <span key={replayKey} aria-hidden='true'>\n        <Typewriter\n          units={units}\n          unit={unit}\n          staggerDelay={staggerDelay}\n          classNames={classNames}\n          slotStyles={slotStyles}\n        />\n      </span>\n    );\n  } else if (variant === AnimatedTextVariant.Scramble) {\n    content = (\n      <span key={replayKey} aria-hidden='true'>\n        <Scramble\n          units={units}\n          unit={unit}\n          staggerDelay={staggerDelay}\n          classNames={classNames}\n          slotStyles={slotStyles}\n        />\n      </span>\n    );\n  } else if (variant === AnimatedTextVariant.Wave) {\n    content = (\n      <span key={replayKey} aria-hidden='true'>\n        {units.map((unitText, index) => (\n          <Fragment key={index}>\n            <span\n              className={cx(styles.animatedTextLetter, styles.animatedTextVariantWave, classNames?.letter)}\n              style={{\n                // Negative delay starts the loop already mid-cycle, phase-shifted per unit.\n                // That's what makes it read as a travelling wave from the very first frame,\n                // instead of every unit bobbing in lockstep or staggering in once and then syncing.\n                animationDelay: `-${index * staggerDelay}ms`,\n                animationDuration: `${duration ?? 1200}ms`,\n                ...slotStyles?.letter,\n              }}\n            >\n              {unitText === ' ' ? ' ' : unitText}\n            </span>\n            {unit === AnimatedTextUnit.Word && index < units.length - 1 ? ' ' : null}\n          </Fragment>\n        ))}\n      </span>\n    );\n  } else {\n    const entranceClass = ENTRANCE_VARIANT_CLASS[variant] ?? styles.animatedTextVariantRiseUp;\n    content = (\n      <span key={replayKey} aria-hidden='true'>\n        {units.map((unitText, index) => (\n          <Fragment key={index}>\n            <span\n              className={cx(styles.animatedTextLetter, entranceClass, classNames?.letter)}\n              style={{\n                animationDelay: `${index * staggerDelay}ms`,\n                animationDuration: `${duration ?? 500}ms`,\n                ...slotStyles?.letter,\n              }}\n            >\n              {unitText === ' ' ? ' ' : unitText}\n            </span>\n            {unit === AnimatedTextUnit.Word && index < units.length - 1 ? ' ' : null}\n          </Fragment>\n        ))}\n      </span>\n    );\n  }\n\n  return (\n    <span {...rest} ref={rootRef} className={cx(styles.animatedText, className)} style={style}>\n      <span className={styles.animatedTextVisuallyHidden}>{text}</span>\n      {content}\n    </span>\n  );\n}\n\nexport default AnimatedText;\n"],"mappings":"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"}