shimmer.tsx 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. 'use client';
  2. import { cn } from '@/lib/utils';
  3. import { type MotionProps, motion } from 'motion/react';
  4. import { type CSSProperties, type ElementType, type JSX, memo, useMemo, useRef } from 'react';
  5. type MotionComponentType = React.FC<
  6. MotionProps & React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }
  7. >;
  8. export type TextShimmerProps = {
  9. children: string;
  10. as?: ElementType;
  11. className?: string;
  12. duration?: number;
  13. spread?: number;
  14. };
  15. /* eslint-disable react-hooks/refs -- Ref-based cache for motion.create component identity */
  16. const ShimmerComponent = ({
  17. children,
  18. as: Component = 'p',
  19. className,
  20. duration = 2,
  21. spread = 2,
  22. }: TextShimmerProps) => {
  23. const motionRef = useRef<MotionComponentType | null>(null);
  24. const prevComponentRef = useRef(Component);
  25. if (!motionRef.current || prevComponentRef.current !== Component) {
  26. motionRef.current = motion.create(
  27. Component as keyof JSX.IntrinsicElements,
  28. ) as unknown as MotionComponentType;
  29. prevComponentRef.current = Component;
  30. }
  31. const MotionComponent = motionRef.current;
  32. const dynamicSpread = useMemo(() => (children?.length ?? 0) * spread, [children, spread]);
  33. return (
  34. <MotionComponent
  35. animate={{ backgroundPosition: '0% center' }}
  36. className={cn(
  37. 'relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent',
  38. '[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]',
  39. className,
  40. )}
  41. initial={{ backgroundPosition: '100% center' }}
  42. style={
  43. {
  44. '--spread': `${dynamicSpread}px`,
  45. backgroundImage:
  46. 'var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))',
  47. } as CSSProperties
  48. }
  49. transition={{
  50. repeat: Number.POSITIVE_INFINITY,
  51. duration,
  52. ease: 'linear',
  53. }}
  54. >
  55. {children}
  56. </MotionComponent>
  57. );
  58. };
  59. /* eslint-enable react-hooks/refs */
  60. export const Shimmer = memo(ShimmerComponent);