"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { StepControls } from "./step-controls"; type Mode = "toSF" | "toOrd"; interface DisplayState { digits: string[]; decimalPos: number; initPos: number; targetPos: number; origPower: number; } function parseOrdinary(raw: string): { digits: string[]; decimalPos: number } { const s = raw.trim().replace(/[\s,]/g, ""); if (s.startsWith("-")) throw new Error("Please enter a positive number."); if (!/^\d*\.?\d+$/.test(s) || s === "") throw new Error("Invalid number. Try something like 7438 or 0.0055"); if (s.length > 16) throw new Error("Number too long (max 16 digits)."); const dot = s.indexOf("."); if (dot === -1) { return { digits: s.split(""), decimalPos: s.length }; } return { digits: (s.slice(0, dot) + s.slice(dot + 1)).split(""), decimalPos: dot, }; } function firstNonZero(digits: string[]): number { return digits.findIndex((d) => d !== "0"); } function sfTargetPos(digits: string[]): number { const i = firstNonZero(digits); return i === -1 ? 1 : i + 1; } function buildCoeff(sigDigs: string[]): string { if (sigDigs.length === 0) return "0"; if (sigDigs.length === 1) return sigDigs[0] + ".0"; const rest = sigDigs.slice(1).join("").replace(/0+$/, "") || "0"; return sigDigs[0] + "." + rest; } function buildOrdinary(digits: string[], pos: number): string { let s: string; if (pos <= 0) { s = "0." + "0".repeat(-pos) + digits.join(""); } else if (pos >= digits.length) { s = digits.join("") + "0".repeat(pos - digits.length); } else { s = digits.slice(0, pos).join("") + "." + digits.slice(pos).join(""); } s = s.replace(/^0+(?=[1-9])/, ""); if (s.startsWith(".")) s = "0" + s; if (s.includes(".")) s = s.replace(/\.?0+$/, ""); return s || "0"; } export function StandardFormExplorer() { const [mode, setMode] = useState("toSF"); const [ordinaryInput, setOrdinaryInput] = useState(""); const [coeffInput, setCoeffInput] = useState(""); const [powerInput, setPowerInput] = useState(""); const [error, setError] = useState(""); const [display, setDisplay] = useState(null); const [currentPos, setCurrentPos] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [done, setDone] = useState(false); const numberRowRef = useRef(null); const dotRef = useRef(null); const [dotLeft, setDotLeft] = useState(0); const [dotAnimate, setDotAnimate] = useState(false); const [powerPop, setPowerPop] = useState(false); const totalSteps = display ? Math.abs(display.targetPos - display.initPos) : 0; const currentStep = display ? Math.abs(currentPos - display.initPos) : 0; const currentPower = display ? mode === "toSF" ? display.initPos - currentPos : display.origPower - (currentPos - display.initPos) : 0; // Position decimal dot const placeDot = useCallback( (animate: boolean) => { if (!numberRowRef.current || !display) return; const boxes = numberRowRef.current.querySelectorAll("[data-digit-box]"); if (!boxes.length) return; let leftPx: number; const dp = currentPos; if (dp <= 0) { leftPx = boxes[0].offsetLeft - 6; } else if (dp >= boxes.length) { const last = boxes[boxes.length - 1]; leftPx = last.offsetLeft + last.offsetWidth - 4; } else { const prev = boxes[dp - 1]; const next = boxes[dp]; leftPx = Math.round((prev.offsetLeft + prev.offsetWidth + next.offsetLeft) / 2); } setDotAnimate(animate); setDotLeft(leftPx); }, [currentPos, display], ); useEffect(() => { if (display) { requestAnimationFrame(() => { requestAnimationFrame(() => placeDot(true)); }); } }, [currentPos, display, placeDot]); // Initial placement without animation useEffect(() => { if (display) { requestAnimationFrame(() => { requestAnimationFrame(() => placeDot(false)); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [display?.digits.join("")]); function initDisplay(digits: string[], start: number, target: number, origPower: number) { setIsPlaying(false); setDone(start === target); setDisplay({ digits, decimalPos: start, initPos: start, targetPos: target, origPower }); setCurrentPos(start); setError(""); } function handleConvertToSF() { setError(""); try { const { digits, decimalPos } = parseOrdinary(ordinaryInput); const target = sfTargetPos(digits); initDisplay(digits, decimalPos, target, 0); } catch (e) { setError((e as Error).message); } } function handleConvertToOrd() { setError(""); try { if (!coeffInput.trim()) throw new Error("Enter the coefficient A."); const n = parseInt(powerInput, 10); if (isNaN(n)) throw new Error("Enter the power n as a whole number."); if (Math.abs(n) > 12) throw new Error("Power too large for display (max ±12)."); if (n === 0) throw new Error("Power is 0 — the number is already in ordinary form."); const { digits } = parseOrdinary(coeffInput); const A = parseFloat(coeffInput); if (isNaN(A) || A < 1 || A >= 10) throw new Error(`Coefficient must satisfy 1 ≤ A < 10 (got ${coeffInput}).`); const decimalPos = 1; const expandedDigits = [...digits]; let startPos: number; let targetPos: number; if (n > 0) { targetPos = decimalPos + n; startPos = decimalPos; while (expandedDigits.length < targetPos) expandedDigits.push("0"); } else { const absN = Math.abs(n); const zeros = Array(absN).fill("0"); expandedDigits.unshift(...zeros); startPos = decimalPos + absN; targetPos = startPos + n; } initDisplay(expandedDigits, startPos, targetPos, n); } catch (e) { setError((e as Error).message); } } const stepForward = useCallback(() => { if (!display || currentPos === display.targetPos) return; const dir = display.targetPos > display.initPos ? 1 : -1; const nextPos = currentPos + dir; setCurrentPos(nextPos); setPowerPop(true); setTimeout(() => setPowerPop(false), 300); if (nextPos === display.targetPos) { setDone(true); setIsPlaying(false); } }, [display, currentPos]); const stepBack = useCallback(() => { if (!display || currentPos === display.initPos) return; if (done) setDone(false); const dir = display.targetPos > display.initPos ? -1 : 1; setCurrentPos((p) => p + dir); setPowerPop(true); setTimeout(() => setPowerPop(false), 300); }, [display, currentPos, done]); const togglePlay = useCallback(() => { setIsPlaying((p) => !p); }, []); const reset = useCallback(() => { setIsPlaying(false); setDisplay(null); setDone(false); setError(""); }, []); // Direction label const remaining = display ? display.targetPos - currentPos : 0; const dirText = !display || done || remaining === 0 ? null : remaining < 0 ? `← Moving left · ${Math.abs(remaining)} step${Math.abs(remaining) !== 1 ? "s" : ""} remaining` : `→ Moving right · ${remaining} step${remaining !== 1 ? "s" : ""} remaining`; // Result const fz = display ? firstNonZero(display.digits) : 0; return (
{/* Mode Tabs */}
{/* Input Card */}

{mode === "toSF" ? "Enter an ordinary number" : "Enter a number in standard form A × 10ⁿ"}

{mode === "toSF" ? (
setOrdinaryInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleConvertToSF()} placeholder="e.g. 7438 or 0.0055" maxLength={18} autoComplete="off" className="max-w-[220px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-2" />
) : (
setCoeffInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleConvertToOrd()} placeholder="A e.g. 3.6" maxLength={12} autoComplete="off" className="max-w-[140px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-2" /> × 10n where n = setPowerInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleConvertToOrd()} placeholder="e.g. 4 or −3" autoComplete="off" className="max-w-[110px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-2" />
)} {error &&

{error}

}
{/* Display Card */} {!display ? (

Enter a number above and click Convert

) : ( <> {/* Step info */}

{totalSteps === 0 ? "Number is already in standard form position — power = 0" : `Step ${currentStep} of ${totalSteps}`}

{/* Digit row */}
{display.digits.map((ch, i) => { const isLeadingZero = i < fz && fz !== -1; const isHighlight = i === display.targetPos - 1; return (
{ch}
); })} {/* Decimal dot */}
= display.digits.length ? 0 : 1, }} />
{/* Direction label */} {dirText && (

{dirText}

)} {/* Power counter */}
× 10 {currentPower}
)} {/* Controls */} {/* Result Card */} {!display || !done ? (

Result will appear here when steps are complete

) : mode === "toSF" ? ( <>

Standard Form

{buildCoeff(display.digits.slice(fz === -1 ? 0 : fz))} × 10 {display.initPos - display.targetPos}

) : ( <>

Ordinary Form

{buildOrdinary(display.digits, display.targetPos)}

)}
); }