v0.1.8
Menu
Download

Framer Motion

Framer Motion shows up on the marketing site hero, not in the desktop player control dock.

Landing hero title is a React island (HeroAnimatedTitle.tsx) that wraps shared AnimatedText. On mount, motion variants fade the headline in and draw an SVG underline path. Hover morphs the path to a second Bezier for a hand-drawn wiggle.

The desktop app lists motion/react (and legacy framer-motion in places) for mini player visualizer morphs and some AnimatePresence transitions in App.tsx tab swaps. Site and app pin different motion packages; each package.json owns its version.

Simple hovers (buttons, cards, mega-menu links) stay CSS transitions in global.css. Motion is reserved for sequenced hero text where coordinating opacity + path length in CSS would be annoying to maintain.

In the repo

  return (
    <AnimatedText
      className="w-full mb-4 md:mb-6"
      textClassName="font-hand text-5xl font-bold tracking-tight text-rf-text sm:text-6xl lg:text-7xl leading-[1.1]"
      underlineClassName="text-rf-accent"
      underlinePath="M 0,10 Q 75,0 150,10 Q 225,20 300,10"
      underlineHoverPath="M 0,10 Q 75,20 150,10 Q 225,0 300,10"
      underlineDuration={1.5}
    >
      {title}
    </AnimatedText>
  );
}
const AnimatedText = React.forwardRef<HTMLDivElement, AnimatedTextProps>(
  (
    {
      text,
      children,
      textClassName,
      underlineClassName,
      underlinePath = 'M 0,10 Q 75,0 150,10 Q 225,20 300,10',
      underlineHoverPath = 'M 0,10 Q 75,20 150,10 Q 225,0 300,10',
      underlineDuration = 1.5,
      ...props
    },
    ref,
  ) => {
    const pathVariants: Variants = {
      hidden: {
        pathLength: 0,
        opacity: 0,
      },
      visible: {
        pathLength: 1,
        opacity: 1,
        transition: {
          duration: underlineDuration,
          ease: 'easeInOut',
        },
      },
    };

    return (
      <div
        ref={ref}
        className={cn('flex flex-col items-center justify-center gap-2', props.className)}
        {...props}
      >
        <div className="relative pb-6">
          <motion.h1
            className={cn('text-4xl font-bold text-center', textClassName)}
            initial={{ y: -20, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            transition={{ duration: 0.6 }}
            whileHover={{ scale: 1.02 }}
          >
            {children ?? text}
          </motion.h1>

          <motion.svg
            width="100%"
            height="20"
            viewBox="0 0 300 20"
            className={cn('absolute bottom-0 left-0', underlineClassName)}
            aria-hidden="true"
          >
            <motion.path
              d={underlinePath}
              stroke="currentColor"
              strokeWidth="2"
              fill="none"
              variants={pathVariants}
              initial="hidden"
              animate="visible"
              whileHover={{
                d: underlineHoverPath,
                transition: { duration: 0.8 },
              }}
            />
          </motion.svg>
        </div>

Where it shows up

  • website/src/components/HeroAnimatedTitle.tsx
  • website/src/components/ui/animated-underline-text-one.tsx
  • MiniPlayer.tsx compact visualizer morph paths (app bundle)
  • App.tsx AnimatePresence tab transitions (motion/react)