KQ-2080 · TWO/KEYS
Two Keys
A two-person approval for an agent's action, drawn as two key switches beneath a summary of it: each approver turns their own key a quarter turn on springs.snap, the first key opens a window that drains as a hairline, and only when the second turns inside it do the two halves of the bar between the switches close on springs.glide, the lock swap open on springs.flick and the Run control unlock — while a lone key turns itself back on snap if the window lapses. Only the current user's key is a control: a real switch that Space or Enter turns, or a sideways drag that follows the pointer 1:1 and commits by projection; under 34rem the switches stack beside a shorter summary with the bar running down between them.
Gaugeworks billing · approvals · you are Mara Ilves
Gaugeworks billing agent · 4 min ago· expires in 3 h 0 min
Refund £4,820.00
to Basinworks Supply Co.
Over the £2,500 single-approver limitTwelve pressure gauges arrived cracked; the warehouse has them back.
Order GW-30418 · RMA 7731
Needs both keys within 60 s: finance lead and support manager.
waiting for both keys · 60 s window · gaugeworks
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/two-keys
Props
- switchStyle
- "key" | "lever" | "dial"
- default "key"
- A key in a round keyhole plate, a throw lever swinging across an arc, or a dial whose ring fills as it turns.
- windowLength
- number
- default 60
- Seconds the second key has after the first one turns.
- accent
- string
- default "var(--accent-bright)"
- Lit ticks, the key, the closed bar and Run — any CSS colour.
- action
- TwoKeysAction
- default defaultTwoKeysAction
- What the agent wants to do: title, amount, currency, payee, reference, reason, limitNote, requestedBy, requestedAt and expiresAt.
- approvers
- TwoKeysApprover[]
- default defaultTwoKeysApprovers
- The two people whose keys it needs, in switch order: id, name, role, turnedAt, rejected ({ at, note }) and comment.
- currentUser
- string
- default "mara"
- Whose key is a control here; every other key is a drawing with its state in text. Someone who is not an approver is offered Withdraw.
- turned / defaultTurned
- boolean
- default — / false
- Whether the current user's key is turned, controlled or initial.
- onTurnKey
- (turned: boolean, reason: "key" | "lapsed") => void
- default —
- Fires from the press, the drag, Space or Enter — or with "lapsed" when the window ran out on the user's own key.
- onReject
- (note: string) => void
- default —
- Reject, with the reason the approver wrote (a reason is required).
- onRemind
- (approverId: string) => void
- default —
- Remind, for an approver whose key is not turned.
- onWithdraw
- () => void
- default —
- Withdraw, offered to anyone viewing who is not an approver.
- onRun
- () => void
- default —
- The unlocked Run control was pressed.
- now
- number
- default defaultTwoKeysNow
- The moment times count from, in ms; while a window is open the card keeps time from it itself.
- status
- "open" | "expired" | "withdrawn" | "done"
- default "open"
- Whether the request can still be decided; done means it ran. It also reads as expired once now passes action.expiresAt.
- runLabel
- string
- default "Run refund"
- The Run control's words.
- format
- (amount: number, currency: string) => string
- default en-GB currency
- Formats the amount.
- label
- string
- default the action's title and amount
- The card's accessible name.
- disabled
- boolean
- default false
- Nothing can be turned or pressed.
- className
- string
- default —
- Classes for the outer surface.
| Prop | Type | Default | Description |
|---|---|---|---|
| switchStyle | "key" | "lever" | "dial" | "key" | A key in a round keyhole plate, a throw lever swinging across an arc, or a dial whose ring fills as it turns. |
| windowLength | number | 60 | Seconds the second key has after the first one turns. |
| accent | string | "var(--accent-bright)" | Lit ticks, the key, the closed bar and Run — any CSS colour. |
| action | TwoKeysAction | defaultTwoKeysAction | What the agent wants to do: title, amount, currency, payee, reference, reason, limitNote, requestedBy, requestedAt and expiresAt. |
| approvers | TwoKeysApprover[] | defaultTwoKeysApprovers | The two people whose keys it needs, in switch order: id, name, role, turnedAt, rejected ({ at, note }) and comment. |
| currentUser | string | "mara" | Whose key is a control here; every other key is a drawing with its state in text. Someone who is not an approver is offered Withdraw. |
| turned / defaultTurned | boolean | — / false | Whether the current user's key is turned, controlled or initial. |
| onTurnKey | (turned: boolean, reason: "key" | "lapsed") => void | — | Fires from the press, the drag, Space or Enter — or with "lapsed" when the window ran out on the user's own key. |
| onReject | (note: string) => void | — | Reject, with the reason the approver wrote (a reason is required). |
| onRemind | (approverId: string) => void | — | Remind, for an approver whose key is not turned. |
| onWithdraw | () => void | — | Withdraw, offered to anyone viewing who is not an approver. |
| onRun | () => void | — | The unlocked Run control was pressed. |
| now | number | defaultTwoKeysNow | The moment times count from, in ms; while a window is open the card keeps time from it itself. |
| status | "open" | "expired" | "withdrawn" | "done" | "open" | Whether the request can still be decided; done means it ran. It also reads as expired once now passes action.expiresAt. |
| runLabel | string | "Run refund" | The Run control's words. |
| format | (amount: number, currency: string) => string | en-GB currency | Formats the amount. |
| label | string | the action's title and amount | The card's accessible name. |
| disabled | boolean | false | Nothing can be turned or pressed. |
| className | string | — | Classes for the outer surface. |