A staged voice memo that becomes a polished transcript.
Recording
Voice detected · 00:08
"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>
);
}