{"version":3,"file":"ngwr-split-text.mjs","sources":["../../../projects/lib/split-text/split-text.ts","../../../projects/lib/split-text/split-text.html","../../../projects/lib/split-text/ngwr-split-text.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n *\n * Angular adaptation of the SplitText effect by David Haz / reactbits.dev.\n * Original: https://www.reactbits.dev/text-animations/split-text\n *\n * The reactbits version uses GSAP's paid `SplitText` plugin. This port is\n * GSAP-free — splits via plain DOM spans, animates via the Web Animations\n * API, triggers on viewport entry via `IntersectionObserver`.\n */\n\nimport { isPlatformBrowser } from '@angular/common';\nimport {\n  Component,\n  DestroyRef,\n  ElementRef,\n  PLATFORM_ID,\n  ViewEncapsulation,\n  afterNextRender,\n  computed,\n  effect,\n  inject,\n  input,\n  output,\n} from '@angular/core';\n\nimport { WrPlatform } from 'ngwr/platform';\nimport { numAttr } from 'ngwr/utils';\n\nimport type { WrSplitTextUnit, WrSplitTextMotion } from './interfaces';\n\ntype Piece = { readonly kind: 'piece'; readonly text: string } | { readonly kind: 'space'; readonly text: string };\n\n/** Split `text` into render pieces, preserving whitespace as `kind: 'space'`. */\nfunction splitPieces(text: string, unit: WrSplitTextUnit): readonly Piece[] {\n  if (!text) return [];\n  if (unit === 'words') {\n    // Split on whitespace, keeping spaces as separate pieces.\n    return text.split(/(\\s+)/).flatMap<Piece>(seg => {\n      if (seg.length === 0) return [];\n      if (/^\\s+$/.test(seg)) return [{ kind: 'space', text: seg } as const];\n      return [{ kind: 'piece', text: seg } as const];\n    });\n  }\n  // chars: each character is its own piece; whitespace becomes 'space'.\n  return [...text].map<Piece>(ch => (/\\s/.test(ch) ? { kind: 'space', text: ch } : { kind: 'piece', text: ch }));\n}\n\nfunction motionToStyle(m: WrSplitTextMotion): { opacity: number; transform: string } {\n  const opacity = m.opacity ?? 1;\n  const parts: string[] = [];\n  if (m.x !== undefined || m.y !== undefined) {\n    parts.push(`translate3d(${m.x ?? 0}px, ${m.y ?? 0}px, 0)`);\n  }\n  if (m.scale !== undefined) parts.push(`scale(${m.scale})`);\n  if (m.rotate !== undefined) parts.push(`rotate(${m.rotate}deg)`);\n  return { opacity, transform: parts.length ? parts.join(' ') : 'none' };\n}\n\n/**\n * Animated text reveal — splits the input string into chars or words and\n * staggers each piece in from a `from` motion state to a `to` motion\n * state when the host scrolls into view. Whitespace is preserved.\n *\n * @example\n * ```html\n * <wr-split-text\n *   text=\"Hello, ngwr!\"\n *   splitType=\"chars\"\n *   [delay]=\"40\"\n *   [duration]=\"1.2\"\n *   [from]=\"{ opacity: 0, y: 40 }\"\n *   [to]=\"{ opacity: 1, y: 0 }\"\n *   (animationComplete)=\"onDone()\"\n * />\n * ```\n *\n * @see https://www.reactbits.dev/text-animations/split-text\n */\n@Component({\n  selector: 'wr-split-text',\n  templateUrl: './split-text.html',\n  styleUrl: './split-text.scss',\n  encapsulation: ViewEncapsulation.None,\n  host: { class: 'wr-split-text', '[style.text-align]': 'textAlign()' },\n})\nexport class WrSplitText {\n  /** Text to animate. Required. */\n  readonly text = input.required<string>();\n\n  /** Split granularity. @default 'chars' */\n  readonly splitType = input<WrSplitTextUnit>('chars');\n\n  /** Stagger delay between pieces in ms. @default 50 */\n  readonly delay = input(50, { transform: numAttr(50) });\n\n  /** Animation duration in seconds (matches reactbits' API). @default 1.25 */\n  readonly duration = input(1.25, { transform: numAttr(1.25) });\n\n  /** CSS easing function. @default 'cubic-bezier(0.16, 1, 0.3, 1)' (~power3.out) */\n  readonly easing = input('cubic-bezier(0.16, 1, 0.3, 1)');\n\n  /** Start state for each piece. @default { opacity: 0, y: 40 } */\n  readonly from = input<WrSplitTextMotion>({ opacity: 0, y: 40 });\n\n  /** End state for each piece. @default { opacity: 1, y: 0 } */\n  readonly to = input<WrSplitTextMotion>({ opacity: 1, y: 0 });\n\n  /** `IntersectionObserver.threshold` — 0..1. @default 0.1 */\n  readonly threshold = input(0.1, { transform: numAttr(0.1) });\n\n  /** `IntersectionObserver.rootMargin`. @default '-100px' */\n  readonly rootMargin = input('-100px');\n\n  /** Text alignment of the host. @default 'center' */\n  readonly textAlign = input<'left' | 'center' | 'right' | 'justify'>('center');\n\n  /** Emitted once all pieces finish animating. */\n  readonly animationComplete = output<void>();\n\n  /** Pieces to render. Each piece is `{ kind: 'piece', text }` or `{ kind: 'space' }`. */\n  protected readonly pieces = computed(() => splitPieces(this.text(), this.splitType()));\n\n  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n  private readonly destroyRef = inject(DestroyRef);\n  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n  private readonly platform = inject(WrPlatform);\n\n  private hasAnimated = false;\n\n  constructor() {\n    if (!this.isBrowser) return;\n\n    afterNextRender(() => this.startObserver());\n\n    // Re-run animation if `text` (and therefore the split pieces) changes.\n    effect(() => {\n      this.text();\n      this.splitType();\n      this.hasAnimated = false;\n      queueMicrotask(() => this.startObserver());\n    });\n  }\n\n  private startObserver(): void {\n    const host = this.host.nativeElement;\n    const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;\n    const ready = fonts?.status === 'loaded' ? Promise.resolve() : (fonts?.ready ?? Promise.resolve());\n\n    const observe = (): void => {\n      if (this.hasAnimated) return;\n      const io = new IntersectionObserver(\n        (entries, obs) => {\n          for (const e of entries) {\n            if (e.isIntersecting) {\n              obs.disconnect();\n              this.animate();\n              break;\n            }\n          }\n        },\n        { threshold: this.threshold(), rootMargin: this.rootMargin() }\n      );\n      io.observe(host);\n      this.destroyRef.onDestroy(() => io.disconnect());\n    };\n\n    void ready.then(observe);\n  }\n\n  private animate(): void {\n    if (this.hasAnimated) return;\n    this.hasAnimated = true;\n\n    // Reduced motion: pieces already render at their natural (final)\n    // state — skip the tween and report completion right away.\n    if (this.platform.prefersReducedMotion()) {\n      this.animationComplete.emit();\n      return;\n    }\n\n    const host = this.host.nativeElement;\n    const targets = host.querySelectorAll<HTMLElement>('.wr-split-text__piece');\n    if (targets.length === 0) {\n      this.animationComplete.emit();\n      return;\n    }\n\n    const fromStyle = motionToStyle(this.from());\n    const toStyle = motionToStyle(this.to());\n    const durationMs = this.duration() * 1000;\n    const stagger = this.delay();\n    const easing = this.easing();\n\n    let remaining = targets.length;\n    targets.forEach((el, i) => {\n      // Set initial state immediately so there's no flash of final state\n      // before the animation kicks in.\n      el.style.opacity = String(fromStyle.opacity);\n      el.style.transform = fromStyle.transform;\n      const animation = el.animate(\n        [\n          { opacity: fromStyle.opacity, transform: fromStyle.transform },\n          { opacity: toStyle.opacity, transform: toStyle.transform },\n        ],\n        { duration: durationMs, delay: i * stagger, easing, fill: 'forwards' }\n      );\n      animation.onfinish = (): void => {\n        // Commit final styles + clean up the WAAPI tween.\n        el.style.opacity = String(toStyle.opacity);\n        el.style.transform = toStyle.transform;\n        try {\n          animation.cancel();\n        } catch {\n          /* noop */\n        }\n        remaining -= 1;\n        if (remaining === 0) this.animationComplete.emit();\n      };\n    });\n  }\n}\n\nexport type { WrSplitTextUnit, WrSplitTextMotion } from './interfaces';\n","@for (p of pieces(); track $index) {\n  @if (p.kind === 'piece') {\n    <span class=\"wr-split-text__piece\">{{ p.text }}</span>\n  } @else {\n    <span class=\"wr-split-text__space\">{{ p.text }}</span>\n  }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}