luminomorphism
Version:
A UI design system built around light, blur, ambient motion and perceptual feedback.
25 lines (24 loc) • 3.01 kB
JavaScript
class LFocusRingMagnet extends HTMLElement{static get observedAttributes(){return["radius","color","magnet-range","pulse-on-focus"]}constructor(){super(),this.attachShadow({mode:"open"}),this.radius=parseInt(this.getAttribute("radius"))||30,this.color=this.getAttribute("color")||"#00ffff",this.range=parseInt(this.getAttribute("magnet-range"))||80,this.pulseOnFocus=this.hasAttribute("pulse-on-focus"),this.currentTarget=null,this.ring=null}connectedCallback(){this.render(),document.addEventListener("mousemove",this.track.bind(this)),document.addEventListener("focusin",this.focus.bind(this)),document.addEventListener("focusout",this.unfocus.bind(this))}disconnectedCallback(){document.removeEventListener("mousemove",this.track),document.removeEventListener("focusin",this.focus),document.removeEventListener("focusout",this.unfocus)}render(){const t=document.createElement("style");t.textContent=`
.ring {
position: fixed;
pointer-events: none;
border-radius: 50%;
width: ${this.radius*2}px;
height: ${this.radius*2}px;
margin-left: -${this.radius}px;
margin-top: -${this.radius}px;
background: radial-gradient(circle, ${this.color}55, transparent);
box-shadow: 0 0 12px ${this.color};
opacity: 0;
transform: scale(1);
transition: opacity 0.2s ease, transform 0.4s ease;
z-index: 9999;
}
.ring.pulse {
animation: pulse 1.5s ease-in-out infinite;
}
pulse {
0%, 100% { transform: scale(1); opacity: 0.6; }
50% { transform: scale(1.2); opacity: 1; }
}
`;const e=document.createElement("div");for(e.className="ring";this.shadowRoot.firstChild;)this.shadowRoot.removeChild(this.shadowRoot.firstChild);this.shadowRoot.appendChild(t),this.shadowRoot.appendChild(e),this.ring=e}track(t){const s=[...document.querySelectorAll("button, input, textarea, [tabindex]")].find(i=>{const n=i.getBoundingClientRect(),r=t.clientX-(n.left+n.width/2),o=t.clientY-(n.top+n.height/2);return Math.sqrt(r*r+o*o)<this.range});if(s&&s!==this.currentTarget){const i=s.getBoundingClientRect();this.ring.style.left=`${i.left+i.width/2}px`,this.ring.style.top=`${i.top+i.height/2}px`,this.ring.style.opacity="1",this.ring.classList.remove("pulse"),this.currentTarget=s}s||(this.ring.style.opacity="0",this.currentTarget=null)}focus(t){if(!this.pulseOnFocus)return;const e=t.target.getBoundingClientRect();this.ring.style.left=`${e.left+e.width/2}px`,this.ring.style.top=`${e.top+e.height/2}px`,this.ring.style.opacity="1",this.ring.classList.add("pulse")}unfocus(){this.ring.classList.remove("pulse"),this.ring.style.opacity="0"}attributeChangedCallback(t,e,s){e!==s&&(t==="radius"&&(this.radius=parseInt(s)||30),t==="color"&&(this.color=s||"#00ffff"),t==="magnet-range"&&(this.range=parseInt(s)||80),t==="pulse-on-focus"&&(this.pulseOnFocus=this.hasAttribute("pulse-on-focus")),this.render())}}customElements.define("l-focus-ring-magnet",LFocusRingMagnet);