import React, { useMemo, useState } from “react”;
import { Card, CardContent, CardHeader, CardTitle } from “@/components/ui/card”;
import { Slider } from “@/components/ui/slider”;
import { Switch } from “@/components/ui/switch”;
import { Label } from “@/components/ui/label”;
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from “@/components/ui/select”;
import { Badge } from “@/components/ui/badge”;
import { Button } from “@/components/ui/button”;
import { motion } from “framer-motion”;
import { Info, RotateCcw, Waves, Triangle, Lightbulb } from “lucide-react”;

const COLORS = {
navy: “#002a5c”,
cyan: “#00a3e0”,
gold: “#f0b323”,
red: “#a12b2f”,
light: “#eef6fb”,
border: “#cfe4f2”,
glass: “rgba(0, 163, 224, 0.10)”,
air: “rgba(240, 179, 35, 0.08)”,
};

const mediaPresets = {
air: { label: “Air”, n: 1.0 },
water: { label: “Water”, n: 1.333 },
acrylic: { label: “Acrylic”, n: 1.49 },
glass: { label: “Glass”, n: 1.52 },
sapphire: { label: “Sapphire”, n: 1.77 },
};

function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}

function degToRad(deg) {
return (deg * Math.PI) / 180;
}

function radToDeg(rad) {
return (rad * 180) / Math.PI;
}

function formatNumber(value, digits = 1) {
return Number.isFinite(value) ? value.toFixed(digits) : “—”;
}

function polarFromNormal(centerX, centerY, angleFromNormalDeg, length, side = “top”) {
const signY = side === “top” ? -1 : 1;
const x = centerX + length * Math.sin(degToRad(angleFromNormalDeg));
const y = centerY + signY * length * Math.cos(degToRad(angleFromNormalDeg));
return { x, y };
}

function Arc({ cx, cy, radius, startDeg, endDeg, side = “top”, stroke = COLORS.gold, strokeWidth = 3, dashed = false }) {
const step = endDeg >= startDeg ? 1 : -1;
const pts = [];
for (let a = startDeg; step > 0 ? a <= endDeg : a >= endDeg; a += step) {
pts.push(polarFromNormal(cx, cy, a, radius, side));
}
const path = pts.map((p, i) => `${i === 0 ? “M” : “L”} ${p.x} ${p.y}`).join(” “);
return ;
}

function ProtractorTicks({ cx, cy, radius, side = “top” }) {
const ticks = [];
for (let a = -90; a <= 90; a += 10) {
const absA = Math.abs(a);
const long = absA % 30 === 0;
const inner = polarFromNormal(cx, cy, a, radius – (long ? 14 : 8), side);
const outer = polarFromNormal(cx, cy, a, radius, side);
ticks.push(

{long && (

{absA}

)}

);
}
return {ticks};
}

export default function AmericomBendingLightWidget() {
const [topMedium, setTopMedium] = useState(“air”);
const [bottomMedium, setBottomMedium] = useState(“glass”);
const [topCustomN, setTopCustomN] = useState(1.0);
const [bottomCustomN, setBottomCustomN] = useState(1.52);
const [incidentAngle, setIncidentAngle] = useState(35);
const [wavelengthNm, setWavelengthNm] = useState(532);
const [showNormal, setShowNormal] = useState(true);
const [showProtractor, setShowProtractor] = useState(true);
const [showEquation, setShowEquation] = useState(true);
const [showGuide, setShowGuide] = useState(true);

const n1 = topMedium === “custom” ? topCustomN : mediaPresets[topMedium].n;
const n2 = bottomMedium === “custom” ? bottomCustomN : mediaPresets[bottomMedium].n;

const optics = useMemo(() => {
const theta1 = incidentAngle;
const sin2 = (n1 / n2) * Math.sin(degToRad(theta1));
const tir = Math.abs(sin2) > 1;
const theta2 = tir ? null : radToDeg(Math.asin(clamp(sin2, -1, 1)));
const reflectAngle = theta1;
const criticalAngle = n1 > n2 ? radToDeg(Math.asin(n2 / n1)) : null;
const v1 = 100 / n1;
const v2 = 100 / n2;
const lambda2 = wavelengthNm * (n1 / n2);
return { theta1, theta2, tir, reflectAngle, criticalAngle, v1, v2, lambda2 };
}, [incidentAngle, n1, n2, wavelengthNm]);

const cx = 430;
const cy = 220;
const rayLen = 230;
const start = polarFromNormal(cx, cy, -optics.theta1, rayLen, “top”);
const reflected = polarFromNormal(cx, cy, optics.reflectAngle, rayLen * 0.78, “top”);
const refracted = optics.tir ? null : polarFromNormal(cx, cy, optics.theta2, rayLen * 0.92, “bottom”);

const quizPrompt = optics.tir
? “You are above the critical angle. Why does no transmitted ray appear?”
: n2 > n1
? “The lower ray bends toward the normal. What changed inside the second medium?”
: “The lower ray bends away from the normal. What does that tell you about refractive index?”;

return (

AmeriCOM Light Bending Explorer

Interactive refraction, reflection, Snell’s law, wavelength shift, and total internal reflection.

Optics Fundamentals
Training Widget
AmeriCOM Branded

 

Controls






{topMedium === “custom” && (

n{topCustomN.toFixed(3)}

setTopCustomN(v[0])} />

)}






{bottomMedium === “custom” && (

n{bottomCustomN.toFixed(3)}

setBottomCustomN(v[0])} />

)}

{incidentAngle.toFixed(0)}°

setIncidentAngle(v[0])} />

{wavelengthNm.toFixed(0)} nm

setWavelengthNm(v[0])} />

{[
[showNormal, setShowNormal, “Show normal”],
[showProtractor, setShowProtractor, “Show protractor”],
[showEquation, setShowEquation, “Show equation panel”],
[showGuide, setShowGuide, “Show coaching prompts”],
].map(([val, setter, label]) => (

))}

 

Key readouts

θ₁ incident
{formatNumber(optics.theta1, 0)}°
θ₂ refracted
{optics.tir ? “TIR” : `${formatNumber(optics.theta2, 1)}°`}
Speed in top
{formatNumber(optics.v1, 1)}% c
Speed in bottom
{formatNumber(optics.v2, 1)}% c
Bottom wavelength
{formatNumber(optics.lambda2, 0)} nm
Critical angle
{optics.criticalAngle ? `${formatNumber(optics.criticalAngle, 1)}°` : “—”}

 

Drag the angle and medium indices to see how the ray bends. Angles are measured from the normal.

 

Top medium: n = {n1.toFixed(3)}
Bottom medium: n = {n2.toFixed(3)}
Interface
{showNormal && Normal}

{showProtractor && (

{!optics.tir && }
θ₁
{!optics.tir && θ₂}

)}

{refracted && }

Laser source

Reflected ray
{refracted ? (
Refracted ray
) : (

Total internal reflection
No real transmitted angle exists because n₁ > n₂ and θ₁ exceeds θc.

)}

What changes at the boundary?
Speed changes with refractive index.
Frequency stays constant.
Wavelength changes inside the medium.

AmeriCOM takeaway
Higher refractive index → slower light
Entering slower medium → bends toward normal
Leaving slower medium → bends away from normal

 

{showEquation && (

Snell’s law

n₁ sin(θ₁) = n₂ sin(θ₂)
n₁ = {n1.toFixed(3)}
n₂ = {n2.toFixed(3)}
sin(θ₁) = {Math.sin(degToRad(optics.theta1)).toFixed(4)}
sin(θ₂) = {optics.tir ? “No real value” : Math.sin(degToRad(optics.theta2)).toFixed(4)}

When light enters a material with a larger refractive index, it slows down and bends toward the normal. When it enters a smaller refractive index, it speeds up and bends away from the normal.

)}

{showGuide && (

Coaching prompt

{quizPrompt}

Try this: Set top to glass and bottom to air, then raise θ₁ until the transmitted ray disappears.

Then compare: Air → glass at the same angle. Why is the result different?

Notice: Frequency does not change across the boundary, but speed and wavelength do.

)}

);
}