"use client"; import { type MotionValue, motion, useReducedMotion, useSpring, useTransform } from "motion/react"; import { type ComponentProps, useEffect, useId } from "react"; import { cn } from "@/lib/utils"; export type MorphIconName = "menu" | "play" | "plus" | "theme"; const lerp = (a: number, b: number, t: number) => a + (b - a) * t; const clamp01 = (t: number) => Math.min(Math.max(t, 0), 1); type Seg = { x: number; y: number; a: number; l: number }; function segPath(from: Seg, to: Seg, t: number) { const x = lerp(from.x, to.x, t); const y = lerp(from.y, to.y, t); const a = (lerp(from.a, to.a, t) * Math.PI) / 180; const l = Math.max(lerp(from.l, to.l, t), 0) / 2; const dx = Math.cos(a) * l; const dy = Math.sin(a) * l; return `M${(x - dx).toFixed(2)} ${(y - dy).toFixed(2)}L${(x + dx).toFixed(2)} ${(y + dy).toFixed(2)}`; } type Pt = [number, number]; function polyPath(from: Pt[], to: Pt[], t: number) { return `${from.map((p, i) => `${i ? "L" : "M"}${lerp(p[0], to[i][0], t).toFixed(2)} ${lerp(p[1], to[i][1], t).toFixed(2)}`).join("")}Z`; } function SegPath({ p, start, end, opacity }: { p: MotionValue; start: Seg; end: Seg; opacity?: MotionValue }) { const d = useTransform(p, (t) => segPath(start, end, t)); return ; } function PolyPath({ p, from, to }: { p: MotionValue; from: Pt[]; to: Pt[] }) { const d = useTransform(p, (t) => polyPath(from, to, t)); return ; } function MenuClose({ p }: { p: MotionValue }) { const mid = useTransform(p, [0, 0.45], [1, 0]); return ( <> ); } const PLAY: [Pt[], Pt[]] = [ [ [8.5, 5], [14, 8.5], [14, 15.5], [8.5, 19], ], [ [14, 8.5], [19.5, 12], [19.5, 12], [14, 15.5], ], ]; const PAUSE: [Pt[], Pt[]] = [ [ [6.5, 5], [10, 5], [10, 19], [6.5, 19], ], [ [14, 5], [17.5, 5], [17.5, 19], [14, 19], ], ]; function PlayPause({ p }: { p: MotionValue }) { return ( ); } function PlusCheck({ p }: { p: MotionValue }) { return ( <> ); } const RAYS = Array.from({ length: 8 }, (_, i) => i * 45); function SunMoon({ p }: { p: MotionValue }) { const maskId = `morph-moon-${useId().replace(/:/g, "")}`; const t = useTransform(p, clamp01); const r = useTransform(p, (v) => lerp(4.5, 8, v)); const mx = useTransform(t, (v) => lerp(26, 17, v)); const my = useTransform(t, (v) => lerp(-2, 7, v)); const spin = useTransform(p, (v) => v * 90); const rayOpacity = useTransform(t, [0, 0.6], [1, 0]); return ( <> {RAYS.map((a) => ( ))} ); } const ICONS = { menu: MenuClose, play: PlayPause, plus: PlusCheck, theme: SunMoon }; export interface MorphIconProps extends Omit, "children"> { name: MorphIconName; morphed: boolean; } export function MorphIcon({ name, morphed, className, ...props }: MorphIconProps) { const reduce = useReducedMotion(); const p = useSpring(morphed ? 1 : 0, { stiffness: 420, damping: 28, mass: 0.8 }); useEffect(() => { if (reduce) p.jump(morphed ? 1 : 0); else p.set(morphed ? 1 : 0); }, [morphed, reduce, p]); const Icon = ICONS[name]; return ( ); }