const BAG_PATH = 'M18 10 H102 Q108 10 108 16 V138 Q108 150 98 156 L84 170 H36 L22 156 Q12 150 12 138 V16 Q12 10 18 10 Z';
const rr1 = (n,p=1)=>{ if(!isFinite(n)) return '—'; const f=Math.pow(10,p); const v=Math.round(n*f)/f; return v%1===0?String(v):v.toFixed(p); };
const clampN = (v,a,b)=>Math.max(a,Math.min(b,v));
const hhmm2 = (d)=>String(d.getHours()).padStart(2,'0')+String(d.getMinutes()).padStart(2,'0');
function DripChamber({ gtt, color, blood }) {
const ok = isFinite(gtt) && gtt > 0, sec = ok ? 60/gtt : 0, dur = ok ? clampN(sec,.28,6) : 0, stream = ok && sec < .28;
return <div style={{display:'flex',flexDirection:'column',alignItems:'center',marginTop:6}}>
<div style={{width:3,height:14,background:color,opacity:.6}}></div>
<div style={{position:'relative',width:34,height:70,border:`1.5px solid ${blood?'rgba(229,60,80,.6)':'var(--fg-3)'}`,borderRadius:'5px 5px 12px 12px',background:'rgba(255,255,255,.04)',overflow:'hidden'}}>
<span style={{position:'absolute',left:'50%',top:0,width:5,height:9,background:'var(--fg-3)',transform:'translateX(-50%)',borderRadius:'0 0 3px 3px'}}></span>
{ok && !stream && <span style={{position:'absolute',left:'50%',top:8,width:8,height:10,background:color,borderRadius:'50% 50% 50% 50% / 60% 60% 40% 40%',boxShadow:`0 0 6px ${color}`,animation:`dropfall2 ${dur}s linear infinite`}}></span>}
{stream && <span style={{position:'absolute',left:'50%',top:7,width:3,height:48,background:color,transform:'translateX(-50%)',boxShadow:`0 0 6px ${color}`,opacity:.9}}></span>}
<span style={{position:'absolute',left:0,right:0,bottom:0,height:14,background:color,opacity:.5}}></span>
</div>
<div style={{width:3,height:10,background:color,opacity:.6}}></div>
<span style={{fontFamily:'var(--mono)',fontSize:8.5,color:blood?'var(--crit)':'var(--fg-2)',letterSpacing:'.1em',marginTop:2}}>{blood?'BLOOD':ok?(stream?'STREAM':`1 gtt / ${sec.toFixed(1)} s`):'—'}</span>
</div>;
}
function IVBag({ title, sub, vol, mlHr, drops, hung, onHang, color, blood, lines = [], now, footer }) {
const gtt = mlHr * drops / 60;
const bagMin = mlHr > 0 ? vol / mlHr * 60 : Infinity;
const elapsed = hung ? ((now||Date.now()) - hung) / 60000 : 0;
const frac = hung && isFinite(bagMin) ? clampN(1 - elapsed / bagMin, 0, 1) : 1;
const levelY = 30 + (1 - frac) * 126;
const flowDur = (isFinite(gtt) && gtt > 0 ? clampN(60/gtt, .3, 4) : 1) + 's';
const emptyAt = hung && isFinite(bagMin) ? hhmm2(new Date(hung + bagMin*60000)) : null;
const id = 'bagclip-' + (title||'b').replace(/\W/g,'');
return <div className="ivbag" style={{'--bag':color}}>
<div style={{display:'flex',flexDirection:'column',alignItems:'center'}}>
<div style={{position:'relative',width:120,height:200}}>
<svg viewBox="0 0 120 200" style={{width:'100%',height:'100%',display:'block',overflow:'visible'}}>
<defs><clipPath id={id}><path d={BAG_PATH}/></clipPath></defs>
<rect x="52" y="0" width="16" height="10" rx="2" fill="var(--line-2)"/>
<path d={BAG_PATH} fill="rgba(255,255,255,.05)" stroke="var(--fg-3)" strokeWidth="1.5"/>
<g clipPath={`url(#${id})`}>
<rect x="0" y={levelY} width="120" height="200" fill={color} opacity=".42" style={{transition:'y .6s'}}/>
<rect x="0" y={levelY} width="120" height="3" fill={color} opacity=".9" style={{transition:'y .6s'}}/>
</g>
<path d={BAG_PATH} fill="none" stroke="var(--fg-1)" strokeWidth="1.2" opacity=".7"/>
<g stroke="var(--fg-1)" strokeWidth="1.2" opacity=".8">{[30,42,54,66,78,90,102,114,126].map((y,i)=><line key={y} x1={i%2?96:92} y1={y} x2="102" y2={y}/>)}</g>
<rect x="42" y="170" width="9" height="18" rx="2" fill="var(--line-2)" stroke="var(--fg-3)"/>
<rect x="68" y="170" width="9" height="18" rx="2" fill="var(--op)" opacity=".7" stroke="var(--fg-3)"/>
<line x1="72.5" y1="188" x2="72.5" y2="200" stroke={color} strokeWidth="3" strokeDasharray="4 4" style={{animation:`flow ${flowDur} linear infinite`}}/>
</svg>
<div style={{position:'absolute',left:20,top:24,right:34,fontFamily:'var(--mono)',color:'var(--fg-0)',lineHeight:1.25,pointerEvents:'none'}}>
<div style={{fontSize:10,fontWeight:700,letterSpacing:'.02em',textWrap:'pretty'}}>{title}</div>
<div style={{fontSize:8.5,color:'var(--fg-1)',marginTop:2}}>{sub}</div>
<div style={{fontSize:15,fontWeight:600,marginTop:8}}>{vol}<span style={{fontSize:8,color:'var(--fg-1)',marginLeft:2}}>mL</span></div>
<div style={{fontSize:7.5,color:'var(--fg-2)',letterSpacing:'.1em',marginTop:6}}>LOT {String(vol).padStart(4,'0')}-{drops}</div>
</div>
<div style={{position:'absolute',left:0,right:0,bottom:-2,textAlign:'center',fontFamily:'var(--mono)',fontSize:8,letterSpacing:'.12em',color}}>{hung?`${Math.round(frac*100)}% · ${Math.round(frac*vol)} mL left`:'NOT HUNG'}</div>
</div>
<DripChamber gtt={gtt} color={color} blood={blood}/>
</div>
<div style={{display:'flex',flexDirection:'column',gap:6,minWidth:0,fontFamily:'var(--mono)'}}>
<div style={{display:'flex',gap:16,flexWrap:'wrap'}}><div><span style={{fontSize:26,color}}>{rr1(mlHr,mlHr<10?1:0)}</span><span style={{fontSize:10,color:'var(--fg-2)',marginLeft:5}}>mL/hr</span></div><div><span style={{fontSize:26,color:'var(--fg-0)'}}>{rr1(gtt,0)}</span><span style={{fontSize:10,color:'var(--fg-2)',marginLeft:5}}>gtt/min</span></div></div>
{[['1 drop every',`${rr1(60/gtt,1)} s`],['Bag runs ~',isFinite(bagMin)?`${rr1(bagMin,0)} min`:'—'],['Bag empty at',emptyAt||'— hang to start'],...lines].map(([k,v],i)=><div key={i} style={{display:'flex',justifyContent:'space-between',gap:8,fontSize:11,borderTop:'1px dashed var(--line-2)',paddingTop:4}}><span style={{color:'var(--fg-2)'}}>{k}</span><b style={{fontWeight:500,textAlign:'right',color:'var(--fg-0)'}}>{v}</b></div>)}
<div style={{display:'flex',gap:6,marginTop:'auto',paddingTop:6}}><button className={'mini-btn'+(hung?' warn':'')} style={{flex:1,padding:'6px 0',...(hung?{}:{background:color,borderColor:color,color:'#0a0c0d'})}} onClick={onHang}>{hung?'⟲ RESET BAG':'▲ HANG BAG'}</button></div>
<div style={{fontSize:9,color:'var(--fg-3)',lineHeight:1.4}}>{footer || (hung?'Level drains against the computed rate. Tap RESET after a bag change.':'HANG starts the bag clock — the level drains at the computed rate.')}</div>
</div>
</div>;
}
const DRIP_PRESETS = {
'Ketamine':{amt:250,amtU:'mg',vol:250,dose:0.3,dU:'mg/kg/hr',note:'Analgesia 0.1–0.3 · dissociative 1–2 mg/kg/hr'},
'Fentanyl':{amt:1000,amtU:'mcg',vol:100,dose:1,dU:'mcg/kg/hr',note:'0.5–2 mcg/kg/hr'},
'Norepinephrine':{amt:4,amtU:'mg',vol:250,dose:0.1,dU:'mcg/kg/min',note:'Start 0.05–0.3 mcg/kg/min, titrate to MAP ≥65'},
'Epinephrine':{amt:1,amtU:'mg',vol:250,dose:0.1,dU:'mcg/kg/min',note:'0.01–0.5 mcg/kg/min'},
'TXA (infusion)':{amt:1000,amtU:'mg',vol:100,dose:125,dU:'mg/hr',note:'2nd gram over 8 h after 1 g bolus (or 2 g push)'},
'Custom':{amt:4,amtU:'mg',vol:250,dose:0.1,dU:'mcg/kg/min',note:''},
};
const DRIP_DEFAULT = { mode:'infusion', drug:'Ketamine', ...DRIP_PRESETS['Ketamine'], drops:60, fVol:500, fTime:20, fTimeU:'min', fDrops:10, fluid:'Crystalloid (LR/NS)', gauge:18, pressure:false, warmer:'none', hung:null, teach:true };
const GAUGE_FLOW = {14:270,16:200,18:100,20:60}, VISC = {'Whole Blood':.75,'LTOWB':.78,'PRBC':.5,'Plasma':.7};
function DripTab({ mb, upd, now }) {
const d = { ...DRIP_DEFAULT, ...(mb.drip||{}) };
const set = (patch) => upd('drip', { ...d, ...patch });
const kg = +mb.weight || 0;
const isInf = d.mode==='infusion', isBlood = d.mode==='fluid' && d.fluid!=='Crystalloid (LR/NS)';
const conc = (d.amtU==='mg' ? d.amt*1000 : d.amt) / (d.vol||1); // mcg/mL
const perHr = { 'mcg/kg/min':d.dose*kg*60, 'mcg/kg/hr':d.dose*kg, 'mg/kg/hr':d.dose*kg*1000, 'mcg/min':d.dose*60, 'mg/hr':d.dose*1000 }[d.dU] || 0;
const mlHrInf = perHr/conc;
const tMin = d.fTimeU==='hr' ? d.fTime*60 : d.fTime, mlHrFlu = d.fVol / ((tMin||1)/60);
let bf = (GAUGE_FLOW[d.gauge]||60) * (VISC[d.fluid]||.6); if (d.pressure) bf*=2.5; if (d.warmer==='inline') bf*=.9; if (d.warmer==='rapid') bf=Math.max(bf, d.gauge<=16?150:80);
const mlHr = isInf ? mlHrInf : mlHrFlu, drops = isInf ? d.drops : d.fDrops, vol = isInf ? d.vol : d.fVol;
const color = isBlood ? 'var(--crit)' : isInf ? 'var(--op)' : 'var(--info)';
const Seg = ({opts, val, pick, col}) => <div className="seg-row">{opts.map(([k,l])=><button key={k} className={val===k?'active':''} style={val===k&&col?{background:col,borderColor:col}:{}} onClick={()=>pick(k)}>{l}</button>)}</div>;
const F = ({label, children}) => <label className="calc-field"><span>{label}</span>{children}</label>;
const teach = isInf ? [
`Bag concentration = ${d.amt} ${d.amtU} ÷ ${d.vol} mL = ${rr1(conc,conc<10?2:0)} mcg/mL`,
`Dose/hr = ${d.dose} ${d.dU}${d.dU.includes('/kg')?` × ${kg} kg`:''}${d.dU.includes('/min')?' × 60':''} = ${rr1(perHr,0)} mcg/hr`,
`mL/hr = ${rr1(perHr,0)} ÷ ${rr1(conc,2)} = ${rr1(mlHrInf,1)} mL/hr · gtt/min = ${rr1(mlHrInf,1)} × ${d.drops} ÷ 60 = ${rr1(mlHrInf*d.drops/60,1)}`,
] : [
`Time = ${d.fTime} ${d.fTimeU}${d.fTimeU==='hr'?` × 60 = ${tMin} min`:''}`,
`gtt/min = ${d.fVol} × ${d.fDrops} ÷ ${tMin} = ${rr1(d.fVol*d.fDrops/(tMin||1),1)} · mL/hr = ${d.fVol} ÷ ${rr1(tMin/60,2)} h = ${rr1(mlHrFlu,0)}`,
...(isBlood ? [`Gravity flow ≈ ${GAUGE_FLOW[d.gauge]} mL/min (${d.gauge}g) × viscosity ${VISC[d.fluid]}${d.pressure?' × 2.5 pressure bag':''} ≈ ${rr1(bf,0)} mL/min`] : []),
];
return <div className="scrollable drip-tab">
<div className="calc" style={{gap:10}}>
<Seg opts={[['infusion','Infusion'],['fluid','Fluid / Blood']]} val={d.mode} pick={v=>set({mode:v})}/>
{isInf ? <>
<F label="Drug"><select value={d.drug} onChange={e=>set({drug:e.target.value, ...DRIP_PRESETS[e.target.value]})}>{Object.keys(DRIP_PRESETS).map(k=><option key={k}>{k}</option>)}</select></F>
<div className="calc-row"><F label="Amount in bag"><div style={{display:'flex',gap:4}}><input type="number" value={d.amt} onChange={e=>set({amt:+e.target.value||0})}/><select value={d.amtU} onChange={e=>set({amtU:e.target.value})} style={{width:70}}><option>mg</option><option>mcg</option></select></div></F><F label="Bag volume (mL)"><select value={d.vol} onChange={e=>set({vol:+e.target.value})}>{[50,100,250,500,1000].map(v=><option key={v} value={v}>{v}</option>)}</select></F></div>
<div className="calc-row"><F label={`Dose · ${kg} kg`}><div style={{display:'flex',gap:4}}><input type="number" step="0.01" value={d.dose} onChange={e=>set({dose:+e.target.value||0})}/><select value={d.dU} onChange={e=>set({dU:e.target.value})} style={{flex:1.3}}>{['mcg/kg/min','mcg/kg/hr','mg/kg/hr','mcg/min','mg/hr'].map(u=><option key={u}>{u}</option>)}</select></div></F><F label="Drop set · gtt/mL"><Seg opts={[[10,'10'],[15,'15'],[20,'20'],[60,'60 µ']]} val={d.drops} pick={v=>set({drops:v})}/></F></div>
{DRIP_PRESETS[d.drug]?.note && <div className="burn-note" style={{color:'var(--fg-2)'}}>{DRIP_PRESETS[d.drug].note}</div>}
</> : <>
<F label="Fluid"><select value={d.fluid} onChange={e=>set({fluid:e.target.value})}>{['Crystalloid (LR/NS)','Whole Blood','LTOWB','PRBC','Plasma'].map(k=><option key={k}>{k}</option>)}</select></F>
<div className="calc-row"><F label="Volume (mL)"><input type="number" value={d.fVol} onChange={e=>set({fVol:+e.target.value||0})}/></F><F label="Over"><div style={{display:'flex',gap:4}}><input type="number" value={d.fTime} onChange={e=>set({fTime:+e.target.value||0})}/><select value={d.fTimeU} onChange={e=>set({fTimeU:e.target.value})} style={{width:70}}><option value="min">min</option><option value="hr">hr</option></select></div></F></div>
<F label="Drop set · gtt/mL"><Seg opts={[[10,'10'],[15,'15'],[20,'20'],[60,'60 µ']]} val={d.fDrops} pick={v=>set({fDrops:v})}/></F>
{isBlood && <div className="blood-box">
<div className="bb-h">● Blood delivery · rapid / bolus</div>
<F label="IV access"><Seg opts={[[14,'14g'],[16,'16g'],[18,'18g'],[20,'20g']]} val={d.gauge} pick={v=>set({gauge:v})} col="var(--crit)"/></F>
<div className="calc-row"><F label="Delivery"><Seg opts={[[false,'Gravity'],[true,'Pressure bag']]} val={d.pressure} pick={v=>set({pressure:v})} col="var(--crit)"/></F><F label="Warmer"><select value={d.warmer} onChange={e=>set({warmer:e.target.value})}><option value="none">None</option><option value="inline">Inline warmer</option><option value="rapid">Rapid infuser-warmer</option></select></F></div>
<div style={{display:'flex',gap:20,fontFamily:'var(--mono)'}}><div><span style={{fontSize:24,color:'var(--crit)'}}>{rr1(bf,0)}</span><span style={{fontSize:10,color:'var(--fg-2)',marginLeft:5}}>mL/min max</span></div><div><span style={{fontSize:24,color:'var(--crit)'}}>{rr1(bf/450*60,1)}</span><span style={{fontSize:10,color:'var(--fg-2)',marginLeft:5}}>units/hr</span></div></div>
<div className="kv2"><span>{d.fVol} mL wide open in</span><b>~{rr1(d.fVol/bf,1)} min</b></div><div className="kv2"><span>1 unit (~450 mL) in</span><b>~{rr1(450/bf,1)} min</b></div>
<div className="burn-note" style={{color:'var(--fg-3)'}}>Estimates for teaching — real flow varies with hematocrit, line length and venous pressure. Warm every unit · Ca²⁺ 1 g after the first.</div>
</div>}
</>}
<IVBag title={isInf ? d.drug : d.fluid==='Crystalloid (LR/NS)' ? "Lactated Ringer's Injection USP" : d.fluid} sub={isInf ? `${d.amt} ${d.amtU} in ${d.vol} mL · ${rr1(conc,conc<10?2:0)} mcg/mL` : isBlood ? 'blood product · warm it' : '0.9% / LR crystalloid'} vol={vol} mlHr={mlHr} drops={drops} hung={d.hung} onHang={()=>set({hung:d.hung?null:Date.now()})} color={color} blood={isBlood} now={now && now.getTime()} lines={isInf ? [['Delivering',`${rr1(perHr,0)} mcg/hr`]] : [['Total',`${d.fVol} mL over ${tMin} min`]]}/>
<div className="teach" onClick={()=>set({teach:!d.teach})}><div className="teach-h">▸ How this is calculated {d.teach?'':'(tap to show)'}</div>{d.teach && teach.map((l,i)=><div key={i}>{l}</div>)}</div>
<div style={{fontSize:9.5,color:'var(--fg-3)',letterSpacing:'.05em',fontFamily:'var(--mono)'}}>gtt/min = mL/hr × drop set ÷ 60 · weight {kg} kg from roster</div>
</div>
</div>;
}
Object.assign(window, { IVBag, DripChamber, DripTab });