// Pull Lamp / Ship Notes. Standalone SVG Web Component, no libraries.
(() => {
if(customElements.get('pull-lamp'))return;
class PullLamp extends HTMLElement {
static get observedAttributes(){return ['on'];}
connectedCallback(){
if(this.shadowRoot){this.media.addEventListener('change',this.motionChange);this.updateLabel();this.animate();return;}
const r=this.attachShadow({mode:'open'});
r.innerHTML=`
Pull the cord down, or press Enter or Space. `;
this.button=r.querySelector('button');this.pull=0;this.sway=0;this.velocity=0;this.sideVelocity=0;this.light=this.hasAttribute('on')?1:0;this.last=0;
this.media=matchMedia('(prefers-reduced-motion: reduce)');
this.motionChange=()=>{if(this.media.matches){cancelAnimationFrame(this.frame);this.frame=0;this.pull=this.sway=this.velocity=this.sideVelocity=0;this.light=this.hasAttribute('on')?1:0;this.pose(0,0,this.light);}};
this.media.addEventListener('change',this.motionChange);
this.button.addEventListener('pointerdown',e=>{if(e.button!==0)return;this.drag={id:e.pointerId,x:e.clientX,y:e.clientY,scale:480/this.getBoundingClientRect().width};this.button.setPointerCapture(e.pointerId);this.moved=0;});
this.button.addEventListener('pointermove',e=>{if(!this.drag||e.pointerId!==this.drag.id)return;const dx=(e.clientX-this.drag.x)*this.drag.scale,dy=(e.clientY-this.drag.y)*this.drag.scale;this.moved=Math.max(this.moved,Math.hypot(dx,dy));this.pull=Math.max(0,Math.min(65,dy));this.sway=Math.max(-24,Math.min(24,dx));this.pose(this.pull,this.sway,this.light);});
this.button.addEventListener('pointerup',e=>this.release(e,false));
this.button.addEventListener('pointercancel',e=>this.release(e,true));
this.button.addEventListener('lostpointercapture',e=>{if(this.drag)this.release(e,true);});
this.button.addEventListener('click',e=>{if(e.detail===0)this.toggle();});
this.updateLabel();this.pose(0,0,this.light);
}
disconnectedCallback(){cancelAnimationFrame(this.frame);this.frame=0;this.media?.removeEventListener('change',this.motionChange);this.drag=null;}
attributeChangedCallback(){if(!this.button)return;this.updateLabel();if(!this.hasAttribute('recording'))this.animate();}
updateLabel(){const on=this.hasAttribute('on');this.button.setAttribute('aria-pressed',String(on));this.button.setAttribute('aria-label',on?'Switch to dark theme':'Switch to light theme');}
release(e,cancelled){if(!this.drag||e.pointerId!==this.drag.id)return;const toggle=!cancelled&&(this.pull>16||this.moved<5);this.drag=null;if(this.button.hasPointerCapture(e.pointerId))this.button.releasePointerCapture(e.pointerId);if(toggle)this.toggle();this.animate();}
toggle(){this.toggleAttribute('on');this.dispatchEvent(new CustomEvent('themechange',{detail:{theme:this.hasAttribute('on')?'light':'dark'},bubbles:true,composed:true}));}
animate(){if(this.media.matches){this.pull=this.sway=0;this.light=this.hasAttribute('on')?1:0;this.pose(0,0,this.light);return;}if(!this.frame){this.last=0;this.frame=requestAnimationFrame(this.tick);}}
tick=(now)=>{const dt=this.last?Math.min((now-this.last)/1000,.025):.016;this.last=now;
if(!this.drag){this.velocity+=(-this.pull*220-this.velocity*19)*dt;this.pull+=this.velocity*dt;this.sideVelocity+=(-this.sway*100-this.sideVelocity*9)*dt;this.sway+=this.sideVelocity*dt;}
const target=this.hasAttribute('on')?1:0;this.light+=(target-this.light)*(1-Math.exp(-dt*9));this.pose(this.pull,this.sway,this.light);
if(Math.abs(this.pull)+Math.abs(this.velocity)+Math.abs(this.sway)+Math.abs(this.sideVelocity)+Math.abs(target-this.light)>.015||this.drag){this.frame=requestAnimationFrame(this.tick);}else{this.frame=0;this.pose(0,0,target);}
};
pose(pull=0,sway=0,light=0){const r=this.shadowRoot;if(!r)return;this.style.setProperty('--light',String(Math.max(0,Math.min(1,light))));const x=350+sway,y=329+pull,d='M350 226 Q'+(350+sway*.22)+' '+(277+pull*.45)+' '+x+' '+y;r.getElementById('cord').setAttribute('d',d);r.getElementById('beads').setAttribute('d',d);r.getElementById('knob').setAttribute('transform',`translate(${sway} ${pull})`);this.button.style.left=(x/480*100)+'%';this.button.style.top=((y+3)/560*100)+'%';}
}
customElements.define('pull-lamp',PullLamp);
})();