Back

Transcription Demo

A staged voice memo that becomes a polished transcript.

Recording

Voice detected · 00:08

Live
Listening for context•••
transcription-demo.tsx
"use client";

import {
  AnimatePresence,
  domAnimation,
  LazyMotion,
  MotionConfig,
  useReducedMotion,
} from "motion/react";
import * as m from "motion/react-m";
import { useEffect, useMemo, useState } from "react";

const transcriptLines = [
  "We should lead with the customer quote, then tighten the transition into the product demo.",
  "Action items: Caleb owns the prototype polish, Maya will trim the intro, and we review again at 3:30.",
];

const peaks: [string, number][] = [
  ["01", 0.16],
  ["02", 0.28],
  ["03", 0.2],
  ["04", 0.42],
  ["05", 0.36],
  ["06", 0.58],
  ["07", 0.3],
  ["08", 0.46],
  ["09", 0.68],
  ["10", 0.84],
  ["11", 0.54],
  ["12", 0.38],
  ["13", 0.26],
  ["14", 0.34],
  ["15", 0.56],
  ["16", 0.74],
  ["17", 0.88],
  ["18", 0.62],
  ["19", 0.42],
  ["20", 0.24],
  ["21", 0.18],
  ["22", 0.3],
  ["23", 0.48],
  ["24", 0.72],
  ["25", 0.58],
  ["26", 0.36],
  ["27", 0.22],
  ["28", 0.3],
  ["29", 0.5],
  ["30", 0.82],
  ["31", 0.7],
  ["32", 0.44],
  ["33", 0.26],
  ["34", 0.18],
  ["35", 0.24],
  ["36", 0.4],
  ["37", 0.64],
  ["38", 0.52],
  ["39", 0.32],
  ["40", 0.2],
];

export default function TranscriptionDemo() {
  const [phase, setPhase] = useState<"recording" | "transcribing" | "done">(
    "recording",
  );
  const shouldReduceMotion = useReducedMotion();

  useEffect(() => {
    const timers = [
      window.setTimeout(() => setPhase("transcribing"), 3200),
      window.setTimeout(() => setPhase("done"), 5600),
    ];

    return () => timers.forEach(window.clearTimeout);
  }, []);

  const status = useMemo(() => {
    if (phase === "recording")
      return { label: "Recording", detail: "Voice detected · 00:08" };
    if (phase === "transcribing")
      return {
        label: "Transcribing",
        detail: "Cleaning filler words and speaker pauses",
      };
    return { label: "Transcript ready", detail: "2 notes · 3 action items" };
  }, [phase]);

  return (
    <LazyMotion features={domAnimation}>
      <MotionConfig reducedMotion="user">
        <div className="flex min-h-[360px] items-center justify-center px-1 py-2 text-[#191714] antialiased">
          <div className="w-full max-w-[520px] rounded-[24px] bg-[#fffdf8] p-3 shadow-[0_18px_60px_rgba(32,24,12,0.12),0_2px_8px_rgba(32,24,12,0.06)]">
            <div className="rounded-[16px] bg-[#fffdf8] p-4 shadow-[inset_0_0_0_1px_rgba(25,23,20,0.07)] sm:p-5">
              <div className="rounded-2xl bg-[#191714] p-4 text-[#fffdf8] shadow-[0_16px_40px_rgba(25,23,20,0.16)] sm:p-5">
                <div className="flex items-center justify-between gap-3">
                  <div>
                    <AnimatePresence mode="wait" initial={false}>
                      <m.p
                        key={status.label}
                        initial={{ y: 8, opacity: 0, filter: "blur(4px)" }}
                        animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
                        exit={{ y: -6, opacity: 0, filter: "blur(4px)" }}
                        transition={{ duration: 0.28, ease: [0.2, 0, 0, 1] }}
                        className="text-sm font-medium"
                      >
                        {status.label}
                      </m.p>
                    </AnimatePresence>
                    <p className="mt-1 text-xs text-[#cfc7b8]">
                      {status.detail}
                    </p>
                  </div>
                  <div className="flex h-8 items-center gap-1 rounded-full bg-white/10 px-3 text-xs tabular-nums text-[#efe7d9]">
                    <span
                      className={
                        phase === "recording"
                          ? "size-2 rounded-full bg-[#ff6f61]"
                          : "size-2 rounded-full bg-[#86e0b8]"
                      }
                    />
                    {phase === "done" ? "Saved" : "Live"}
                  </div>
                </div>

                <div className="mt-6 flex h-[92px] items-center justify-center gap-[3px] overflow-hidden rounded-xl bg-[#26221d] px-4 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
                  {peaks.map(([id, peak], index) => (
                    <span
                      key={id}
                      className="flex h-16 w-[3px] items-center rounded-full bg-[#3a342c]"
                    >
                      <m.span
                        className="block w-full rounded-full bg-[#f6d365]"
                        style={{ height: "100%", transformOrigin: "50% 50%" }}
                        initial={false}
                        animate={{
                          scaleY:
                            phase === "recording" && !shouldReduceMotion
                              ? [
                                  Math.max(0.1, peak * 0.34),
                                  Math.min(
                                    1,
                                    peak * (1.06 + (index % 3) * 0.08),
                                  ),
                                  Math.max(0.12, peak * 0.48),
                                  Math.min(
                                    0.92,
                                    peak * (0.86 + (index % 4) * 0.07),
                                  ),
                                ]
                              : phase === "transcribing"
                                ? 0.18 + (index % 5) * 0.035
                                : 0.12,
                          opacity: phase === "done" ? 0.38 : 0.92,
                        }}
                        transition={{
                          duration: 0.42 + (index % 6) * 0.045,
                          delay:
                            phase === "recording" ? -(index % 9) * 0.08 : 0,
                          repeat:
                            phase === "recording" && !shouldReduceMotion
                              ? Infinity
                              : 0,
                          repeatType: "mirror",
                          ease: [0.25, 0.1, 0.25, 1],
                        }}
                      />
                    </span>
                  ))}
                </div>
              </div>

              <div className="mt-5 min-h-[124px] rounded-2xl bg-[#fffdf8]/80 p-4 shadow-[inset_0_0_0_1px_rgba(25,23,20,0.06)] backdrop-blur">
                <AnimatePresence mode="wait" initial={false}>
                  {phase !== "done" ? (
                    <m.div
                      key="thinking"
                      initial={{ opacity: 0, y: 8 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -6 }}
                      transition={{ duration: 0.24 }}
                      className="flex h-[92px] items-center justify-center gap-2 text-sm font-medium text-[#6d655b]"
                    >
                      <span>
                        {phase === "recording"
                          ? "Listening for context"
                          : "Drafting transcript"}
                      </span>
                      <m.span
                        animate={{ opacity: [0.25, 1, 0.25] }}
                        transition={{
                          duration: 1,
                          repeat: shouldReduceMotion ? 0 : Infinity,
                        }}
                      >
                        •••
                      </m.span>
                    </m.div>
                  ) : (
                    <m.div key="transcript" className="space-y-3">
                      {transcriptLines.map((line, index) => (
                        <m.p
                          key={line}
                          initial={{ opacity: 0, y: 10, filter: "blur(4px)" }}
                          animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
                          transition={{
                            duration: 0.36,
                            delay: index * 0.12,
                            ease: [0.2, 0, 0, 1],
                          }}
                          className="text-pretty text-sm leading-6 text-[#2a2621]"
                        >
                          {line}
                        </m.p>
                      ))}
                    </m.div>
                  )}
                </AnimatePresence>
              </div>
            </div>
          </div>
        </div>
      </MotionConfig>
    </LazyMotion>
  );
}