← Blurr Motion cta-hold-to-confirm-brutalist
Categorie cta Tier 1 Techniek #18 Deps
[ 01 / CONFIRM ]

HOLD
TO
CONFIRM

NO ACCIDENTAL CLICKS — PRESS AND KEEP PRESSING.

// READY // HOLDING… // CONFIRMED ✓

1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: cta-hold-to-confirm-brutalist
// Mousedown/touchstart vult progress 0 -> 100% in 1.2s; volledig vol = success.
// Auto-demo simuleert om de 4s een hold om te tonen wat het block doet.
const root = document.querySelector('.hold-block');
if (root && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  const btn  = root.querySelector('.hold-btn');
  const fill = root.querySelector('.hold-fill');
  const status = root.querySelector('.hold-status');
  const DURATION = 1200;
  let raf, t0, holding = false, autoTimer, autoDemo = true;

  const setProgress = (p) => {
    fill.style.transform = 'scaleX(' + p + ')';
    status.dataset.state = p >= 1 ? 'confirmed' : (p > 0 ? 'holding' : 'idle');
  };
  const tick = (ts) => {
    if (!holding) return;
    if (!t0) t0 = ts;
    const p = Math.min(1, (ts - t0) / DURATION);
    setProgress(p);
    if (p >= 1) { holding = false; return; }
    raf = requestAnimationFrame(tick);
  };
  const start = () => { holding = true; t0 = 0; cancelAnimationFrame(raf); raf = requestAnimationFrame(tick); };
  const cancel = () => { if (holding) { holding = false; setProgress(0); } };
  const stopAuto = () => { autoDemo = false; clearTimeout(autoTimer); };

  btn.addEventListener('mousedown', () => { stopAuto(); start(); });
  btn.addEventListener('touchstart', (e) => { e.preventDefault(); stopAuto(); start(); }, { passive: false });
  ['mouseup','mouseleave','touchend','touchcancel'].forEach(ev => btn.addEventListener(ev, cancel));

  const autoLoop = () => {
    if (!autoDemo) return;
    start();
    autoTimer = setTimeout(() => {
      if (!autoDemo) return;
      setTimeout(() => { setProgress(0); autoTimer = setTimeout(autoLoop, 2800); }, 800);
    }, DURATION + 50);
  };
  setTimeout(autoLoop, 1500);
}
2. Skeleton — DOM + class-namen, mag herschikken
<!-- Skeleton: cta-hold-to-confirm-brutalist -->
<section class="hold-block">
  <div class="hold-grid">
    <span class="hold-tag">[ 01 / CONFIRM ]</span>
    <h2 class="hold-headline">HOLD<br/>TO<br/>CONFIRM</h2>
    <p class="hold-sub">No accidental clicks. Press and keep pressing.</p>
  </div>
  <button class="hold-btn" type="button">
    <span class="hold-label">HOLD ▸</span>
    <span class="hold-track">
      <span class="hold-fill"></span>
    </span>
  </button>
  <p class="hold-status" data-state="idle">
    <span class="s-idle">// READY</span>
    <span class="s-hold">// HOLDING…</span>
    <span class="s-ok">// CONFIRMED ✓</span>
  </p>
</section>
3. Styling-template — verplicht eigen invulling per merk
/* Styling: cta-hold-to-confirm-brutalist — 2px borders, JetBrains Mono UPPERCASE, hot-spot #FF4A1C */
.hold-block {
  --paper: #FFFFFF;
  --ink: #0A0A0A;
  --hot: #FF4A1C;
  background: var(--paper);
  color: var(--ink);
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  padding: clamp(2rem, 5vw, 4rem);
  display: grid;
  gap: 2rem;
  border: 2px solid var(--ink);
  box-shadow: 12px 12px 0 0 var(--ink);
}
.hold-headline {
  font-size: clamp(3rem, 9vw, 6rem);
  line-height: .88;
  letter-spacing: -.02em;
  font-weight: 700;
  margin: 0;
}
.hold-btn {
  display: grid;
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: 8px 8px 0 0 var(--ink);
  padding: 0;
  font-family: inherit;
  cursor: pointer;
}
.hold-btn:active { transform: translate(4px, 4px); box-shadow: 4px 4px 0 0 var(--ink); }
.hold-label { padding: 1.6rem 2rem; font-size: 1.6rem; font-weight: 700; text-align: left; }
.hold-track { height: 14px; border-top: 2px solid var(--ink); overflow: hidden; }
.hold-fill { display: block; height: 100%; background: var(--hot); transform: scaleX(0); transform-origin: left; }
[data-state="confirmed"] { color: var(--hot); }
@media (prefers-reduced-motion: reduce) {
  .hold-fill { transform: scaleX(1); }
}