"use client"; import { useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; type Point = { t: number; before: number; after: number }; const DROP_REASONS: Record = { 6: "Weak hook caused early drop-off — viewers didn't engage with the opening line", 12: "Mid-section lacks pacing — generic advice triggered attention drop", 20: "CTA appeared too early — created friction before value was delivered", 30: "Momentum lost after repositioned CTA — body needs stronger bridge", 45: "Engagement floor reached — retained viewers are highly-interested segment", 60: "End-card CTA fires here — drop is expected at natural completion point", }; function auc(points: Point[], key: "before" | "after"): number { let area = 0; for (let i = 1; i < points.length; i++) { const dt = points[i].t - points[i - 1].t; area += ((points[i][key] + points[i - 1][key]) / 2) * dt; } return area / (points[points.length - 1].t * 100); } interface CustomDotProps { cx?: number; cy?: number; payload?: Point; dataKey?: string; activePoint?: number | null; onHover?: (t: number | null) => void; } function CustomDot({ cx = 0, cy = 0, payload, dataKey, activePoint, onHover }: CustomDotProps) { if (!payload || !onHover) return null; const isActive = activePoint === payload.t; const color = dataKey === "after" ? "#8b5cf6" : "#4b3a7a"; return ( onHover(payload.t)} onMouseLeave={() => onHover(null)} /> ); } export function RetentionChart({ data }: { data: Point[] }) { const [activePoint, setActivePoint] = useState(null); const aucBefore = auc(data, "before"); const aucAfter = auc(data, "after"); const dropPoint = data.find((p, i) => i > 0 && p.before < data[i - 1].before - 10); const dropAfterPoint = data.find((p, i) => i > 0 && p.after < data[i - 1].after - 10); return (
Retention Curve (0–60s)
setActivePoint(null)}> { if (!active || !payload?.length) return null; const t = payload[0].payload.t as number; const reason = DROP_REASONS[t]; return (

t = {t}s

Before: {payload.find((p) => p.dataKey === "before")?.value}%

After: {payload.find((p) => p.dataKey === "after")?.value}%

{reason &&

{reason}

}
); }} /> ( )} /> ( )} />
{activePoint !== null && DROP_REASONS[activePoint] && ( t={activePoint}s — {DROP_REASONS[activePoint]} )}
Retention Analysis

AUC Before → After

{aucBefore.toFixed(2)} → {aucAfter.toFixed(2)}

+{(((aucAfter - aucBefore) / aucBefore) * 100).toFixed(0)}% improvement

First Major Drop

{dropPoint?.t ?? "—"}s → {dropAfterPoint?.t ?? "—"}s

Drop point moved later

Explanation

Hook rewrite improved early engagement by delaying the first major drop-off and creating a stronger open loop in the first 6 seconds.

); }