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.
Training Widget
AmeriCOM Branded
Controls
{topMedium === “custom” && (
setTopCustomN(v[0])} />
)}
{bottomMedium === “custom” && (
setBottomCustomN(v[0])} />
)}
setIncidentAngle(v[0])} />
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
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
Snell’s law
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
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.
)}
);
}