Confirm Dial Align
ns-ui/confirm-dial-alignFreeComponent
Destructive-action confirm gated on precision, not patience: rotate an SVG dial's notch to within 3deg of a fixed index, hold it there 400ms, and the dial snaps home and arms the confirm button — overshoot and it springs back like a slipping safe tumbler.
Live preview
live · rendered by the registry
Rendered by ns-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://design.helpmarq.com/r/confirm-dial-align.jsonSource
1"use client";23import { useEffect, useRef, useState, type ReactNode } from "react";45// TumblerGate — a destructive-action confirm gated on ACCURACY, the third6// axis besides confirm-hold-ink's time and confirm-slide-shatter's distance. An7// SVG dial carries one rotating notch; it must be turned (drag, wheel, or8// arrow keys) until the notch sits within 3deg of the fixed index mark and9// HELD there for a 400ms dwell — leaving the band before the dwell completes10// is a failed catch: the dial springs out to +8deg like a safe tumbler that11// almost caught and slipped. Completing the dwell spring-snaps to exact12// zero, both ticks thicken to --foreground, and the destructive button arms13// (its one legitimate use of --accent). Because fine-motor precision is14// hostile to some users, an always-visible "type to confirm" disclosure15// offers an equivalent path, and two failed catches auto-expand it. Direct-16// DOM writes on the drag/spring hot path, React state only at the rare17// armed/failed/fallback transitions. Zero deps.1819const TOL = 3; // deg — dwell capture band around the index mark20const DWELL_MS = 400;21const STEP = 2; // deg per wheel tick / arrow key22const FRICTION = 0.6; // drag angular delta damping23const SLIP_TARGET = 8; // deg — where a failed catch springs out to24const SNAP_K = 260;25const SNAP_ZETA = 0.85; // one tiny overshoot settling to exact zero26const SLIP_K = 380;27const SLIP_ZETA = 0.55; // stiffer, snappier "give"28const SETTLE_EPS = 0.05; // deg29const V_EPS = 0.3; // deg/s30const FORCE_SETTLE_MS = 700;3132function wrapDeg(a: number) {33 let r = a % 360;34 if (r > 180) r -= 360;35 if (r <= -180) r += 360;36 return r;37}3839export function TumblerGate({40 destructiveLabel = "Delete organization",41 doneLabel = "Deleted",42 confirmWord = "delete",43 initialAngle = 150,44 onConfirm,45 className = "",46}: {47 destructiveLabel?: ReactNode;48 doneLabel?: ReactNode;49 /** word the type-to-confirm fallback checks against, case-insensitive */50 confirmWord?: string;51 /** starting dial offset in degrees from the index mark */52 initialAngle?: number;53 onConfirm?: () => void;54 className?: string;55}) {56 const knobRef = useRef<HTMLDivElement>(null);57 const inputRef = useRef<HTMLInputElement>(null);58 const notchGroupRef = useRef<SVGGElement>(null);59 const notchLineRef = useRef<SVGLineElement>(null);60 const indexLineRef = useRef<SVGLineElement>(null);6162 const [armed, setArmed] = useState(false);63// … truncated
Files it writes
More from ns-ui
All 228 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordion Latchaccordion-latch | ns-ui | Components | Free | no deps | |
| Approval Inline Diffapproval-inline-diff | ns-ui | Components | Free | no deps | |
| ASCII Engraving Contourascii-engraving-contour | ns-ui | Components | Free | no deps | |
| ASCII Globe Spinascii-globe-spin | ns-ui | Components | Free | no deps | |
| ASCII Torus Donutascii-torus-donut | ns-ui | Components | Free | no deps | |
| Autosave Ratchetautosave-ratchet | ns-ui | Components | Free | no deps | |
| Avatar Stack Flockavatar-stack-flock | ns-ui | Components | Free | no deps | |
| Background ASCII Ditherbackground-ascii-dither | ns-ui | Components | Free | no deps |
