'use client' import * as React from 'react' import { useReducedMotion } from '../../hooks/use-reduced-motion' import { cn } from '../../internal/utils' type TextAnimateSegment = 'char' | 'word' | 'line' type TextAnimateEffectName = 'typewriter' | 'scramble' | 'rise' | 'highlight' | 'wave' | 'flip' | 'shimmer' interface TextAnimateUnitContext { index: number total: number text: string by: TextAnimateSegment globalProgress: number reduced: boolean } type TextAnimateEffect = ( progress: number, ctx: TextAnimateUnitContext, ) => { style?: React.CSSProperties className?: string content?: React.ReactNode } type TextAnimateContainerEffect = ( progress: number, ctx: { reduced: boolean }, ) => { style?: React.CSSProperties className?: string } interface PresetConfig { fn: TextAnimateEffect by: TextAnimateSegment stagger: number continuous: boolean /** * Styles the wrapper around every unit rather than the units themselves, for effects that span the * whole string. Lets `by` stay orthogonal: the units can be split any way and the effect is unchanged. */ container?: TextAnimateContainerEffect } const SCRAMBLE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#%&@$?/<>*' const SCRAMBLE_STEPS = 10 const SHIMMER_SPREAD = 'var(--text-shimmer-spread, calc(3ch + 40px))' const SHIMMER_BASE = 'var(--text-shimmer-base, color-mix(in oklab, currentColor 38%, transparent))' const SHIMMER_GLARE = 'var(--text-shimmer-glare, currentColor)' const SHIMMER_MID = `color-mix(in oklab, ${SHIMMER_GLARE}, ${SHIMMER_BASE} 50%)` const SHIMMER_GRADIENT = `linear-gradient(calc(90deg + var(--text-shimmer-angle, 20deg)),` + ` ${SHIMMER_BASE} calc(50% - ${SHIMMER_SPREAD}),` + ` ${SHIMMER_MID} calc(50% - ${SHIMMER_SPREAD} * 0.5),` + ` ${SHIMMER_GLARE} 50%,` + ` ${SHIMMER_MID} calc(50% + ${SHIMMER_SPREAD} * 0.5),` + ` ${SHIMMER_BASE} calc(50% + ${SHIMMER_SPREAD}))` const presets: Record = { typewriter: { by: 'char', stagger: 1, continuous: false, fn: (p, ctx) => { const typed = p > 0 const head = Math.min(Math.floor(ctx.globalProgress * ctx.total), ctx.total - 1) const onEdge = ctx.index === head && ctx.globalProgress < 1 const caret = (onEdge || (ctx.globalProgress >= 1 && ctx.index === ctx.total - 1)) && (