// FloatingDock — a draggable "park-anywhere" glass FAB that blooms open into a
// creative arc of shortcuts: Pricing · EMI · Annotate. Lives globally (mounted
// in App, above every screen), remembers its position across screens + reloads,
// and fans its actions toward open space whichever corner you leave it in.
function ensureDockKeys() {
if (typeof document === 'undefined' || document.getElementById('uni-dock-keys')) return;
const s = document.createElement('style'); s.id = 'uni-dock-keys';
s.textContent = `
@keyframes dockPulse { 0%,100%{ box-shadow:0 10px 30px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.28), 0 0 0 0 rgba(201,160,94,0.45);}
50%{ box-shadow:0 10px 30px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.28), 0 0 0 14px rgba(201,160,94,0);} }
`;
document.head.appendChild(s);
}
// liquid-glass disc used by the FAB + shortcuts
const dockGlass = {
background:'linear-gradient(155deg, rgba(34,28,20,0.78) 0%, rgba(20,16,11,0.74) 100%)',
border:'1px solid rgba(255,248,230,0.34)',
backdropFilter:'blur(18px) saturate(1.5)', WebkitBackdropFilter:'blur(18px) saturate(1.5)',
boxShadow:'0 12px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.26), inset 0 -1px 0 rgba(0,0,0,0.3)',
};
const DockGlyph = {
plus: (p) => ,
emi: (p) => ,
pen: (p) => ,
};
// live canvas height (adaptive: 1600 on 16:10 → up to 1920 on iPad 4:3). The FAB
// anchors to the REAL bottom so it never floats mid-screen on the taller canvas.
const dockLiveH = () => (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600;
const dockHasCustomPos = () => { try { const s = JSON.parse(localStorage.getItem('uni-fab-pos')); return !!(s && typeof s.x === 'number'); } catch (e) { return false; } };
function FloatingDock() {
ensureDockKeys();
const W = 2560, FAB = 86, MARGIN = 24;
const [H, setH] = React.useState(dockLiveH);
const [open, setOpen] = React.useState(false);
const [annot, setAnnot] = React.useState(false);
const [pos, setPos] = React.useState(() => {
try { const s = JSON.parse(localStorage.getItem('uni-fab-pos')); if (s && typeof s.x === 'number') return s; } catch (e) {}
return { x: W - 110, y: dockLiveH() - 120 };
});
const posRef = React.useRef(pos); posRef.current = pos;
// keep the FAB pinned to the live bottom-right as the canvas height adapts
// (orientation / device change). A user-dragged custom position is preserved.
React.useEffect(() => {
const onResize = () => {
const nh = dockLiveH(); setH(nh);
if (!dockHasCustomPos()) setPos({ x: W - 110, y: nh - 120 });
};
onResize();
window.addEventListener('resize', onResize);
window.addEventListener('orientationchange', onResize);
if (window.visualViewport) window.visualViewport.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
window.removeEventListener('orientationchange', onResize);
if (window.visualViewport) window.visualViewport.removeEventListener('resize', onResize);
};
}, []);
const drag = React.useRef({ active:false, moved:false, sx:0, sy:0, ox:0, oy:0, scale:1 });
const bezelScale = () => {
const b = document.querySelector('.tablet-bezel');
if (!b) return 1; const r = b.getBoundingClientRect();
return (r.width / b.offsetWidth) || 1;
};
const onMove = (e) => {
const d = drag.current; if (!d.active) return;
const dx = (e.clientX - d.sx) / d.scale, dy = (e.clientY - d.sy) / d.scale;
if (Math.hypot(dx, dy) > 6) d.moved = true;
const nx = clamp(d.ox + dx, FAB/2 + MARGIN, W - FAB/2 - MARGIN);
const ny = clamp(d.oy + dy, FAB/2 + MARGIN, H - FAB/2 - MARGIN);
setPos({ x: nx, y: ny });
};
const onUp = () => {
const d = drag.current;
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
if (!d.moved) { setOpen(o => !o); }
else { try { localStorage.setItem('uni-fab-pos', JSON.stringify(posRef.current)); } catch (e) {} }
d.active = false;
};
const onDown = (e) => {
e.stopPropagation(); e.preventDefault();
drag.current = { active:true, moved:false, sx:e.clientX, sy:e.clientY, ox:pos.x, oy:pos.y, scale:bezelScale() };
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
};
const items = [
{ key:'price', label:'Pricing', glyph:'₹', onClick:() => { setOpen(false); window.dispatchEvent(new CustomEvent('uni-open-tool', { detail:{ tool:'price' } })); } },
{ key:'emi', label:'EMI', icon:DockGlyph.emi, onClick:() => { setOpen(false); window.dispatchEvent(new CustomEvent('uni-open-tool', { detail:{ tool:'emi' } })); } },
{ key:'pen', label:'Annotate', icon:DockGlyph.pen, onClick:() => { setOpen(false); setAnnot(a => !a); } },
];
// fan toward the open quadrant (away from the nearest corner)
const hx = pos.x > W/2 ? -1 : 1;
const vy = pos.y > H/2 ? -1 : 1; // screen y-down; -1 = upward
const base = Math.atan2(-vy, hx); // math angle (y-up) into open space
const SPREAD = 42 * Math.PI / 180;
return (