File size: 2,739 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | import React, { useId, useMemo } from "react";
import { useReducedMotion } from "framer-motion";
import { cn } from "#/utils/utils";
/**
* The gradient is an oversized repeating pattern; animating background-position
* by exactly one period loops with no visible jump and no muted gap between
* sweeps (the pattern is periodic, so a highlight is always near the text).
*/
const SHIMMER_BACKGROUND_SIZE = "200%";
/** Gradient period as a percentage of the (oversized) background image. */
const SHIMMER_PERIOD = 8;
/** Shifting background-position by one period == 2 * period (image is 2x wide). */
const SHIMMER_TRAVEL = SHIMMER_PERIOD * 2;
export type TextShimmerProps = {
children: string;
as?: React.ElementType;
className?: string;
duration?: number;
spread?: number;
} & Omit<React.HTMLAttributes<HTMLElement>, "children" | "className">;
function TextShimmerComponent({
children,
as: Component = "p",
className,
duration = 2,
spread = 2,
style,
...rest
}: TextShimmerProps) {
const reduceMotion = useReducedMotion();
const reactId = useId();
const animationName = `oh-text-shimmer-${reactId.replace(/:/g, "")}`;
// Wider spread => wider bright band within each repeating period.
const bandHalfWidth = useMemo(
() => Math.min(SHIMMER_PERIOD / 2 - 1, 1 + spread / 2),
[spread],
);
const shimmerStyle = useMemo(() => {
const center = SHIMMER_PERIOD / 2;
return {
...style,
backgroundImage: `repeating-linear-gradient(90deg, var(--oh-muted) 0%, var(--oh-muted) ${center - bandHalfWidth}%, var(--oh-foreground) ${center}%, var(--oh-muted) ${center + bandHalfWidth}%, var(--oh-muted) ${SHIMMER_PERIOD}%)`,
backgroundSize: `${SHIMMER_BACKGROUND_SIZE} 100%`,
backgroundRepeat: "no-repeat",
WebkitBackgroundClip: "text",
backgroundClip: "text",
color: "transparent",
WebkitTextFillColor: "transparent",
animation: `${animationName} ${duration}s linear infinite`,
} as React.CSSProperties;
}, [animationName, bandHalfWidth, duration, style]);
if (reduceMotion) {
return (
<Component
className={cn("text-[var(--oh-muted)]", className)}
style={style}
{...rest}
>
{children}
</Component>
);
}
return (
<>
<style
dangerouslySetInnerHTML={{
__html: `@keyframes ${animationName}{from{background-position:${SHIMMER_TRAVEL}% center}to{background-position:0% center}}`,
}}
/>
<Component
className={cn("relative inline-block", className)}
style={shimmerStyle}
{...rest}
>
{children}
</Component>
</>
);
}
export const TextShimmer = React.memo(TextShimmerComponent);
|