{"$schema":"https://ui.shadcn.com/schema/registry.json","name":"bollard-ui","homepage":"https://bollard-ui.vercel.app","items":[{"name":"agent-action-approve","type":"registry:ui","title":"Agent Action Approve","description":"Reads an agent's tool call and says plainly what it will delete, send or change, how far it reaches and whether it can be undone. Marks risky tokens, compares the agent's claim with the call, makes dangerous calls hold-to-approve and refuses risky \"always allow\" rules.","categories":["Destructive actions"],"files":[{"path":"registry/default/agent-action-approve/agent-action-approve.tsx","type":"registry:ui"},{"path":"registry/default/agent-action-approve/analyze.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Unreviewed agent tool calls","mistakes":["ai agent deleted my files","agent ran rm -rf on wrong folder","approve tool call safely","always allow too broad","agent deleted production database"],"aliases":["tool approval","human in the loop","mcp","coding agent","claude code","cursor","tool call confirmation","rm -rf","permission prompt","ai sdk"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"A review screen for an AI agent's tool call that spells out what it will delete, send or change and how far it reaches, before someone approves it.","server":"Enforce the decision at the tool boundary: the agent runtime must refuse calls that were not approved, and check the caller's authorisation for the target.","catches":["Tokens that widen the reach: a lone \\ (drive root), an unset $VAR, ../, 8.3 short names","A stated intent that disagrees with what the call actually does","MCP tools labelled read-only whose call writes or deletes","Commands hidden inside bash -c, cmd /c or encoded PowerShell, and curl | bash","\"Always allow\" rules that would also let through other commands, and refuses them for npx, bash -c, chained or irreversible calls"],"limits":["A review UI only: whether a call is actually blocked depends on your host app honouring the decision","Static analysis of the call text; it cannot know what a script or binary does once run","Unknown tools and argument shapes are described less precisely than shell and SQL calls","\"User approved this\" does not mean the operation is safe"],"a11y":["Decision result receives focus (tabIndex=-1) and status text is announced in a role=\"status\" region","Hold-to-approve works with Space or Enter held down, not just the pointer","Refusals of an \"always\" rule use role=\"alert\"","Risk level is shown with text and icon, not colour alone; motion respects prefers-reduced-motion"]}},{"name":"mcp-tool-review","type":"registry:ui","title":"MCP Tool Review","description":"Review card for MCP tool changes after approval: fingerprints each definition, diffs the description word by word, marks injected instructions (<IMPORTANT> blocks, \"do not tell the user\", ~/.ssh, other servers' tools), new free-text parameters and flipped read-only hints.","categories":["Destructive actions"],"files":[{"path":"registry/default/mcp-tool-review/mcp-tool-review.tsx","type":"registry:ui"},{"path":"registry/default/mcp-tool-review/tool-diff.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Helps catch an MCP server swapping in poisoned tools","mistakes":["mcp tool poisoning","mcp rug pull","tool description changed after approval","malicious mcp server","prompt injection in tool description"],"aliases":["mcp","model context protocol","tool pinning","ai agent","claude desktop","cursor","tools/list","supply chain","diff","ai safety"],"flows":["devtools"],"fields":["code","text"],"summary":"When an MCP server changes a tool you approved, shows the word-by-word diff and highlights poisoned instructions before the agent uses it.","server":"Pin approved tool definitions in the MCP host and refuse calls to tools whose fingerprint changed until re-approved.","catches":["Tool definitions changed since approval (fingerprint mismatch)","Injected instructions such as <IMPORTANT> blocks or \"do not tell the user\"","References to secrets or config such as ~/.ssh and mcp.json, or to another server's tools","New free-text parameters that could carry data out","Invisible characters and read-only annotations that flipped"],"limits":["Phrase-based detection; a reworded injection can slip through","Only sees definitions the host passes in; it doesn't talk to MCP servers","Keep, disable or accept is passed to onResolve; enforcing it is the host's job"],"a11y":["Each tool card expands with aria-expanded and aria-controls","Focus moves to the open card's first action","Risk is shown in text with icons aria-hidden","Animations are skipped under reduced motion"]}},{"name":"reset-hard-readback","type":"registry:ui","title":"Reset Hard Readback","description":"Readback for destructive git commands from porcelain status: edited files with +/- lines, staged new files, untracked and ignored files (.env and keys first), commits the branch moves past with unpushed ones marked, stash ids to recover, what stays, and a stash-first option.","categories":["Destructive actions"],"files":[{"path":"registry/default/reset-hard-readback/git-loss.ts","type":"registry:ui"},{"path":"registry/default/reset-hard-readback/reset-hard-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Uncommitted work wiped by git reset","mistakes":["git reset hard lost my changes","git clean deleted untracked files","stash drop lost work","lost .env after git clean"],"aliases":["git reset --hard","git clean -fdx","git stash","discard changes","working tree","cli confirm","terminal","uncommitted changes"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Lists exactly what git reset --hard, clean, checkout -- . or stash drop would destroy, and offers to stash first.","server":"In tooling that runs these commands for users, keep a reflog/backup and require explicit confirmation server-side; this component only describes the loss.","catches":["Tracked edits that would be discarded, with +/- line counts","Staged new files and untracked/ignored files, secrets like .env and keys listed first","Commits the branch would move past, with unpushed ones marked","Stash entries dropped, with the sha to recover them","What the command leaves alone (e.g. submodules without --recurse-submodules)"],"limits":["Only as complete as the status, numstat, commits and stashes you pass","Parses common forms of reset, clean, checkout/restore and stash drop; unusual flags may not be modelled","Does not run git; onStash and onConfirm are yours"],"a11y":["Each loss group is a section labelled by its heading; the list is a labelled group","Long groups expand with a button instead of truncating silently","Outcome announced via role=status","Confirm receives focus when the readback opens; buttons set aria-busy while pending"]}},{"name":"account-deletion-readback","type":"registry:ui","title":"Account Deletion Readback","description":"Before Delete, lists what goes, sole-owner teams (with transfer), App Store / Google Play plans that keep billing until cancelled there, the restore deadline, what is kept and a data export. Delete unlocks after a typed confirmation and can wait for the paid period to end.","categories":["Destructive actions"],"files":[{"path":"registry/default/account-deletion-readback/account-deletion-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Deleted account still billing subscriptions","mistakes":["delete account keeps charging","account deleted lost team data","user deleted account by mistake","no data export before deletion"],"aliases":["delete account","close account","account settings","gdpr","dpdp","data export","app store subscription","play store billing","type to confirm"],"flows":["account-settings","danger-zone"],"fields":["text"],"summary":"Reads back everything an account deletion loses or keeps charging for, so users do not delete more than they expect or keep paying an app store.","server":"Authorise the deletion on the server, re-check ownership and active subscriptions there, and run any scheduled deletion and retention from the backend.","catches":["Teams or workspaces where the user is the only owner, with a transfer action","App Store and Google Play subscriptions that keep billing after the account is gone","Deleting mid-way through a paid period, with an option to delete when it ends","No data export taken yet (the button reads \"Delete without export\")","A typed confirmation that does not match the phrase"],"limits":["Only shows the items, teams and subscriptions you pass in; it does not discover them","Whether a store plan is cancelled comes from your data (e.g. the store's server API), not from the component","It does not perform deletion, export or transfer; those are your callbacks"],"a11y":["Labelled confirmation input with aria-invalid and aria-describedby on a mismatch","Timing options are native radio inputs with labels","Export progress is announced in an aria-live=\"polite\" region","The Delete button is described by the list of what still blocks it; focus moves to the first fix button"]}},{"name":"force-push-confirm","type":"registry:ui","title":"Force Push Confirm","description":"Lists every commit a force push would drop with author, message and age, marks other people's commits and protected branches, builds a --force-with-lease command pinned to the tip shown (or --force-if-includes), and asks you to type the branch name.","categories":["Destructive actions"],"files":[{"path":"registry/default/force-push-confirm/force-push-confirm.tsx","type":"registry:ui"},{"path":"registry/default/force-push-confirm/force-push.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Teammates' commits erased by force push","mistakes":["force push deleted commits","git push -f overwrote remote","lost teammate commits","force with lease bypassed"],"aliases":["git push --force","force-with-lease","force-if-includes","rebase","protected branch","github","remote branch"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Lists every commit a force push would drop, marks other people's work and builds a safer lease command, so teammates' commits aren't erased.","server":"Protect shared branches in your Git host (no force pushes, required reviews); this dialog only prepares a safer command.","catches":["Commits on the remote that the push would drop","Commits by other people, including co-authors and GitHub noreply addresses","Protected branches","A remote tip that moved since you looked","Plain --force, replaced by a pinned --force-with-lease"],"limits":["Uses the commit lists you pass; it doesn't run git","--force-with-lease only protects the tip you pinned; your Git host's branch protection is the real guard"],"a11y":["Dropped commits are a labelled list","The lease option is a labelled checkbox; branch confirmation is a typed field","Warnings use icons plus text"]}},{"name":"infra-plan-readback","type":"registry:ui","title":"Infra Plan Readback","description":"Reads a `terraform show -json` plan as create, update, replace and destroy counts, lists stateful destroys and replacements first with the attribute forcing each, and keeps Apply locked until each loss is acknowledged (and, in production, the workspace typed).","categories":["Destructive actions"],"files":[{"path":"registry/default/infra-plan-readback/infra-plan-readback.tsx","type":"registry:ui"},{"path":"registry/default/infra-plan-readback/plan.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent terraform apply destroying a production database","mistakes":["terraform destroyed my database","apply replaced rds instance","force_destroy deleted bucket","wrong workspace terraform apply"],"aliases":["terraform plan","terraform apply","opentofu","iac","infra","aws rds","s3 bucket","devops","prod workspace"],"flows":["devtools","danger-zone"],"fields":["code","text"],"summary":"Reads a Terraform or OpenTofu plan back with data-losing destroys and replacements first, so an apply doesn't quietly delete a production database.","server":"Protect production state with policy checks (OPA/Sentinel), prevent_destroy and deletion protection, and require reviewed plans in CI.","catches":["Destroys and replacements of databases, buckets, volumes and keys, listed first","The attribute that forces a replacement (before → after, sensitive values hidden)","Destroy-before-create replacements that leave a gap","force_destroy and skip_final_snapshot consequences","Production workspace, or var.environment saying production when the workspace doesn't"],"limits":["Only as good as the plan JSON; a plan made against stale state or a different workspace isn't detected","Stateful detection uses a built-in type list plus your statefulTypes/isStateful","The lock is in this UI; terraform apply from a terminal or CI is not affected"],"a11y":["Parse errors and unappliable plans use role=alert","Data-loss list is a labelled section with a checkbox label per item","Workspace confirmation input has a visible label","Status uses role=status; sign icons are aria-hidden with the action named in text"]}},{"name":"recovery-contact-cooldown","type":"registry:ui","title":"Recovery Contact Cooldown","description":"Shows a pending recovery-contact change: the masked new contact, a countdown to when it applies, that codes still go to the old contact, an urgent state in the last hour, a cancel button and a \"this wasn't me\" path. Announces the outcome, not the ticking.","categories":["Destructive actions"],"files":[{"path":"registry/default/recovery-contact-cooldown/recovery-contact-cooldown.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps the owner catch a swapped recovery contact before it takes effect","mistakes":["attacker changed recovery email","recovery phone changed without me","account takeover recovery contact","sim swap changed recovery number"],"aliases":["recovery email","recovery phone","security hold","account recovery","change email","mobile number","+91","this wasn't me"],"flows":["account-settings","signup-login"],"fields":["email","phone"],"summary":"Makes a pending recovery email or phone change visible during its hold, so the real owner can cancel a takeover attempt in time.","server":"The backend must hold the change, keep the old contact working, notify it, and accept cancellation; this card only surfaces that state.","catches":["A recovery email or phone change that hasn't taken effect yet","Masked old and new contacts so the owner can recognise them","Time left, turning urgent in the final hour","One-tap cancel and a \"this wasn't me\" path","Says the old contact was notified only when previousNotified is true"],"limits":["Displays the hold; it does not create or enforce it","Pending turns to applied on the client clock unless you pass now/status from the server","Masking is cosmetic; the full values still reach the browser"],"a11y":["Section labelled by its title; countdown is role=timer and is not announced every second","Only the outcome (applied) is announced via a polite sr-only region","Cancel errors show role=alert text","After cancel or apply, focus moves to the result message","Urgency shown with text and colour"]}},{"name":"migration-preview","type":"registry:ui","title":"Migration Preview","description":"Postgres migration reader (strings, dollar quotes, comments handled) showing what each step locks, which rewrite big tables and which drops can't be undone, and catching failing NOT NULL adds, mid-deploy renames, CONCURRENTLY in a transaction and no lock_timeout.","categories":["Destructive actions"],"files":[{"path":"registry/default/migration-preview/migration-preview.tsx","type":"registry:ui"},{"path":"registry/default/migration-preview/review-migration.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a migration locking a production table for hours","mistakes":["migration took down production","alter table locked table","not null column failed migration","dropped column lost data"],"aliases":["db migration","postgres","alter table","lock_timeout","create index concurrently","prisma migrate","drizzle","sql preview","schema change"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Reads a Postgres migration back step by step with what each step locks and what can't be undone, so it doesn't take production down or lose data.","server":"Run migrations with lock_timeout and statement_timeout, test on a production-sized copy, and keep a rollback plan.","catches":["Locks each step takes and what other sessions can't do meanwhile","Steps that rewrite or scan every row of a large table (from tableSizes)","Dropped tables and columns that can't be undone","NOT NULL without a default on existing rows, and renames that break running code","CREATE INDEX CONCURRENTLY inside a transaction, and missing lock_timeout"],"limits":["Postgres only, and lock behaviour can differ between versions","A static read: it doesn't run the SQL or know current load","Row counts only if you pass tableSizes"],"a11y":["Section labelled by its title; summary uses role=status","Each step is labelled with its number; icons are aria-hidden","Fix and undo are buttons, and focus moves to Undo after a fix"]}},{"name":"feature-flag-rollout","type":"registry:ui","title":"Feature Flag Rollout","description":"Reads back who gets the flag (segments plus a percentage of everyone else, in users), asks for a second click before production skips ramp steps or gets ahead of staging, and warns how many users lose the feature on a pullback.","categories":["Destructive actions"],"files":[{"path":"registry/default/feature-flag-rollout/feature-flag-rollout.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Feature flag released to everyone early","mistakes":["flag rolled out to 100 percent","skipped staging rollout","rollback removed feature from users","wrong segment got feature"],"aliases":["feature flag","rollout","percentage rollout","canary","launchdarkly","kill switch","ramp","segments","a/b test"],"flows":["devtools"],"fields":["number"],"summary":"Reads back exactly who gets a feature flag and asks again before production skips ramp steps, so a flag isn't released to everyone early.","server":"Enforce rollout rules and approvals in the flag service, and keep an audit log of changes.","catches":["Production rollouts that skip ramp steps","Production ahead of staging","Who actually gets the flag, counted in users","How many users lose the feature on a pullback"],"limits":["User counts come from the segment sizes you pass","Saving runs your callback; the flag service decides real exposure"],"a11y":["Each environment is a fieldset with a legend; the slider has a <label> and an aria-valuetext readback","Segments are a labelled group of aria-pressed toggles","Warnings are aria-live=\"polite\"; save status uses role=\"status\""]}},{"name":"branch-target","type":"registry:ui","title":"Branch Target","description":"Checks the chosen base against branch rules (feature → main is caught, with a one-tap switch to develop), counts commits the branch would drag in that aren't its own, and flags a head that is behind its target with an Update branch action.","categories":["Destructive actions"],"files":[{"path":"registry/default/branch-target/branch-target.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Feature branch merged into wrong base","mistakes":["merged into main instead of develop","pr opened against wrong branch","deployed wrong branch","branch behind target"],"aliases":["base branch","pull request","merge request","git flow","deploy target","main","develop","update branch"],"flows":["devtools"],"fields":["text"],"summary":"Checks a merge or deploy target against your branch rules, so a feature branch doesn't land in the wrong base.","server":"Enforce branch protection and allowed merge targets in your Git host; this picker is a guide, not a protection rule.","catches":["A target the rules don't allow for this head, with a one-tap switch to an allowed one","Commits the merge would bring in that are not the head's own (wrong fork point)","A head that is behind its target","Targeting the head branch itself"],"limits":["Uses the branch rules, commit lists and fork point you pass; it does not query Git","Wildcard rules are simple * patterns; the first matching rule wins"],"a11y":["Native <select> with a <label>, aria-invalid when blocked and aria-describedby to its hint","The hint is aria-live=\"polite\"","Update uses aria-disabled while updating"]}},{"name":"session-list","type":"registry:ui","title":"Session List","description":"Signed-in sessions list with this device pinned and marked, \"Sign out all others\" that never includes it, an inline confirm (Cancel focused) before signing out here, and sessions from another country flagged with \"approximate location\" wording.","categories":["Destructive actions"],"files":[{"path":"registry/default/session-list/session-list.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Signed out own device by mistake","mistakes":["sign out all logged me out","logged out current device","cant tell which session is mine","unknown login location"],"aliases":["active sessions","devices","logged in devices","sign out everywhere","security settings","where you're logged in","login activity"],"flows":["account-settings","signup-login"],"fields":[],"summary":"Keeps the current device safe while cleaning up sessions: it's pinned and marked, excluded from \"sign out all others\", and confirmed inline.","server":"Revoke sessions server-side and never trust a client flag for which session is current.","catches":["Signing out the current device by accident (inline confirm, Cancel focused)","\"Sign out all others\" including this device (it never does)","Sessions from a different country than this device","Location shown as approximate, not exact"],"limits":["Country comparison uses the IP-derived location you pass, which can be wrong (VPNs, mobile carriers)","Does not revoke sessions itself; your callbacks do"],"a11y":["Section labelled by its title; rows set aria-busy while revoking","Inline confirm is a labelled group; Cancel gets focus, Escape closes and focus returns","Revoke buttons include sr-only device and location","Outcome announced in a polite live region"]}},{"name":"sign-in-approve","type":"registry:ui","title":"Sign-in Approve","description":"Sign-in approval prompt with no one-tap Approve: the user types the number from the sign-in screen, sees location and device, is warned about request bursts and \"support\" asking for codes, and every denial ends on a secure-my-account step.","categories":["Destructive actions"],"files":[{"path":"registry/default/sign-in-approve/sign-in-approve.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent approving an attacker's sign-in through push fatigue","mistakes":["mfa fatigue attack","user approved fake login","push notification spam approve","number matching 2fa"],"aliases":["mfa","2fa","push approval","number matching","login approval","authenticator","is this you","sign-in request"],"flows":["signup-login","account-settings"],"fields":["code"],"summary":"Replaces one-tap sign-in approval with typing the number shown on the sign-in screen, to make MFA-fatigue approvals harder.","server":"Verify the number, enforce attempts and expiry, and rate-limit push requests on the server; deny means kill the pending session.","catches":["Approvals without seeing the sign-in screen (number matching)","Unusual locations far from normal sign-ins","Bursts of requests in the last 15 minutes","Someone claiming to be support asking for the number","Wrong numbers, stopping after maxAttempts","Expired requests"],"limits":["Number checking and attempt limits run through your onApprove; the server must enforce them","Location is approximate and IP-based","Does not send or create the request"],"a11y":["Section labelled by its title; details in a <dl>","Focus moves to the number input on open and to the result (role=status) when finished","Unusual location shown with an icon and text","Secure-my-account is a labelled button"]}},{"name":"hold-to-confirm","type":"registry:ui","title":"Hold to Confirm","description":"Button that runs onConfirm only after a full press-and-hold by mouse, touch or keyboard, rewinds on early release, shows \"Hold to confirm\" on a plain click, and lets users who can't hold (screen reader, switch access) confirm with a second press.","categories":["Destructive actions"],"files":[{"path":"registry/default/hold-to-confirm/hold-to-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Helps prevent a destructive action fired by a stray click","mistakes":["accidental click deleted item","user tapped delete by mistake","need long press to confirm","hold button to delete"],"aliases":["long press","press and hold","hold button","delete button","confirm button","mobile tap","destructive action"],"flows":["danger-zone"],"fields":[],"summary":"A button that fires only after a full press-and-hold, so a stray click or tap doesn't trigger a destructive action.","server":"Authorize the destructive action on the server and make it idempotent.","catches":["Stray click or tap on a delete or other irreversible button","Releasing early: progress rewinds","Plain click: a hint explains to hold","Users who can't hold: a second press confirms instead"],"limits":["Friction, not authorization: anyone who holds it confirms","The second-press path is quicker than holding, by design","Doesn't name the target; pair it with clear wording or a readback"],"a11y":["Native button; Space or Enter can be held from the keyboard","Hint text is linked via aria-describedby and status changes use role=status","aria-busy while onConfirm is pending; icons are aria-hidden","Fill and label animations are reduced under reduced motion"]}},{"name":"two-step-button","type":"registry:ui","title":"Two-Step Button","description":"Destructive button where the first click arms it with a draining timer and a second deliberate click confirms; accidental double-clicks, held keys, blur, Escape or the timeout all disarm it.","categories":["Destructive actions"],"files":[{"path":"registry/default/two-step-button/two-step-button.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"One click deleted without confirmation","mistakes":["double click deleted twice","delete button no confirm","click again to confirm","accidental delete click"],"aliases":["confirm button","click twice","arm button","inline confirm","delete button","are you sure","destructive button"],"flows":["danger-zone"],"fields":[],"summary":"Needs two deliberate clicks for a destructive action: the first arms it with a draining timer, the second confirms.","catches":["A single accidental click (only arms)","Double-clicks faster than guard ms","Held Enter (auto-repeat)","Losing focus, Escape or timeout, which disarm"],"limits":["A deliberate second click still confirms; it is not a full review step","UI guard only, not authorization"],"a11y":["Arming is announced via a polite live region (armedHint)","Escape disarms; held Enter is ignored","aria-busy while an async confirm is pending","Label changes are text, not colour alone"]}},{"name":"slide-to-confirm","type":"registry:ui","title":"Slide to Confirm","description":"Slide-to-confirm control where the thumb must travel the whole track (threshold), a held arrow key stops short of the end, an async confirm shows a pending state, and a rejection slides it back.","categories":["Destructive actions"],"files":[{"path":"registry/default/slide-to-confirm/slide-to-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Pocket tap sent a payment","mistakes":["accidental tap sent payment","pocket press ended session","swipe to pay","slide to unlock confirm"],"aliases":["swipe to pay","slide to confirm","swipe button","upi pay","slider","mobile checkout","end ride","pay now"],"flows":["checkout","india"],"fields":["amount"],"summary":"Requires the thumb to travel the whole track before confirming, so accidental taps and pocket presses don't send payments.","server":"For payments, keep server-side idempotency and authorization; this only reduces accidental triggers.","catches":["Taps and short drags that don't reach the threshold","Held arrow keys (auto-repeat) that would otherwise complete it","Repeat confirms while an async confirm is pending","Failed confirms, which slide back"],"limits":["An accidental full slide still confirms","A UX guard, not authorization"],"a11y":["role=slider with aria-valuenow/valuetext and an aria-label from label","Keyboard operable; holding a key won't complete the slide","Phase text in a polite live region; aria-disabled while locked","Spring animation respects reduced motion"]}},{"name":"type-to-confirm","type":"registry:ui","title":"Type to Confirm","description":"GitHub-style confirm dialog (native <dialog>) that unlocks only after the exact phrase is typed, highlighting matched characters and flagging a typo the moment it happens; errors keep the dialog open.","categories":["Destructive actions"],"files":[{"path":"registry/default/type-to-confirm/type-to-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong repo deleted on reflex","mistakes":["deleted wrong project","type name to delete","github delete repo confirm","confirm by typing name"],"aliases":["type to confirm","delete repository","delete project","github style","confirmation dialog","modal","danger zone"],"flows":["danger-zone","account-settings"],"fields":["text"],"summary":"Confirmation dialog that unlocks a destructive action only after the exact name is typed, flagging a typo as it happens.","server":"Authorize the destructive action server-side and confirm the target id there; the typed phrase is a UX guard only.","catches":["Confirming before the exact phrase is typed","A typo, flagged at the first mismatching character","Failed confirms, which keep the dialog open with the error","Repeat confirms while pending"],"limits":["Case-sensitive exact match after trimming leading and trailing spaces","Typing a name proves attention, not authorization","Make the phrase the real target (repo, env, account) so it names what's at stake"],"a11y":["Native <dialog> labelled and described by its title and description","Input has a <label>, autoFocus, aria-invalid and a polite hint","Confirm uses aria-disabled (stays discoverable) and aria-busy while pending","Errors use role=alert"]}},{"name":"undo-toast","type":"registry:ui","title":"Undo Toast","description":"Deferred actions with an undo window: actions apply optimistically and commit only after a countdown that pauses on hover, focus or a hidden tab, with Ctrl/⌘+Z, a commit on page close (pagehide), and a rollback notice if the commit fails.","categories":["Destructive actions"],"files":[{"path":"registry/default/undo-toast/undo-toast.tsx","type":"registry:ui"}],"dependencies":["@number-flow/react","lucide-react","motion"],"meta":{"stops":"Deleted item gone with no undo","mistakes":["deleted email no undo","accidentally archived item","undo delete toast","action committed too fast"],"aliases":["undo","snackbar","toast","sonner","optimistic update","ctrl+z","gmail undo","archive"],"flows":["danger-zone","messaging"],"fields":[],"summary":"Runs actions optimistically and commits only after an undo window, so a mistake can be taken back without an \"Are you sure?\".","catches":["Regretted actions, undone before onCommit runs","Countdown paused while hovered, focused or the tab is hidden","Leaving the page, which flushes pending commits","Failed commits, rolled back with a notice"],"limits":["Commits on pagehide can't be awaited; use fetch keepalive or sendBeacon","If the tab crashes, a pending commit may never run","Other users may see the optimistic state only after commit"],"a11y":["Toast region has an aria-label and polite live updates; failures use role=alert","Undo button has an aria-label with the message and aria-keyshortcuts","Ctrl/⌘+Z triggers undo when hotkey is on","Countdown ring respects reduced motion"]}},{"name":"impact-preview","type":"registry:ui","title":"Impact Preview","description":"Delete confirmation that loads every dependent of a parent object, grouped by type with counts and samples and marked deleted, detached or kept, and keeps Confirm locked until the list has loaded.","categories":["Destructive actions"],"files":[{"path":"registry/default/impact-preview/impact-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a cascade delete wiping child records","mistakes":["deleted project lost all tasks","cascade delete surprise","what else gets deleted","on delete cascade"],"aliases":["cascade delete","delete preview","related records","dependencies","foreign key","delete folder","delete workspace"],"flows":["danger-zone"],"fields":[],"summary":"Before a delete, loads and lists everything that goes with the parent object, so a cascade doesn't wipe child records nobody saw.","server":"Run the delete in a transaction with the same cascade rules and authorization on the server.","catches":["Child records a cascade will delete","Records that will be detached rather than deleted","Confirming before the impact has loaded: Confirm stays locked","Load failures, with a retry"],"limits":["Accurate only if your load function mirrors the real cascade rules","The list can be stale if data changes between preview and delete","Not authorization; it doesn't decide who may delete"],"a11y":["Dialog content labelled by its title and aria-busy while loading","Loading text in a polite live region; errors use role=alert","Groups expand with aria-expanded and aria-controls","Locked Confirm uses aria-disabled so it stays focusable"]}},{"name":"bulk-scope-confirm","type":"registry:ui","title":"Bulk Scope Confirm","description":"States the exact count and risky subsets (\"including 12 you don't own\"), defaults to only the selected items on this page, and locks Confirm if the match count changes while the dialog is open.","categories":["Destructive actions"],"files":[{"path":"registry/default/bulk-scope-confirm/bulk-scope-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bulk action hit every record","mistakes":["select all deleted everything","bulk delete more than expected","applied to all pages not just this","mass update wrong rows"],"aliases":["bulk delete","select all","bulk edit","mass action","data table","admin panel","batch update"],"flows":["danger-zone"],"fields":["number"],"summary":"States exactly how many records a bulk action will hit and defaults to this page only, so \"select all\" doesn't hit every record.","server":"Resolve the scope on the server at execution time, check the count you confirmed, and authorise each record.","catches":["\"All matching\" scope when only this page was meant (page is the default)","Risky subsets inside the selection, counted by category","The match count changing while the dialog is open, which locks Confirm until acknowledged"],"limits":["Counts and categories come from your props; it does not query the data","Drift is only noticed when you pass updated counts while it is open"],"a11y":["Scope choice is a fieldset with a legend and labelled radio options","The summary and the drift warning are aria-live=\"polite\"","Confirm uses aria-disabled while locked, aria-busy while pending and aria-describedby to the drift warning","Breakdown and sample lists have aria-labels"]}},{"name":"drop-confirm","type":"registry:ui","title":"Drop Confirm","description":"The target names exactly what will move and where, ignores a drop before the pointer has rested on it, asks once before large moves or moves that change who can see the items, and offers Undo, with a Move here button for keyboard and touch.","categories":["Destructive actions"],"files":[{"path":"registry/default/drop-confirm/drop-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Folder dropped into wrong place","mistakes":["drag and drop moved wrong folder","accidental drag drop","dropped file in shared folder","drag changed permissions"],"aliases":["drag and drop","dnd","move to folder","kanban","file manager","dnd-kit","drop zone"],"flows":["danger-zone"],"fields":["file"],"summary":"A drop target that names what will move and where, ignores flick drops and offers Undo, so files and folders don't land in the wrong place.","server":"Re-check permissions for the destination on the server, and support undo for moves that change sharing.","catches":["Drops that land in passing, before the pointer settles (settleTime)","Large moves above a threshold (confirmAbove)","Moves into a different space that change who can see the items","Regret, with a timed Undo"],"limits":["Item counts and spaces come from your drag data and props","Undo calls your onUndo; it can't reverse a move your backend already finalised"],"a11y":["Each target is a role=\"group\" labelled with the destination and described by its status","A Move here button gives keyboard and touch users the same action, with a counted aria-label","The confirm step focuses Cancel; Escape closes it"]}},{"name":"danger-zone","type":"registry:ui","title":"Danger Zone","description":"Destructive actions stay listed with their consequences but locked until the user deliberately unlocks the section, which relocks after one action, an idle timeout (60 s default), Escape, a hidden tab or focus leaving it.","categories":["Destructive actions"],"files":[{"path":"registry/default/danger-zone/danger-zone.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Stray click deleted from settings","mistakes":["delete button in settings clicked by mistake","settings danger zone","transfer repo by accident","locked delete section"],"aliases":["danger zone","settings page","delete account","delete project","transfer ownership","github settings","admin settings"],"flows":["danger-zone","account-settings"],"fields":[],"summary":"Keeps destructive settings visible but locked until deliberately unlocked, so a stray click doesn't delete or transfer something.","server":"Authorise every destructive action on the server and require re-authentication where it matters; the lock is a UI guard.","catches":["Stray clicks on destructive actions while the section is locked","An unlocked section left open (idle timeout, tab hidden, focus leaving)","Chained destructive actions, by relocking after each one"],"limits":["Unlocking is a deliberate click, not authentication; pair it with re-auth or typed confirmation for serious actions","It does not perform or confirm the action itself"],"a11y":["Section labelled by its title; the unlock toggle uses aria-pressed","Locked actions use aria-disabled so they stay focusable and are described by their consequence","Escape relocks; lock state changes are aria-live=\"polite\"","Actions set aria-busy while pending"]}},{"name":"click-shield","type":"registry:ui","title":"Click Shield","description":"Wraps a control and ignores pointer presses for about 500 ms after it appears, shifts, changes its label or the tab regains focus, with a quiet fade-in cue. Keyboard users are only shielded from focus that code moved onto the control.","categories":["Destructive actions"],"files":[{"path":"registry/default/click-shield/click-shield.tsx","type":"registry:ui"}],"meta":{"stops":"Clicked button that just appeared","mistakes":["button moved under cursor","layout shift click","clicked wrong button after load","popup appeared and got clicked"],"aliases":["layout shift","cls","misclick","click protection","late button","dialog appears","ghost click"],"flows":["danger-zone","forms"],"fields":[],"summary":"Ignores clicks on a button for a moment after it appears, moves or changes, so people don't press something that just slid under the cursor.","catches":["Clicks on a control that just mounted","Clicks after the control moved under the pointer (layout shift)","Clicks right after its label changed, or when the tab comes back","Enter/Space key repeats on focus that code moved there"],"limits":["Swallows real clicks during the window too; keep duration short","A timing guard only: it doesn't confirm intent for destructive actions"],"a11y":["Keyboard presses are not delayed unless focus was moved by code","Exposes data-shielded for styling; no ARIA changes to the wrapped control"]}},{"name":"shortcut-confirm","type":"registry:ui","title":"Shortcut Confirm","description":"Two-press guard for destructive shortcuts: the first press shows \"Press ⌘⌫ again to delete 3 items\", and only a separate second press within the window confirms. Held keys, extra modifiers, stray keys in between and keystrokes in text fields don't count.","categories":["Destructive actions"],"files":[{"path":"registry/default/shortcut-confirm/shortcut-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Keyboard shortcut deleted items","mistakes":["shortcut deleted by accident","cmd backspace deleted all","hotkey fired in text field","press again to confirm"],"aliases":["keyboard shortcut","hotkey","cmd+backspace","keybinding","power user","press again","shortcuts"],"flows":["danger-zone"],"fields":[],"summary":"Makes a destructive keyboard shortcut need a deliberate second press, so a slipped finger only shows a prompt.","catches":["A first press, which only arms and explains","Key auto-repeat from holding the shortcut","An extra modifier (a slipped Shift)","Any other key pressed between the two presses","Shortcuts typed inside inputs, unless allowInInputs","IME composition keystrokes"],"limits":["Guards the keyboard path only; menu or button paths need their own confirm","Letters also match by physical key, so a layout remap could still trigger it"],"a11y":["Armed and done prompts render in a polite live region","Shortcut shown as ⌘ on Apple platforms and Ctrl elsewhere","Icons are aria-hidden"]}},{"name":"guarded-switch","type":"registry:ui","title":"Guarded Switch","description":"Toggle for critical settings with a see-through cover, like an aircraft switch guard: lift it first, it states what flipping will do while open, drops back on its own after a few seconds, and reads back the consequence after the change. Red tone for irreversible effects.","categories":["Destructive actions"],"files":[{"path":"registry/default/guarded-switch/guarded-switch.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a kill switch flipped by accident","mistakes":["toggled maintenance mode by mistake","kill switch toggled","critical toggle accidental click","turned off payments by mistake"],"aliases":["toggle","switch","maintenance mode","kill switch","feature toggle","settings toggle","guarded toggle"],"flows":["devtools","account-settings","danger-zone"],"fields":[],"summary":"Puts a lift-up cover over a critical switch like maintenance mode or a kill switch, so a stray click can't flip it.","server":"Authorize and audit-log the setting change on the server.","catches":["Stray click or tap on a critical toggle","Flipping without knowing the effect: the on/off consequence is shown while the guard is up","Guard left open: it closes again on its own","A rejected change keeps the old state"],"limits":["A deliberate user can still flip it; it adds friction, not authorization","Does not confirm who flipped it or log the change"],"a11y":["The switch has role=switch, aria-checked, a label and a description; it is out of the tab order until the cover is lifted","Cover is a button with aria-expanded and an aria-label; Escape closes it and focus moves sensibly between cover and switch","Effect line is a polite live region","Pending state uses aria-busy; icons are aria-hidden"]}},{"name":"swipe-delete-row","type":"registry:ui","title":"Swipe Delete Row","description":"Swipe-to-delete row that deletes only when dragged past a visible line (not on a fast flick or vertical scroll), resists like a rubber band, springs back if released early, has a trash button for keyboard users and keeps an Undo before the delete commits.","categories":["Destructive actions"],"files":[{"path":"registry/default/swipe-delete-row/swipe-delete-row.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Swipe while scrolling deleted row","mistakes":["swipe deleted by accident","flick deleted list item","scroll triggered delete","swipe to delete undo"],"aliases":["swipe to delete","swipe actions","list row","mobile list","inbox","gesture","ios style"],"flows":["danger-zone","messaging"],"fields":[],"summary":"Makes swipe-to-delete deliberate: the row deletes only past a visible line, never on a flick or scroll, and keeps an Undo.","catches":["Swipes that don't cross the threshold","Fast flicks that cross it too quickly (minSwipeMs)","Vertical scrolls mistaken for swipes","Deletes regretted within undoFor"],"limits":["onDelete runs after the undo window, so the item stays in your data until then","Undo is in-memory; a navigation commits the delete"],"a11y":["Trash button with an aria-label naming the item, for keyboard and switch users","After a keyboard delete, focus moves to Undo; after undo, back to the trash button","Status uses role=status and a polite live region","Spring motion respects reduced motion"]}},{"name":"grace-send","type":"registry:ui","title":"Grace Send","description":"Send button with a grace window: pressing it starts a countdown ring before your onSend runs, so Cancel, Escape or a leave-page warning can stop it; Send now skips the wait. Shows pending, sent, cancelled and failed states.","categories":["Destructive actions"],"files":[{"path":"registry/default/grace-send/grace-send.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a send before second thoughts","mistakes":["sent message to wrong person","undo send email","regret sending","send too fast"],"aliases":["undo send","unsend","send delay","gmail undo send","chat","whatsapp","compose","send button"],"flows":["messaging"],"fields":["text"],"summary":"Gives a short change-of-mind window after pressing Send, so a regretted message or action can be cancelled before it actually goes.","server":"Make the send idempotent on the server; the grace window is client-side only.","catches":["Send pressed too early or by mistake","Leaving the page during the countdown (beforeunload warning)","Held Enter auto-repeating the press","Failed send surfaced with an alert"],"limits":["The delay lives in the open tab; closing the tab or a crash during the window cancels nothing on the server because nothing has been sent","Not undo: once onSend runs, the action is out","Does not de-duplicate sends on the server"],"a11y":["Countdown is a labelled group with the seconds in its name; status text in a polite live region and failures in role=alert","Escape cancels during the countdown; focus moves to Cancel when it starts and back to Send after","Pending button has aria-busy; icons are aria-hidden","Ring animation is dropped under reduced motion"]}},{"name":"two-person-approve","type":"registry:ui","title":"Two-Person Approve","description":"Approval UI for high-risk actions that surfaces the two-person rule: the requester can't approve their own request here, approval is tied to a fingerprint of the exact plan so edits need re-review, rejections need a reason, and expired requests can't be approved.","categories":["Destructive actions"],"files":[{"path":"registry/default/two-person-approve/two-person-approve.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a requester approving their own risky change","mistakes":["self approval of request","four eyes principle","plan changed after approval","maker checker"],"aliases":["maker checker","four eyes","approval flow","two person rule","approve request","change management","bank approval","access request"],"flows":["devtools","danger-zone"],"fields":[],"summary":"Review screen for the two-person rule: it shows the exact plan, hides Approve from the requester, and flags edits made after review.","server":"Enforce it on the server: reject approvals from the requester or non-approvers, bind the approval to a server-side hash of the exact plan, and refuse expired requests.","catches":["The requester trying to approve their own request","Someone not on the approver list","A plan edited after review (fingerprint changed), with changed lines marked","Rejections without a reason","Expired requests"],"limits":["UI only: who may approve and whether the plan changed must be checked by your server","The fingerprint is FNV-1a (8 hex digits), a change detector, not a cryptographic hash","Expiry uses the client clock unless you pass now"],"a11y":["Section labelled by its title; plan <pre> is described by the changed-plan notice","Changed lines have a sr-only \"Changed:\" prefix","Outcome and expiry use role=status; changed-plan notice is a polite live region","Reason textarea has a <label>, aria-invalid when missing, and receives focus on reject"]}},{"name":"trash-countdown","type":"registry:ui","title":"Trash Countdown","description":"Trash list showing time left before each item is purged, highlighting soon-to-go items, with one-tap restore, and an empty-trash confirm that needs the typed item count and resets if new items arrive mid-confirmation.","categories":["Destructive actions"],"files":[{"path":"registry/default/trash-countdown/trash-countdown.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Trash purged before anyone restored","mistakes":["trash emptied by mistake","items deleted forever surprise","restore from trash","auto delete after 30 days"],"aliases":["trash","recycle bin","bin","restore","soft delete","empty trash","deleted items","30 days"],"flows":["danger-zone"],"fields":[],"summary":"Shows how long each trashed item has before it's deleted forever, and makes emptying the trash need the typed item count.","server":"Run purges server-side on the stated schedule and authorize empty-trash requests; keep restore possible until the purge.","catches":["Items close to permanent deletion (within soonDays)","Time left per item, as text and a bar","Recently trashed items, called out before emptying","New items arriving during the empty confirmation, which resets it"],"limits":["Purge timing comes from retentionDays you pass; the server's purge job decides","Typing the count proves attention, not authorization"],"a11y":["Section labelled by its title; empty toggle uses aria-expanded/aria-controls","Confirm area is inert until opened; focus moves to the count input; Escape cancels","Count input has a <label> and is described by the grew-meanwhile note","Countdown bars respect reduced motion"]}},{"name":"range-select-guard","type":"registry:ui","title":"Range Select Guard","description":"Selectable list that reads back how many rows a shift-click range selected (with Undo), skips and reports rows hidden by the filter, flags rows on pages never opened with an \"only this page\" fix, and calls out selected rows a filter now hides.","categories":["Destructive actions"],"files":[{"path":"registry/default/range-select-guard/range-select-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Shift-click selected thousand rows","mistakes":["shift click selected too many","selected hidden rows","range select wrong","select all included filtered rows"],"aliases":["shift click","range select","multi select","data table","checkbox rows","tanstack table","bulk select"],"flows":["danger-zone","forms"],"fields":["number"],"summary":"Reads back how many rows a shift-click just selected, with Undo, so a runaway range selection doesn't act on rows nobody saw.","catches":["Shift-click ranges much larger than intended","Rows hidden by the filter: skipped and reported","Selected rows on pages never opened","Selected rows that a filter now hides, with deselect"],"limits":["Works on the rows you pass; server-side \"select all\" isn't covered","Bulk actions are yours to gate"],"a11y":["Pagination buttons have aria-labels; the page indicator is a polite live region","Selection readback is a polite live region","Focus stays on the clicked row; highlight motion is skipped under reduced motion"]}},{"name":"reauth-gate","type":"registry:ui","title":"Reauth Gate","description":"Step-up prompt for sensitive actions. Checks a passkey, password or PIN through your own verify function, names the action and why it asks, runs the queued action after confirmation, and remembers it for a short window shown as an endable countdown chip.","categories":["Destructive actions"],"files":[{"path":"registry/default/reauth-gate/reauth-gate.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a sensitive action on an unattended signed-in screen","mistakes":["no password confirm before delete","sudo mode","reauthenticate before change","someone used my open laptop"],"aliases":["sudo mode","re-authenticate","confirm password","passkey","pin","step-up auth","verify identity","mpin"],"flows":["account-settings","signup-login","danger-zone"],"fields":["secret"],"summary":"Asks the user to re-confirm with a passkey, password or PIN before a sensitive action, so an unattended signed-in screen is harder to misuse.","server":"Re-check the credential and a server-side recent-auth timestamp on every sensitive endpoint, and rate-limit verify; the client window is only a convenience.","catches":["Sensitive action started without a recent re-confirmation","Names the exact action and the reason before asking","Runs the queued action right after a successful verify","Shows how long the unlock lasts, with a button to end it early","Wrong credential or a rejected verify shows an error and keeps the action queued"],"limits":["The unlock window lives in component state only; a reload, another tab or a direct API call is not covered","Security depends entirely on your verify function and your server","No attempt limit or lockout; rate-limit verification on the server","Passkey mode calls verify with no secret; you run the WebAuthn ceremony inside verify"],"a11y":["Prompt is a labelled region (role=region with aria-labelledby); focus moves to its first control when it opens","Escape cancels the prompt","Method switch uses aria-pressed buttons in a labelled group","Credential input has a sr-only label, aria-invalid on a wrong attempt, and a polite live message","Countdown chip is role=timer with an aria-label; its progress animation is skipped under reduced motion"]}},{"name":"rollback-preview","type":"registry:ui","title":"Rollback Preview","description":"Rollback confirmation listing what reverts, what won't undo (irreversible migrations, data written since), the target's age and every newer version skipped. Confirms only when the target's name is typed, and calls out a different version's name instead of silently not matching.","categories":["Destructive actions"],"files":[{"path":"registry/default/rollback-preview/rollback-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Rollback skipped more versions than expected","mistakes":["rollback reverted too much","rolled back wrong version","migration cant undo on rollback","redeploy old version"],"aliases":["rollback","revert deploy","redeploy","release","vercel","version history","deployment","downgrade"],"flows":["devtools","danger-zone"],"fields":["text"],"summary":"Shows what a rollback reverts, what it can't undo and which versions it skips, then confirms by typing the target's name.","server":"Authorize the rollback server-side and check that the target is still deployable (schema compatibility) before running it.","catches":["Changes that revert, listed","What stays: irreversible migrations and data written since, with consequences","Newer versions the rollback skips","Typed name that matches a different version (\"v4\" for \"v40\")","Case and whitespace differences, normalised (\"V40 \" matches \"v40\")"],"limits":["Lists only what you pass; it cannot inspect your deploys or migrations","Typing a name proves attention, not authorization"],"a11y":["Name input has a <label>, aria-invalid on a wrong version and a polite hint via aria-describedby","Enter in the input confirms when it matches","Confirm sets aria-busy while pending","Sections use icons plus text counts; motion skipped under reduced motion"]}},{"name":"edit-lock","type":"registry:ui","title":"Edit Lock","description":"Locks the form in a disabled fieldset (\"Locked by Priya after approval\"), says before unlocking that any change needs re-approval, voids the approval only when something actually changes, and relocks after idleness, never mid-typing.","categories":["Destructive actions"],"files":[{"path":"registry/default/edit-lock/edit-lock.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Approved invoice edited by mistake","mistakes":["edited approved record","changed invoice after approval","locked form","re-approval needed"],"aliases":["read only","locked form","approval","invoice","purchase order","fieldset disabled","edit mode","po"],"flows":["forms"],"fields":["amount"],"summary":"Locks a finished or approved record and states that edits need re-approval before unlocking, so approved invoices aren't changed by mistake.","server":"Enforce the lock on the server: reject edits to approved records unless the approval is voided and logged.","catches":["Accidental edits to approved records","Unlocking without knowing the consequence, via a confirmation step","Approvals voided by an unlock with no real change (only real edits void them)","Records left unlocked, by relocking after idleness"],"limits":["A UI lock: the record can still be changed through your API","Re-approval is signalled through onEdit; your workflow must act on it"],"a11y":["Locks with a native disabled <fieldset>","Lock state and edits are announced in an aria-live=\"polite\" region","The unlock confirmation focuses its confirm button; Escape cancels and returns focus","After unlocking, focus moves to the first field"]}},{"name":"archive-or-delete","type":"registry:ui","title":"Archive or Delete","description":"Shows Archive and Delete together with each affected thing's fate under both. The reversible choice starts selected and focused so Enter archives; choosing Delete highlights what is lost and relabels the button \"Delete forever\".","categories":["Destructive actions"],"files":[{"path":"registry/default/archive-or-delete/archive-or-delete.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Deleted forever instead of archived","mistakes":["clicked delete instead of archive","meant to archive","permanent delete by mistake","archive vs delete"],"aliases":["archive","delete forever","soft delete","inbox","project archive","hide","remove"],"flows":["danger-zone"],"fields":[],"summary":"Puts Archive and Delete side by side with what happens to each thing under both, so people don't delete what they only meant to put away.","server":"Authorise the delete on the server and, where possible, keep a recovery window; the dialog does not make deletion reversible.","catches":["Choosing permanent deletion when archiving would do","Hidden differences between the two choices, by listing only the items whose fate differs first","Ambiguous confirm buttons, by naming the choice and the thing (\"Delete forever\")"],"limits":["Only describes the fates you pass in; it does not know what your backend really deletes","It does not archive or delete anything; onConfirm does"],"a11y":["Choices are a role=\"radiogroup\" labelled by the title, each option described by its summary","Archive is focused on open (autoFocus, default true)","The confirm button sets aria-busy while pending","The Delete highlight animation is skipped with prefers-reduced-motion; fates are written in text, not just colour"]}},{"name":"revoke-access-preview","type":"registry:ui","title":"Revoke Access Preview","description":"Pre-removal readback for members, tokens or apps: what they lose, what they own with a reassign picker and one-tap reassign-all, sessions that end (flagging any in use now) and integrations that stop on their credentials. The button says if anything stays unowned.","categories":["Destructive actions"],"files":[{"path":"registry/default/revoke-access-preview/revoke-access-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Removed member's integrations stopped working","mistakes":["removed user broke integration","revoked token broke prod","offboarding orphaned files","remove member lost ownership"],"aliases":["remove member","offboarding","revoke access","api token","oauth app","team members","reassign","workspace members"],"flows":["account-settings","danger-zone"],"fields":["email","secret"],"summary":"Shows what removing a member, token or app will break: owned work left unowned, sessions ended and integrations that stop.","server":"Revoke tokens and sessions server-side, re-check authorization on the removal, and reassign ownership in the same transaction.","catches":["Owned items left without an owner, with reassign-to and reassign-all","Sessions that end, flagging any active within liveWithin","Integrations that stop because they run on that person's credentials","Integrations that keep running because they belong to the workspace","Confirm label that names how many items stay unowned"],"limits":["Only knows the owned items, sessions and integrations you pass","Does not revoke anything; onConfirm receives the reassignment"],"a11y":["Section labelled by its title; each owned item's picker has a <label>","Unowned warning in a polite live region with a one-tap fix","Stops/keeps-running stated as text, not colour alone","Confirm sets aria-busy while pending; motion skipped under reduced motion"]}},{"name":"transfer-ownership","type":"registry:ui","title":"Transfer Ownership","description":"Ownership transfer that reads back what moves and what you keep, warns when the email is outside your org with a one-tap switch to the colleague you probably meant, blocks a recipient whose plan can't own it, and transfers only once they accept, showing the pending request.","categories":["Destructive actions"],"files":[{"path":"registry/default/transfer-ownership/transfer-ownership.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Ownership transferred to the wrong email","mistakes":["transferred to wrong person","typo in transfer email","transfer ownership outside org","new owner plan cant own"],"aliases":["transfer ownership","change owner","workspace owner","admin","transfer repo","invite","handover"],"flows":["account-settings","danger-zone"],"fields":["email"],"summary":"Reads back what an ownership transfer moves and keeps, warns about an outside email or a recipient whose plan can't own it, and waits for acceptance.","server":"Authorize the transfer server-side, require the recipient's acceptance, enforce plan eligibility and expire stale requests.","catches":["Recipient email outside your organisation, with a likely internal match to switch to","Recipient plan too low to own the resource","What moves to them and what stays with you","Pending request and its expiry, with withdraw"],"limits":["Recipient and plan data come from your props","The accept-before-transfer flow must be implemented by your backend"],"a11y":["Section labelled by its title; email input has a <label>, aria-invalid on plan issues and a polite hint","Blocking issues use role=alert","Withdraw sets aria-busy while pending; state changes in a polite live region"]}},{"name":"cancel-readback","type":"registry:ui","title":"Cancel Readback","description":"The cancel screen reads back the exact date and time access ends, what is lost and kept, and the refund (or plainly that there is none), offers pause or downgrade once below a cancel button that never moves, and ends on a receipt with undo.","categories":["Destructive actions"],"files":[{"path":"registry/default/cancel-readback/cancel-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Cancelled without knowing when access ends","mistakes":["cancel subscription confusion","dark pattern cancel","refund unclear on cancel","lost access immediately"],"aliases":["cancel subscription","unsubscribe","billing","pause plan","downgrade","refund","churn","manage plan","autopay"],"flows":["account-settings","checkout"],"fields":["datetime","amount"],"summary":"Reads back exactly when access ends, what is lost and what is refunded before someone cancels, and ends on a receipt with an undo.","server":"Cancel and resume on the server and compute access end and refunds from billing records, not from what the screen showed.","catches":["Cancelling without knowing when access actually ends (date, time, zone and days left)","Unclear refunds, by stating the refund or that there is none","Dark-pattern retention: alternatives appear once, below a cancel button that stays put","Regret, with a resume action on the receipt"],"limits":["Shows the access end, refund and items you pass; it does not compute them","Cancelling and resuming happen in your callbacks"],"a11y":["Labelled by its title (aria-labelledby)","The receipt is announced with role=\"status\"","Buttons set aria-busy while pending","Lost and kept items use icons plus text"]}},{"name":"merge-records","type":"registry:ui","title":"Merge Records","description":"Duplicate merge panel where each field defaults to the most recent non-empty value, real conflicts are marked (ignoring case and spacing), and a live readback names every value that will be discarded, which record keeps its ID and which links move to it.","categories":["Destructive actions"],"files":[{"path":"registry/default/merge-records/merge-records.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Duplicate merge discarded customer data","mistakes":["merge duplicates lost data","which contact id kept","merge overwrote phone number","crm merge wrong values"],"aliases":["merge duplicates","dedupe","crm","contacts","customer records","merge contacts","duplicate leads"],"flows":["forms","danger-zone"],"fields":["name","email","phone"],"summary":"Before merging duplicate records, names every value that will be discarded, so a merge doesn't silently throw data away.","server":"Run the merge in one transaction on the server and keep an audit record of discarded values.","catches":["Values that the merge will discard, listed by name","Real conflicts vs case/spacing differences","Which record keeps its ID","Links (deals, tickets, integrations) that move to the survivor"],"limits":["Defaults by last-updated time, which isn't always the right value","Moves and deletes are done by your onMerge","One value per field; it can't combine lists"],"a11y":["Per-field choices and the survivor are native radio inputs in labelled groups","Readback highlight animation is skipped under reduced motion","Icons are aria-hidden"]}},{"name":"move-readback","type":"registry:ui","title":"Move Readback","description":"Move confirmation that reads back the destination path with the folder setting access highlighted, how many people gain or lose access, path links that break (with a redirect option) and inherited settings that change, and relabels the button when access widens.","categories":["Destructive actions"],"files":[{"path":"registry/default/move-readback/move-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a private doc being moved into a public folder","mistakes":["moved file made it public","move broke links","permissions changed after move","shared folder exposed file"],"aliases":["move to","folder","permissions","sharing","google drive","notion","breadcrumbs","access"],"flows":["danger-zone"],"fields":["file"],"summary":"Before moving an item, reads back who gains or loses access, which links break and which settings change, so a private doc doesn't land in a public folder.","server":"Re-check permissions and apply the move server-side; recompute access after it.","catches":["Moves that make an item visible to more people","Moves that remove access for people who had it","Path links that will break, with a keep-redirect option","Inherited settings (retention, sharing) that change at the destination"],"limits":["Viewer counts and settings come from your props; it doesn't query permissions","Direct shares on the item are only as accurate as your data"],"a11y":["Form labelled by its title; paths are labelled ordered lists","Impact list is a polite live region; the arrow between states has an accessible name","Highlight animation is skipped under reduced motion"]}},{"name":"reset-preview","type":"registry:ui","title":"Reset Preview","description":"Reset-to-defaults preview listing only the settings the user changed as current → default, with per-setting keep checkboxes, an export nudge until a copy is saved, and a draining Undo after the reset.","categories":["Destructive actions"],"files":[{"path":"registry/default/reset-preview/reset-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Reset to defaults wiped customizations","mistakes":["reset settings lost my config","restore defaults by mistake","reset preferences undo","lost theme settings"],"aliases":["reset to defaults","restore defaults","settings","preferences","export settings","factory reset","config"],"flows":["account-settings","danger-zone"],"fields":[],"summary":"Shows only the settings a reset would actually change, current → default, so a reset to defaults doesn't wipe customisations unseen.","catches":["Customised settings that a reset would change, grouped by section","Settings to keep, excluded from the reset","No export saved yet, with a one-tap export","Settings already at default, counted rather than listed","Reset regret, with a timed Undo"],"limits":["Undo relies on your onUndo and lasts undoFor ms","Value display uses a simple formatter; pass format for complex values"],"a11y":["Preview is a group labelled by its title; each section has an aria-label","Keep checkboxes have descriptive aria-labels; arrow icon has an aria-label (\"resets to\")","Export warning in a polite live region; reset button sets aria-busy while pending","After reset, a role=status message with Undo"]}},{"name":"replace-file","type":"registry:ui","title":"Replace File","description":"Name-clash resolver for uploads: both copies side by side with thumbnail, size and date, a warning plus one-tap Keep both when an older file would replace a newer one, identical copies spotted, renamed-copy preview, and apply-to-all with counts.","categories":["Destructive actions"],"files":[{"path":"registry/default/replace-file/replace-file.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Older upload overwrote newer file","mistakes":["upload replaced newer file","file name conflict overwrite","keep both files","replace existing file"],"aliases":["file conflict","replace or keep both","upload","duplicate file name","overwrite","dropbox","file manager"],"flows":["forms","danger-zone"],"fields":["file"],"summary":"Helps prevent an upload from overwriting a newer file with an older one by comparing both copies before replacing.","server":"Check the stored file's version or ETag at write time so a stale client choice cannot overwrite a newer upload.","catches":["Older incoming file about to replace a newer existing one","Identical copies, with a one-tap Skip","Newer and larger copy highlighted per clash","Name the kept copy will get (\"report (1).pdf\")","Clashes still undecided before Continue"],"limits":["Compares the dates and sizes you pass; file timestamps can be wrong after copies or downloads","\"Identical\" is inferred from size and date, not a content hash","Does not upload; onContinue receives the choices"],"a11y":["Form labelled by its title; each clash is a radiogroup of native radios named after the file","Warnings and fixes render in polite live regions","Continue is disabled until every clash is decided, with aria-busy while pending","Newer/larger shown in bold text, not colour alone; animation skipped under reduced motion"]}},{"name":"dns-record-guard","type":"registry:ui","title":"DNS Record Guard","description":"Checks a DNS change against the zone: a removed MX, SPF senders dropped or past the 10-lookup limit, a removed DKIM selector (escalated under enforcing DMARC), a CNAME at the root or beside other records and dangling names, then says how long it takes to spread.","categories":["Destructive actions"],"files":[{"path":"registry/default/dns-record-guard/dns-checks.ts","type":"registry:ui"},{"path":"registry/default/dns-record-guard/dns-record-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Deleted MX record stopped company email","mistakes":["dns change broke email","deleted mx record","spf too many lookups","cname at root"],"aliases":["dns","mx record","spf","dkim","dmarc","cname","ttl","cloudflare dns","domain settings","godaddy"],"flows":["devtools","danger-zone"],"fields":["url"],"summary":"Checks a DNS edit or delete against the whole zone before saving, so a removed MX or broken SPF doesn't quietly stop company email.","server":"Validate zone changes at your DNS provider and keep a record of the previous zone so changes can be rolled back.","catches":["Removing MX records that receive company email","SPF senders dropped, or SPF pushed past the 10 DNS-lookup limit","A DKIM selector removed while DMARC enforces","A CNAME at the zone root or beside other records, and names left pointing at nothing","Long TTLs, with \"lower TTL first\" and a countdown until caches clear"],"limits":["Checks only the records you pass; it doesn't query live DNS","Can't know which third-party senders still need SPF or DKIM"],"a11y":["Dialog content is labelled by its title","Findings are aria-live=\"polite\" and say \"Breaks:\" or \"Risk:\" in text","The TTL countdown is aria-live=\"polite\"; its progress bar is aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 7208: SPF (10 DNS-lookup limit)","url":"https://www.rfc-editor.org/rfc/rfc7208"},{"label":"RFC 2181: CNAME may have no other data","url":"https://www.rfc-editor.org/rfc/rfc2181"}],"note":"Informational. Checked the SPF limit of 10 lookup terms and that a CNAME label may hold no other data. DKIM/DMARC escalation rules were not checked against a standard."}}},{"name":"key-rotate","type":"registry:ui","title":"Key Rotate","description":"Rotation panel that keeps old and new API keys valid until a stated time, shows which clients use the new key, flags rare jobs that won't call before the old key expires, and holds an immediate cut-off while the old key is in use unless it leaked.","categories":["Destructive actions"],"files":[{"path":"registry/default/key-rotate/key-rotate.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a key rotation taking production down","mistakes":["rotated api key broke prod","old key revoked too early","clients still using old key","api key rotation"],"aliases":["api key","rotate key","secret rotation","credentials","revoke key","access token","env vars","webhook secret"],"flows":["devtools","danger-zone"],"fields":["secret"],"summary":"Keeps old and new API keys valid during a rotation and shows which clients have switched, so cutting off the old key doesn't take production down.","server":"Enforce key expiry and revocation in the key service, and log last-used times per key so the client list is real.","catches":["Clients still calling with the old key","Rare jobs whose next call lands after the old key expires","Immediate revoke while the old key is still in use","Leaked key: allows an immediate cut-off on purpose"],"limits":["Only knows the clients and last-seen times you pass","Rhythm-based predictions assume jobs keep their schedule","The overlap window is enforced by your key service, not this UI"],"a11y":["Panel labelled by its title; overlap options are native radio inputs with labels","Client status list and warnings are polite live regions with text, not only icons","A blocked revoke is announced with role=alert"]}},{"name":"retention-change","type":"registry:ui","title":"Retention Change","description":"Retention picker that states what a shorter period deletes and when (\"deletes 14,210 events older than 13 Sep, tonight at 02:00\"), shades the doomed days on a chart, counts down while undoing is still free, and labels confirm \"Delete without export\" until exported.","categories":["Destructive actions"],"files":[{"path":"registry/default/retention-change/retention-change.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Shorter retention purged months of data","mistakes":["lowered retention deleted data","data retention change","logs purged after setting change","export before retention"],"aliases":["data retention","retention period","logs","purge","analytics events","compliance","export","ttl"],"flows":["account-settings","danger-zone","devtools"],"fields":["number"],"summary":"Says exactly what a shorter data-retention period will delete and when, and suggests an export before it purges.","server":"Apply the new retention server-side with its own grace period, and make the purge job re-check the setting before deleting.","catches":["Record count and cutoff date a shorter period would delete","When the next purge runs","Roughly how many more records go each night after","Window in which raising it back still loses nothing","Confirming without an export, named on the button"],"limits":["Counts come from the daily history you pass; days are UTC calendar days","The chart is a summary (bars group days) and is hidden from screen readers; the text states the numbers","Does not delete or export anything itself"],"a11y":["Options are native radios in a group labelled by the title","Deletion summary in a polite live region","Chart is aria-hidden; the same facts are in text","Export and save buttons set aria-busy while pending; icons are aria-hidden"]}},{"name":"seat-reduce","type":"registry:ui","title":"Seat Reduce","description":"Seat-reduction flow that asks who goes, suggests unaccepted invites then least-active members (never owners or admins), warns when you pick an admin or someone active today, and shows the new bill, when it starts and what happens to each person's work.","categories":["Destructive actions"],"files":[{"path":"registry/default/seat-reduce/seat-reduce.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Fewer seats locked out an active admin","mistakes":["reducing seats removed users","downgrade seats who loses access","removed admin seat","seat count billing change"],"aliases":["seats","licenses","team plan","billing","downgrade","remove members","per seat pricing"],"flows":["account-settings","checkout"],"fields":["number"],"summary":"Makes lowering a seat count an explicit choice of who loses access, with the new bill and what happens to their work.","server":"Recheck seats, roles and billing server-side; never let a client choice remove the last owner or admin.","catches":["More members than the new seat count, until you choose who goes","Unaccepted invites suggested first, then least-active plain members","Picking an admin, owner or someone active today","New bill and when it starts","What happens to each removed person's work"],"limits":["Activity and work summaries come from the props you pass","Does not change billing; onConfirm receives the seats and who goes"],"a11y":["Section labelled by its title; member checkboxes have <label>s","Seat count input has an aria-label; arrow keys change it and +/- buttons have aria-labels","Selection status and warnings render in polite live regions","Bill highlight animation skipped under reduced motion"]}},{"name":"cooldown-button","type":"registry:ui","title":"Cooldown Button","description":"Escalating cooldowns per attempt (30s, 60s, 120s) with a visible countdown and reason, and a lock once attempts run out. The deadline is kept in sessionStorage, so a refresh in the same tab keeps it; a new tab or cleared storage does not.","categories":["Submits and rate limits"],"files":[{"path":"registry/default/cooldown-button/cooldown-button.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Resend tapped again and again","mistakes":["resend otp spam","user keeps clicking resend","sms bill from resend","refresh bypasses cooldown"],"aliases":["resend otp","resend code","otp","rate limit","countdown timer","sms","magic link","throttle"],"flows":["signup-login","india"],"fields":["code","phone"],"summary":"A button that waits longer after each attempt and shows why, so \"Resend code\" and retries aren't tapped repeatedly by accident.","server":"Enforce rate limits and attempt counts on the server (per account and IP), and make paid actions idempotent.","catches":["Repeated taps on resend or retry, with a growing wait","Unclear waiting, by showing a countdown and the reason","Running out of attempts, with a clear locked message","Clicks before hydration, by staying inert until the saved state is restored"],"limits":["UI only: a new tab, another device or cleared storage bypasses the cooldown","It reduces accidental repeats; it does not rate-limit or prevent double charges on its own"],"a11y":["Stays focusable while waiting (aria-disabled instead of disabled)","aria-busy while pending and aria-describedby to the reason","Becoming ready is announced in an aria-live=\"polite\" region","Errors and the exhausted message use role=\"alert\""]}},{"name":"safe-submit","type":"registry:ui","title":"Safe Submit","description":"Submit button that runs one request at a time, ignores repeat clicks, Enter spam and double-taps, and hands your onSubmit an idempotency key that is reused on retry after a network error or 408/429/5xx so the server can dedupe.","categories":["Submits and rate limits"],"files":[{"path":"registry/default/safe-submit/safe-submit.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent paying twice after a timeout","mistakes":["double charge on double click","duplicate order submitted","form submitted twice","enter key submitted twice"],"aliases":["submit button","idempotency key","double submit","pay now","place order","loading button","debounce","razorpay","react hook form"],"flows":["checkout","forms"],"fields":["amount"],"summary":"Collapses repeated clicks, Enter spam and double-taps into one request and reuses an idempotency key on retry, so your server can dedupe.","server":"Store the idempotency key with the result and return the stored result for repeats; add a unique constraint so duplicate writes fail.","catches":["Repeat clicks and Enter presses while a request is in flight (synchronous guard)","Double-taps right after a request settles (cooldown, default 400 ms)","Ambiguous failures (network, timeout, 408/429/5xx), retried with the same key","Definite failures, which mint a fresh key for the next attempt"],"limits":["Does not guarantee a single charge or record; that requires the server to honour the idempotency key","The key lives in memory: a reload, remount or second tab gets a new key","Classifies retryable errors with a default rule; pass isRetryable for your API"],"a11y":["Uses aria-busy instead of disabled while pending, so the button keeps focus","Pending, success and retry states have visible text labels; icons are aria-hidden","Errors render with role=alert and are linked via aria-describedby"]}},{"name":"bulk-runner","type":"registry:ui","title":"Bulk Runner","description":"Shows live done/failed/remaining counts. Pause and Stop let in-flight items finish, then say exactly what was and wasn't done. Failures are grouped by reason with retry-failed-only, and progress never jumps backwards.","categories":["Submits and rate limits"],"files":[{"path":"registry/default/bulk-runner/bulk-runner.tsx","type":"registry:ui"}],"dependencies":["@number-flow/react","lucide-react","motion"],"meta":{"stops":"Bulk job half done, nobody knows","mistakes":["bulk job failed partially","dont know which items failed","retry only failed","progress bar jumped back"],"aliases":["bulk job","batch progress","progress bar","queue","retry failed","import job","bulk email","background job"],"flows":["devtools","danger-zone"],"fields":["number"],"summary":"Runs a bulk job with live done, failed and remaining counts, so a stopped or failed job never ends in \"some of it happened, probably\".","server":"Make each item operation idempotent and record per-item results on the server, so a retry or a reloaded tab doesn't repeat completed work.","catches":["Unclear state after a stop, by reporting exactly what was done and left untouched","Repeated failures, grouped by reason and sorted by count","Retrying everything, by offering retry of failed items only","Runaway parallelism, via a concurrency limit"],"limits":["Runs in the browser tab: closing or reloading the tab stops the job and loses its progress","Pause and Stop wait for in-flight items; it cannot cancel a request already sent","Retries call your run function again, so it must be safe to repeat"],"a11y":["role=\"progressbar\" with aria-valuenow/max and an aria-valuetext reading done, failed and remaining","Phase changes are announced in an aria-live=\"polite\" paragraph","All controls are native buttons with visible text","Motion respects prefers-reduced-motion"]}},{"name":"spend-cap-button","type":"registry:ui","title":"Spend Cap Button","description":"Buy button that remembers recent purchases of the same item (\"You bought this 3 min ago\") and needs a deliberate second click to buy again, previews the purchase against each cap, and refuses in the UI any spend that would break a cap, with the overage and reset time.","categories":["Submits and rate limits"],"files":[{"path":"registry/default/spend-cap-button/spend-cap-button.tsx","type":"registry:ui"}],"dependencies":["@number-flow/react","lucide-react","motion"],"meta":{"stops":"Same item bought twice by mistake","mistakes":["bought same thing twice","double purchase","overspend budget","spending limit exceeded"],"aliases":["buy now","spending limit","budget","in-app purchase","credits","top-up","wallet","duplicate purchase"],"flows":["checkout"],"fields":["amount"],"summary":"Asks for a second click before buying the same item again and shows each purchase against daily or monthly caps before it goes through.","server":"Enforce spending caps and dedupe purchases (idempotency key, recent-purchase check) on the server.","catches":["A repeat purchase of the same item within duplicateWindow, including a double-click","Purchases that would break a daily or monthly cap, with the overage and reset time","This purchase as a slice of each cap","Held Enter (auto-repeat) firing a second purchase"],"limits":["Caps and recent purchases come from props; another tab or device isn't seen until you update them","The block is UI-only; the purchase API must enforce caps"],"a11y":["aria-disabled when a cap would break, aria-busy while pending, described by a polite hint","Escape disarms the second-click state; held Enter is ignored","Blocked reason uses role=alert","Each cap is a role=meter with value text; animations respect reduced motion"]}},{"name":"scheduled-queue","type":"registry:ui","title":"Scheduled Queue","description":"Queue of scheduled sends that counts down each row, highlights the last-chance window before the real cancel cutoff, offers a one-tap move for sends landing in quiet hours where the audience lives, flags missed sends, and refuses past reschedule times.","categories":["Submits and rate limits"],"files":[{"path":"registry/default/scheduled-queue/scheduled-queue.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Scheduled campaign sent at 3am","mistakes":["scheduled send went out wrong time","missed scheduled post","cant cancel scheduled email","sent during quiet hours"],"aliases":["schedule send","scheduled posts","campaign","newsletter","queue","cron","send later","whatsapp broadcast","timezone"],"flows":["scheduling","messaging"],"fields":["datetime"],"summary":"Keeps scheduled sends visible: live countdowns, a last-chance window before the cancel cutoff, quiet-hour warnings and missed sends flagged.","server":"Enforce the cancel cutoff and reject past send times on the server; the provider's lock is the source of truth.","catches":["Time left before each send, and the last-chance window before cutoff","Sends locked in by the provider, with cancel disabled","Sends landing in quiet hours (default 22:00–07:00) in the audience's zone, with a move-to-9:00 fix","Missed or overdue sends, with Send now","Reschedule times in the past"],"limits":["Quiet hours are judged in the zone you give each item; mixed audiences need splitting","Cutoff and lock state are as you configure them, not read from the provider","Countdowns use the device clock"],"a11y":["Last-chance and locked changes announced in a polite live region, not every tick","Reschedule input has an aria-label, autoFocus, aria-invalid for past times, and Escape to cancel","Errors use role=alert; locked rows disable Cancel/Reschedule with a lock icon and text","Row highlight animation is skipped under reduced motion"]}},{"name":"commit-combobox","type":"registry:ui","title":"Commit Combobox","description":"Unpicked text is matched on blur (an exact match is picked, a misspelling gets a Did you mean, duplicate names ask which), editing after a pick visibly drops it with Undo or Esc, and the stored option is always read back.","categories":["Validators"],"files":[{"path":"registry/default/commit-combobox/commit-combobox.tsx","type":"registry:ui"},{"path":"registry/default/commit-combobox/match.ts","type":"registry:ui"}],"dependencies":["lucide-react","@floating-ui/react","motion"],"meta":{"stops":"Typed text saved instead of a pick","mistakes":["combobox free text saved","autocomplete not selected","select typed value","combobox blur","stale selection autocomplete"],"aliases":["combobox","autocomplete","typeahead","select","searchable dropdown","city picker","react select","cmdk","headless ui"],"flows":["forms"],"fields":["text"],"summary":"A pick-from-list field that never saves text the user only typed, and visibly drops a pick when it is typed over.","catches":["Typed text left without a pick when leaving the field","Misspellings, with a one-tap Did you mean","Two options with the same name, by asking which","A pick silently replaced by later typing"],"limits":["Fuzzy matching is for suggestions only; it never picks a near match on its own","With loadOptions, matching only covers the options returned"],"a11y":["role=\"combobox\" with aria-autocomplete=\"list\", aria-expanded, aria-controls and aria-activedescendant","Arrow keys move through options, Enter picks, Escape closes or undoes","Visible <label> tied to the input","Motion respects prefers-reduced-motion"]}},{"name":"relative-date-input","type":"registry:ui","title":"Relative Date Input","description":"Reads \"next Fri\", \"in 2 weeks\", \"the 15th\" or \"23 Oct\" against today with date-fns. When a phrase can mean two dates (\"next Friday\" on a Wednesday, \"the 10th\" once passed), it shows both and waits for a pick, then offers a rewrite to the explicit date.","categories":["Validators"],"files":[{"path":"registry/default/relative-date-input/relative-date-input.tsx","type":"registry:ui"},{"path":"registry/default/relative-date-input/relative-date.ts","type":"registry:ui"}],"dependencies":["lucide-react","date-fns","motion"],"meta":{"stops":"Next Friday lands on the wrong day","mistakes":["next friday ambiguous","natural language date","relative date parse","in 2 weeks date","this monday which date"],"aliases":["natural language date","relative date","next friday","due date","chrono","date-fns","reminder","task due","smart date"],"flows":["scheduling","forms"],"fields":["datetime"],"summary":"Stops a date typed the way people say it (\"next Fri\", \"the 10th\") from landing on the wrong day by asking when the phrase has two readings.","catches":["Phrases with two honest readings (next/this weekday, next weekend early in the week)","Day-of-month that has already passed (\"the 10th\")","Read-back of the resolved date with its relative distance","One-tap rewrite of the field to an unambiguous date"],"limits":["English phrases only; other languages are not parsed","Uses the today you pass (or the device clock) and its time zone","Covers common everyday phrases, not every natural-language form"],"a11y":["Visible <label> on the input; aria-invalid and status via aria-describedby","Two readings form a roving-tabindex radiogroup operated with arrow keys","Read-back renders in a polite live region","Animations respect reduced motion (useReducedMotion)","After the rewrite, focus returns to the input"]}},{"name":"namesake-select","type":"registry:ui","title":"Namesake Select","description":"Person picker that flags options sharing a name (ignoring case, accents and word order) with only the details that tell them apart, asks for one identifier before a shared name becomes the value, offers a switch when the answer fits the other person, and reads the choice back.","categories":["Validators"],"files":[{"path":"registry/default/namesake-select/namesake-select.tsx","type":"registry:ui"},{"path":"registry/default/namesake-select/namesake.ts","type":"registry:ui"}],"dependencies":["@floating-ui/react","lucide-react","motion"],"meta":{"stops":"Right name picked for wrong person","mistakes":["same name wrong patient","duplicate names select","two people same name","wrong customer selected","namesake picker"],"aliases":["namesake","same name","patient select","customer picker","contact picker","people search","duplicate records","crm","assign"],"flows":["forms"],"fields":["name"],"summary":"Flags options that share a name and asks for one identifier before a shared name is picked, so the right name doesn't go to the wrong person.","server":"Re-check the selected record ID on the server before acting on it (orders, payments, records).","catches":["Two or more people with the same name, however it is typed","The details that tell namesakes apart, shown inline","Picking a shared name: re-entry of one identifier is required (default verify=\"reenter\")","Identifier that matches a different namesake: one-tap switch","Identical records that can't be told apart"],"limits":["Only as good as the identifiers you pass (date of birth, city, phone)","Name matching ignores case, accents, punctuation and word order, but not nicknames or transliteration variants","Re-entry adds a step for every shared name, by design"],"a11y":["Combobox with a visible label, aria-expanded and arrow-key/Enter navigation","Focus moves into the verification card and back to the input","Animations are skipped under reduced motion"]}},{"name":"cheque-check","type":"registry:ui","title":"Cheque Check","description":"Reads the date as written in the CTS boxes (so 31 November is caught), shows the safe deposit window under RBI's three-month rule with a warning on the disputed last day, catches stale January dates and two-digit years, and says when Positive Pay applies.","categories":["Validators"],"files":[{"path":"registry/default/cheque-check/cheque-check.tsx","type":"registry:ui"},{"path":"registry/default/cheque-check/cheque.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Cheque bounced as expired or post-dated","mistakes":["cheque validity 3 months","post dated cheque deposited","cheque date wrong year","cheque rejected correction","positive pay cheque"],"aliases":["cheque","check","rbi 3 month rule","cts cheque","positive pay","cheque date","bank deposit","pdc","indian bank"],"flows":["india","forms"],"fields":["datetime","amount"],"summary":"Reads a cheque date as written and shows the safe deposit window, so a cheque isn't presented after it expires, before its date or with a rejected correction.","catches":["Impossible dates (31 November) and two-digit years","Cheques older than three months, or post-dated","A January cheque still carrying last year's date","Corrections other than to the date, which need a fresh cheque","Amounts where Positive Pay applies (₹50,000 and above)"],"limits":["RBI doesn't define whether the three-month window ends on the same date or the day before; the earlier day is shown as safe","Your bank may make Positive Pay mandatory at a different amount","India rules by default; other countries need custom rules"],"a11y":["Labelled date, amount and correction fields; aria-invalid and aria-describedby to the issues list","Focus moves to the first fix","Issues use icons plus text"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI FAQs on Cheque Clearing (7 Jan 2026)","url":"https://www.rbi.org.in/commonman/Upload/English/FAQs/PDFs/CHEQUE07012026.pdf"},{"label":"RBI notification: validity of cheques reduced to three months (4 Nov 2011)","url":"https://www.rbi.org.in/commonman/english/scripts/Notification.aspx?Id=961"}],"note":"Informational; check with your bank. Checked: three-month validity from the instrument's date; changes to payee or amount need a fresh cheque; Positive Pay enabled for ₹50,000 and above, banks may mandate it from ₹5,00,000."}}},{"name":"kyc-name-match","type":"registry:ui","title":"KYC Name Match","description":"KYC name field that compares the typed name with each document word by word (order, initials, missing middle name, transliterations like Laxmi/Lakshmi), checks the PAN's 4th and 5th letters, flags differing date of birth or gender, and offers each exact name.","categories":["Validators"],"files":[{"path":"registry/default/kyc-name-match/kyc-name-match.tsx","type":"registry:ui"},{"path":"registry/default/kyc-name-match/name-match.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a KYC rejection for a name mismatch","mistakes":["pan aadhaar name mismatch","kyc failed name not matching","middle name missing on pan","spelling different on aadhaar","dob mismatch kyc"],"aliases":["kyc","pan aadhaar link","name as per pan","ekyc","ckyc","pan card","aadhaar","onboarding","uidai","video kyc"],"flows":["india","signup-login"],"fields":["name","id-number"],"summary":"Compares the typed name with each KYC document word by word and checks the PAN letters, so a KYC or PAN–Aadhaar link doesn't fail on a name mismatch.","server":"Run the real name match and KYC verification through your provider on the server, and avoid storing full Aadhaar numbers.","catches":["Words in a different order, initials, or a missing middle name","Transliteration variants such as Laxmi and Lakshmi","PAN 4th letter that doesn't match the holder type (P for an individual)","PAN 5th letter that doesn't match the surname's first letter","Date of birth or gender that differs between documents"],"limits":["Compares the document details you pass; it doesn't read or verify the documents","Transliteration matching covers common variants only","Whether a minor mismatch passes is decided by UIDAI or the KYC provider, not this check"],"a11y":["Visible label; input has aria-invalid on a mismatch and is described by the hint and summary","Each document's use-exact-name button is described by that document's row","Status uses role=status; icons are aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"Income Tax Department: How PAN is formed","url":"https://www.incometaxindia.gov.in/w/how-pan-is-formed-and-how-it-gets-its-unique-identity-"}],"note":"Informational; check the official source. Confirmed: 4th character is the holder status (P individual, C company, etc.), 5th is the first letter of an individual's surname. The UIDAI FAQ on PAN–Aadhaar name, gender and date-of-birth matching was not re-fetched this round."}}},{"name":"aadhaar-input","type":"registry:ui","title":"Aadhaar Input","description":"Masks Aadhaar entry as XXXX XXXX 1234 (reveal on demand), checks the Verhoeff check digit and the no-0-or-1 first digit, recognises a VID, enrolment ID, masked copy, PAN or mobile pasted by mistake, strips photocopy separators and says how the number is stored.","categories":["Validators"],"files":[{"path":"registry/default/aadhaar-input/aadhaar-input.tsx","type":"registry:ui"},{"path":"registry/default/aadhaar-input/aadhaar.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Aadhaar typos and over-exposure","mistakes":["aadhaar number validation","verhoeff check aadhaar","mask aadhaar number","vid pasted instead of aadhaar","invalid aadhaar kyc"],"aliases":["aadhaar","uidai","aadhar number","vid","masked aadhaar","kyc","ekyc","12 digit","verhoeff","indian id"],"flows":["india","signup-login"],"fields":["id-number"],"summary":"An Aadhaar field that masks the number as it is typed and catches typos, pasted VIDs, PANs or mobile numbers before KYC rejects them.","server":"Verify the number through an authorised KYC/eKYC provider on the server, and avoid storing or logging full Aadhaar numbers unless you are permitted to.","catches":["Typos and swapped neighbouring digits, via the Verhoeff check digit","A first digit of 0 or 1, which Aadhaar numbers never use","A 16-digit VID, 28-digit enrolment ID, masked copy, PAN or mobile number pasted by mistake","Spaces, dashes and dots from a photocopy or PDF, and Indian-script or full-width digits","Letters typed for look-alike digits (O, I, l, S)"],"limits":["Format and checksum only: it does not confirm the number exists or belongs to this person","On a failed check digit it offers to reveal the number rather than guessing which digit is wrong","Masking is visual; your form still receives the full number unless you handle it otherwise","The storage note only describes what you pass in `storage`; it does not store or encrypt anything"],"a11y":["Visible <label> tied to the input; aria-invalid and aria-describedby point at the error and storage note","Show/hide is a real button with aria-pressed and an aria-label, and returns focus to the field","Status changes are announced through a role=\"status\" region","Icons are aria-hidden; errors carry text, not only colour"],"rules":{"verified":null,"sources":[{"label":"UIDAI (official site)","url":"https://uidai.gov.in/"}],"note":"Not verified on 2026-10-17: the UIDAI site did not respond. The no-0-or-1 first digit, Verhoeff check digit, 16-digit VID and masked-Aadhaar conventions are unchecked here."}}},{"name":"password-policy-input","type":"registry:ui","title":"Password Policy Input","description":"Password policy editor that checks rules against NIST SP 800-63B-4 (composition rules, forced rotation, security questions, hints, minimum under 15 or 8 with MFA, maximum under 64, blocked paste, no breach check or attempt limit), shows sample passwords and applies defaults.","categories":["Validators"],"files":[{"path":"registry/default/password-policy-input/password-policy-input.tsx","type":"registry:ui"},{"path":"registry/default/password-policy-input/policy.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch password rules that weaken accounts","mistakes":["password complexity rules bad","forced password rotation","nist password policy","max password length too short","block paste password"],"aliases":["password policy","nist 800-63b","admin settings","security settings","mfa","breached passwords","password rules","auth config"],"flows":["account-settings","signup-login"],"fields":["secret","number"],"summary":"Reads an admin's password rules back against NIST SP 800-63B-4 and shows what they do to real passwords, so the rules don't make accounts weaker.","server":"Enforce length, blocklist and attempt limits in your authentication service.","catches":["Composition rules (mixtures of character types), which NIST says SHALL NOT be imposed","Forced periodic rotation","Security questions and password hints","Minimum under 15 (or 8 when only used with MFA), maximum under 64","Paste blocked, no breached-password check, no failed-attempt limit","What the rules accept and refuse, shown on sample passwords"],"limits":["Covers the password sections of the standard, not the whole of SP 800-63B","Doesn't enforce anything; your auth system applies the policy","Informational; check the official source"],"a11y":["Every policy input has a visible label, aria-invalid on SHALL findings and a description","Fix buttons are real buttons; focus moves to the summary after applying defaults","Icons are aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"NIST SP 800-63B-4, Digital Identity Guidelines: Authentication and Authenticator Management","url":"https://pages.nist.gov/800-63-4/sp800-63b.html"}],"note":"Informational; check the official source. Confirmed: minimum 15 for single-factor, 8 when only used with MFA; SHOULD permit at least 64; SHALL NOT impose composition rules, periodic change, hints or KBA; SHOULD permit paste."}}},{"name":"passenger-name-input","type":"registry:ui","title":"Passenger Name Input","description":"Passenger name field that checks against the ID and catches swapped names, a missing middle name, titles, suffixes or nicknames in the name, single-name passengers, accents the ticket can't print and names over the airline's limit, with fixes and a SURNAME/GIVEN TITLE readback.","categories":["Validators"],"files":[{"path":"registry/default/passenger-name-input/passenger-name-input.tsx","type":"registry:ui"},{"path":"registry/default/passenger-name-input/passenger-name.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/name-input.json"],"meta":{"stops":"Boarding denied over a ticket name","mistakes":["passenger name mismatch id","name change fee flight","first last name swapped ticket","single name passenger","name as per passport"],"aliases":["passenger details","flight booking","irctc","train ticket","name as on id","traveller name","pnr","airline","checkout"],"flows":["checkout","forms"],"fields":["name"],"summary":"Checks a flight or train passenger name against the ID and reads it back as it will print, so a name mismatch doesn't cost a fee or a boarding.","catches":["Given name and surname swapped","Middle name on the ID missing from the booking","Title, suffix or nickname typed into a name field","Single-name passengers, ticketed as repeat (JONES/JONES) or FNU","Accents or characters the ticket can't print, and names over maxLength"],"limits":["Airline rules differ; defaults follow the airlines named in the code and should be checked for yours","Comparison with the ID only works when you pass idName","Doesn't change a booking already made"],"a11y":["Title and name inputs have visible labels and are described by the hint and issues","Fix buttons are disabled while the result is stale","Focus moves to the next fix or the given-name field after a fix"],"rules":{"verified":null,"note":"Not verified this round: airline single-name conventions (IndiGo, Air India, IATA AIRIMP FNU) and length limits come from the code's comments. Check with the airline."}}},{"name":"email-change-guard","type":"registry:ui","title":"Email Change Guard","description":"Catches misspelled providers (gmial.con) with a one-tap fix, shows where the confirmation stops matching, spots the same inbox (Gmail dots, +tags), says what moves with the address, and keeps the old address active until the new one is verified, with resend, edit and cancel.","categories":["Validators"],"files":[{"path":"registry/default/email-change-guard/email-change-guard.tsx","type":"registry:ui"},{"path":"registry/default/email-change-guard/email-change.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/email-input.json"],"meta":{"stops":"Locked out by a typo'd new email","mistakes":["change email typo","update email lockout","new email not verified","gmial.com typo","confirm email mismatch"],"aliases":["change email","update email","account settings","profile","email verification","confirm email","login email","2fa"],"flows":["account-settings","danger-zone"],"fields":["email"],"summary":"Catches typos in a new account email and keeps the old address active until the new one is verified, so nobody locks themselves out.","server":"Keep the old address active until the new one is verified, notify the old address of the change, and rate-limit resends on the server.","catches":["Misspelled email providers, with a one-tap fix","The exact point where the confirmation stops matching","A \"new\" address that is the same inbox (Gmail dots, +tags)","What moves with the address: sign-in, receipts and 2FA codes"],"limits":["Domain typo detection uses a list of known providers; custom domains aren't checked","Keeping the old address active depends on your backend honouring the pending state"],"a11y":["Focus moves to the confirmation field, the fix, or the pending notice as the flow advances","Fields are described by their hints via aria-describedby","The pending state has a focusable heading"]}},{"name":"invoice-number-check","type":"registry:ui","title":"Invoice Number Check","description":"GST invoice number field that counts to 16, names characters other than letters, digits, - or /, catches a number already used this financial year (case-insensitively), a stale year token, skipped or reused counters and gaps, and offers the next number.","categories":["Validators"],"files":[{"path":"registry/default/invoice-number-check/invoice-number-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a GST invoice number the rules don't allow","mistakes":["gst invoice number format","invoice number 16 characters","duplicate invoice number","invoice series financial year","rule 46 invoice"],"aliases":["invoice number","gst invoice","bill number","cgst rule 46","e-invoice","billing","invoice series","financial year","tally"],"flows":["india","forms"],"fields":["id-number","text"],"summary":"Checks a GST invoice number against CGST Rule 46(b): 16 characters at most, only letters, digits, - and /, and unique in the financial year.","server":"Allocate invoice numbers on the server with a unique constraint per GSTIN and financial year.","catches":["More than 16 characters","Characters other than letters, digits, hyphen and slash","Number already used this financial year, including case-only differences and cancelled numbers","Financial-year token that doesn't match the invoice date","Skipped or reused counter and gaps in the series"],"limits":["Only checks against the issued list you pass; numbers issued elsewhere are unknown","Case-insensitive duplicate matching mirrors portal behaviour as the authors understood it, not a rule text","Doesn't validate the rest of the invoice (GSTIN, HSN, tax)"],"a11y":["Visible label; input has aria-invalid on errors and is described by the hint","Problems are announced in a polite live region; icons are aria-hidden","Fix and next-number buttons are real buttons; focus returns to the input after applying"],"rules":{"verified":"2026-10-17","sources":[{"label":"CGST Rules, Rule 46(b) (CBIC tax information portal)","url":"https://taxinformation.cbic.gov.in/content/html/tax_repository/gst/rules/cgst_rules/active/chapter6/rule46_v1.00.html"}],"note":"Informational; check the official source. Confirmed: consecutive serial number not exceeding sixteen characters, letters, numerals, - and /, one or multiple series, unique for a financial year. The case-insensitive portal comparison and e-invoice specifics are not from the rule text and were not verified."}}},{"name":"pin-drop-check","type":"registry:ui","title":"Pin Drop Check","description":"Map-pin check that measures the pin against the geocoded address and PIN area (ignoring gaps inside GPS accuracy), flags pins in water or on a highway from your geo helper, draws the gap on a to-scale SVG sketch with no map library, and asks \"Use pin or address?\".","categories":["Validators"],"files":[{"path":"registry/default/pin-drop-check/pin-drop-check.tsx","type":"registry:ui"},{"path":"registry/default/pin-drop-check/pin-geo.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Delivery sent to the wrong door","mistakes":["map pin far from address","delivery location wrong","gps pin mismatch","pin dropped on highway","pin vs typed address"],"aliases":["map pin","delivery location","drop pin","geolocation","address","swiggy","zomato","location picker","gps"],"flows":["checkout","india"],"fields":["address"],"summary":"Measures a dropped map pin against the typed address and PIN area, so a delivery doesn't go to the wrong door.","catches":["Pin far from the typed address","Pin outside the PIN or postcode area","Pin in water or on a highway (from your classify helper)","Gaps smaller than GPS accuracy are ignored"],"limits":["Needs your geocoder for the address and PIN centre","Surface checks depend on your geo helper","A straight-line distance, not the route"],"a11y":["Sketch is role=img with an aria-label describing the gap","Choice group is labelled and described by the status","Loading and result lines have text; icons are aria-hidden"]}},{"name":"pincode-input","type":"registry:ui","title":"PIN Code Input","description":"Indian PIN field that reads the state from the first digits offline and flags a mismatch with the picked state, catches O typed for 0, wrong lengths, unused prefixes, Army Post codes and ZIP or UK postcodes typed for India, then checks the area and city through your lookup.","categories":["Validators"],"files":[{"path":"registry/default/pincode-input/pincode-input.tsx","type":"registry:ui"},{"path":"registry/default/pincode-input/pincode.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Parcel sent to the wrong state","mistakes":["pincode state mismatch","invalid pin code","pincode validation india","6 digit pincode","zip code india"],"aliases":["pincode","pin code","postal code","zip","india post","shipping address","checkout","delivery","state"],"flows":["india","checkout"],"fields":["address","number"],"summary":"Reads the state from an Indian PIN's first digits as you type and flags a mismatch with the chosen state, so a parcel doesn't go to the wrong place.","catches":["PIN prefix that doesn't match the chosen state","Letter O for 0, and digits from other scripts","Wrong length or unused prefixes","Army Post Office codes","US ZIP or UK postcodes typed for India, with a country switch"],"limits":["Prefix-to-state is a table and can't tell every border case without the lookup","Format looks valid is not the same as deliverable","Area and city checks need your lookup"],"a11y":["Visible label; input has aria-invalid when the lookup finds nothing and is described by a polite status","Fixes are buttons; focus returns to the input","Icons are aria-hidden"],"rules":{"verified":null,"note":"Not verified this round: the prefix-to-state table is from India Post via Wikipedia (\"Postal Index Number\"), not a primary India Post source."}}},{"name":"password-hint-guard","type":"registry:ui","title":"Password Hint Guard","description":"Hint or security-answer field that spots the password inside it (also backwards, spaced out or in leetspeak), a large piece of it, or a \"same as my bank\" reuse admission, without displaying the password, and removes the leak in one tap.","categories":["Validators"],"files":[{"path":"registry/default/password-hint-guard/hint-leak.ts","type":"registry:ui"},{"path":"registry/default/password-hint-guard/password-hint-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Password hint reveals the password","mistakes":["password hint contains password","security question answer weak","adobe password hint leak","hint gives password away","password reminder"],"aliases":["password hint","security question","recovery","signup","account security","memorable word","password reminder"],"flows":["signup-login","account-settings"],"fields":["secret","text"],"summary":"Spots the password inside a password hint or security answer, even reversed, spaced out or in leetspeak, without ever showing the password.","server":"Don't store hints next to passwords, and run the same check on the server if hints are kept.","catches":["The password written in the hint","Reversed, spaced-out or leetspeak versions (P@ssw0rd)","A long shared substring of the password","Admissions of reuse such as \"same as my bank\""],"limits":["Compares only with the password in the same form","Can't catch hints that describe the password indirectly (\"my dog's name\")","English reuse phrases"],"a11y":["Visible label; input has aria-invalid for a severe leak and is described by a polite status","Severe leaks use role=alert","Icons are aria-hidden"]}},{"name":"coupon-input","type":"registry:ui","title":"Coupon Input","description":"Cleans pasted spaces, hidden characters and case, catches O-vs-0 look-alikes with a fix, says why a code didn't apply (cart minimum and the gap, expiry, excluded items, already used), shows the real saving and any cap, and asks before a worse code replaces a better one.","categories":["Validators"],"files":[{"path":"registry/default/coupon-input/coupon-input.tsx","type":"registry:ui"},{"path":"registry/default/coupon-input/coupon.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Valid coupon code rejected","mistakes":["coupon code not working","promo code invalid","discount code spaces","coupon minimum order","better coupon replaced"],"aliases":["coupon","promo code","discount code","voucher","offer code","checkout","cart","apply code","gift card"],"flows":["checkout"],"fields":["code"],"summary":"Cleans and checks a coupon code and says exactly why it didn't apply, so shoppers don't give up on a code that should work.","server":"Validate and price coupons on the server; never trust a discount computed in the browser.","catches":["Pasted spaces, hidden characters and wrong case","Look-alike characters (O/0, I/1) against known codes","The specific reason a code was rejected, such as the amount still needed","A capped discount, shown as the real saving","A worse code replacing a better one"],"limits":["Look-alike suggestions need knownCodes; otherwise it only cleans","Validity and rejection reasons come from your onApply"],"a11y":["Visible <label>; aria-invalid and aria-describedby to the status","Enter applies; suggestion and remove buttons have aria-labels","Focus returns to the input after actions"]}},{"name":"paste-target-guard","type":"registry:ui","title":"Paste Target Guard","description":"Payment-target field that compares what lands in it with what was copied on the page or entered earlier, flags look-alikes of saved recipients, and shows both side by side in groups (ends first, differing characters marked) with the original one tap away.","categories":["Validators"],"files":[{"path":"registry/default/paste-target-guard/paste-target-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a clipboard hijacker swapping the payee address","mistakes":["clipboard hijack upi","crypto address changed on paste","iban clipper malware","pasted wrong upi id","wallet address swapped"],"aliases":["clipboard hijacking","clipper","upi id","iban","wallet address","crypto","payee","send money","paste"],"flows":["checkout","india","danger-zone"],"fields":["id-number","text"],"summary":"Compares a pasted IBAN, UPI ID or wallet address with what was copied or expected, so a clipboard hijacker's swapped address is caught before paying.","server":"Confirm the payee with your provider and require step-up confirmation for new or changed recipients on the server.","catches":["Pasted address that differs from what was copied on the page","Address that differs from the expected one","Look-alikes of saved recipients","Matching start and end with a different middle (the clipper trick)"],"limits":["Only sees copies made on this page (watchCopy) or the expected value you pass","Malware that also changes what the page shows can't be detected","Doesn't verify the payee; use Payee Check or your provider"],"a11y":["Visible label; input has aria-invalid on mismatch and is described by the check","Mismatches use role=alert; a match is announced politely","Show-all and keep buttons have aria-labels"]}},{"name":"email-input","type":"registry:ui","title":"Email Input","description":"Spots misspelled domains like gmial.con or rediffmial.com with Damerau-Levenshtein matching and offers a one-tap, never automatic, fix; can suggest matching providers while the domain is being typed.","categories":["Validators"],"files":[{"path":"registry/default/email-input/email-domains.ts","type":"registry:ui"},{"path":"registry/default/email-input/email-input.tsx","type":"registry:ui"}],"dependencies":["@floating-ui/react","lucide-react","motion"],"meta":{"stops":"Signup lost to gmial.con typo","mistakes":["email typo gmail","bounced signup emails","email domain suggestion","mailcheck alternative","rediffmail typo"],"aliases":["email","email field","signup","newsletter","mailcheck","domain suggestion","login","register","react hook form"],"flows":["signup-login","forms"],"fields":["email"],"summary":"Spots misspelled email domains like gmial.con and offers a one-tap fix, never an automatic one, so sign-ups don't bounce.","server":"Verify ownership with a confirmation email before relying on the address.","catches":["Misspelled common domains (gmial.con, rediffmial.com)","Swapped letters, via Damerau-Levenshtein distance","Typos avoided altogether, with provider completions while typing (completeDomains)"],"limits":["Only knows the domains in its list; a typo of an unlisted domain isn't caught","It doesn't check that the mailbox exists"],"a11y":["Visible <label>; hints via aria-describedby","With completions on: role=\"combobox\", a role=\"listbox\", aria-activedescendant and arrow, Enter/Tab and Escape keys","Fix returns focus to the input; motion respects prefers-reduced-motion"]}},{"name":"password-input","type":"registry:ui","title":"Password Input","description":"Password field that warns as it happens about Caps Lock, a non-Latin keyboard layout (from the physical key vs the character typed) and leading or trailing spaces from a paste, with a one-tap trim and a press-and-hold or toggle peek.","categories":["Validators"],"files":[{"path":"registry/default/password-input/password-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Lockout caused by Caps Lock","mistakes":["caps lock warning password","wrong password trailing space","keyboard layout password","show hide password","login fails caps lock"],"aliases":["password","login","sign in","caps lock","show password","password field","auth form","signup"],"flows":["signup-login"],"fields":["secret"],"summary":"Warns about Caps Lock, a non-Latin keyboard layout and pasted spaces as they happen, so a typo doesn't look like a wrong password.","server":"Rate-limit sign-in attempts on the server; these warnings only reduce honest mistakes.","catches":["Caps Lock on","Non-Latin keyboard layout producing other characters","Pasted leading or trailing spaces, with a one-tap trim","Typing blind: peek to check"],"limits":["Caps Lock state is only known after a key event","Doesn't check strength or breaches; see Password Policy Input"],"a11y":["Visible label; input is described by the hint and warnings","Peek button has an aria-label, aria-pressed and title; hold works with Space or Enter","Focus returns to the input after a fix"]}},{"name":"match-field","type":"registry:ui","title":"Match Field","description":"Enter-twice field for passwords and account numbers that shows the position where the confirmation stops matching without revealing either value, and refuses a paste into the confirm box only when it is a copy of the first entry, so password managers keep working.","categories":["Validators"],"files":[{"path":"registry/default/match-field/match-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Mistyped confirm password or account number","mistakes":["confirm password mismatch","re-enter account number","passwords do not match","confirm field paste","bank account confirm"],"aliases":["confirm password","re-enter","confirm account number","verify","signup","bank details","beneficiary","repeat field"],"flows":["signup-login","forms","india"],"fields":["secret","id-number"],"summary":"Shows which character a confirmation stops matching at, without revealing either value, so a mistyped password or account number is caught.","server":"Validate the account number or password again on the server; the match check is client-side only.","catches":["Confirmation that differs from the first entry, with the mismatch position","Paste into the confirm box copied from the first box","Match state reported through onMatchChange"],"limits":["Only compares the two entries; it can't know the first one is what the user meant","Doesn't validate password strength or account-number checksums"],"a11y":["Both inputs have visible labels; the confirm box has aria-invalid on mismatch and is described by the status","Match status and paste notices are polite live regions","The position marker is aria-hidden; the status text says where it differs"]}},{"name":"amount-input","type":"registry:ui","title":"Amount Input","description":"Groups digits live with Intl.NumberFormat (lakh/crore for en-IN), spells the amount cheque-style, and when you pass `typical` warns about an amount `threshold`× (default 10×) above or below it, with a one-tap fix for a zero slip. Reports a number plus the exact decimal and minor-unit strings.","categories":["Validators"],"files":[{"path":"registry/default/amount-input/amount-input.tsx","type":"registry:ui"}],"dependencies":["@number-flow/react","lucide-react","motion"],"meta":{"stops":"Extra zero on a transfer","mistakes":["extra zero payment","amount in words","lakh crore formatting","money input formatting","paid ten times"],"aliases":["amount","money input","currency","rupees","₹","lakh crore","transfer","send money","payment","upi"],"flows":["checkout","india","forms"],"fields":["amount"],"summary":"A money field that groups digits as you type, spells the amount out and flags an amount about ten times off the usual one.","server":"Validate amount and currency on the server, derive payable amounts from trusted data and use idempotency keys; this warning is a review prompt, not a correctness check.","catches":["An extra or missing zero, when the amount is `threshold`× (default 10) or more off the `typical` value you pass, with a one-tap fix that moves the zeros exactly","A paste in another locale's format (\"1.250,50\", \"1.250.000\", \"12,5\" in an en-IN field), by asking which reading you meant","Hard-to-read long numbers, by grouping digits as you type (Indian grouping for en-IN)","Misread amounts, by spelling the amount out in words","Leading zeros, and digits beyond `maxDigits` or the currency's precision, with a visible hint when digits are dropped"],"limits":["The warning needs a `typical` amount; there is no fallback to a balance, limit or last amount","The number passed to onValueChange is a JavaScript float; use the second argument's `decimal` or `minor` string (or the hidden input's decimal string) for exact money maths","The paste check runs only when the paste replaces the whole field, and only for '.' and ',' separators","Default words are English (\"Rupees … only\", \"paise\", English unit names); pass `wordsFormatter` for other languages","No negative amounts","Defaults to en-IN and INR; set locale and currency for other markets"],"a11y":["Visible <label> tied to the input","aria-describedby lists only the words and notice elements that are currently rendered, plus any id you pass","Warnings, the truncation hint and the paste question are in an aria-live=\"polite\" region; the fix and paste buttons return focus to the input","The animated read-back respects prefers-reduced-motion (motion's useReducedMotion)"]}},{"name":"date-sanity-input","type":"registry:ui","title":"Date Sanity Input","description":"Reads the weekday back and catches year typos, past or future dates on the wrong side of today (per `expect`), dates implausibly far away and weekends, each with a one-tap fix.","categories":["Validators"],"files":[{"path":"registry/default/date-sanity-input/date-sanity-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Booked on the wrong date","mistakes":["wrong year typed date","date in the past booking","weekday check date","date of birth future","date typo"],"aliases":["date picker","date input","booking date","dob","date of birth","appointment","travel date","calendar"],"flows":["scheduling","forms"],"fields":["datetime"],"summary":"Reads a date back with its weekday and catches year typos, dates on the wrong side of today and weekends, so bookings aren't made for the wrong day.","catches":["Year typos (2062 for 2026)","A past date where a future one is expected, or the reverse","Dates more than maxYearsAway from today","Weekends, when warnWeekends is on","The weekday, read back so the user can spot a wrong day"],"limits":["Doesn't know holidays or business-specific closed days","\"Today\" is the visitor's clock unless you pass `today`"],"a11y":["Visible <label>; aria-describedby to an aria-live=\"polite\" status","Fix returns focus to the input","Motion respects prefers-reduced-motion"]}},{"name":"clean-paste-input","type":"registry:ui","title":"Clean Paste Input","description":"Strips zero-width, BOM, soft-hyphen and bidi control characters on paste, says what was removed with an undo, and flags any that arrive by typing or autofill with a one-tap fix.","categories":["Validators"],"files":[{"path":"registry/default/clean-paste-input/clean-paste-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pasted code fails from hidden characters","mistakes":["zero width space in paste","copied code not matching","invisible characters input","bom in pasted text","id fails after copy paste"],"aliases":["zero-width","invisible characters","paste","sanitize","bom","voucher code","api key","clean input","trim"],"flows":["forms","devtools"],"fields":["code","text"],"summary":"Strips invisible characters from pasted codes and IDs, so values that look identical don't fail for hidden reasons.","catches":["Zero-width spaces and joiners, and byte-order marks","Soft hyphens","Bidirectional control characters","Hidden characters arriving by other routes than paste, with a fix"],"limits":["Doesn't change visible look-alike characters (Cyrillic а for Latin a)","Removing joiners can change text in scripts or emoji that need them; use undo where that matters"],"a11y":["Visible <label>; the field is described by an aria-live=\"polite\" notice","Undo and dismiss are buttons; dismiss has an aria-label","Actions return focus to the field"]}},{"name":"otp-paste-input","type":"registry:ui","title":"OTP Paste Input","description":"One-time-code field that extracts the code from a pasted SMS or email, skipping phone numbers, amounts, account endings and order numbers (a run near \"code\" or \"OTP\" wins), and accepts digits typed or pasted in any script (full-width, Devanagari, Arabic-Indic).","categories":["Validators"],"files":[{"path":"registry/default/otp-paste-input/otp-paste-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"OTP verification fails on paste","mistakes":["paste sms otp","otp input paste whole message","verification code wrong","otp autofill","devanagari digits otp"],"aliases":["otp","one-time code","verification code","sms code","2fa","pin input","login","mobile verification","autofill"],"flows":["signup-login","india"],"fields":["code"],"summary":"Pulls the right one-time code out of a whole pasted SMS or email, ignoring phone numbers and amounts, and accepts digits in any script.","server":"Verify the code server-side with expiry, attempt limits and rate limiting.","catches":["Whole SMS or email pasted instead of just the code","Phone numbers, amounts, account endings and order numbers mistaken for the code","Full-width, Devanagari or Arabic-Indic digits from other keyboards","Runs of the wrong length"],"limits":["Picks the likeliest code; unusual message formats can still confuse it","Fixed code length (length prop)","Doesn't verify the code; your server does"],"a11y":["Visible label; input is described by a polite notice region","Digit boxes are a visual layer marked aria-hidden over one real input","Notice icons are aria-hidden"]}},{"name":"url-input","type":"registry:ui","title":"URL Input","description":"URL field that adds a missing https://, flags http, Cyrillic or Greek lookalike hostnames (decoding punycode) and a \"paypal.com@\" prefix that hides the real site, fixes typo'd endings like .con, trims sentence leftovers and strips tracking parameters in one tap.","categories":["Validators"],"files":[{"path":"registry/default/url-input/url-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Lookalike or broken link saved","mistakes":["url missing https","phishing lookalike domain","url ends with .con","tracking params in url","http instead of https"],"aliases":["url","link","website field","https","utm strip","homograph","link input","profile link","portfolio"],"flows":["forms","messaging"],"fields":["url"],"summary":"Catches broken, insecure and disguised links: missing https, lookalike hostnames, user@host tricks, typo'd endings and tracking clutter.","server":"Validate and normalise URLs server-side, and check them against allowlists or safe-browsing services where it matters.","catches":["Missing scheme, with https:// added","http links, with a switch to https","Lookalike (homoglyph) hostnames, shown decoded from xn-- punycode","A user@host prefix that disguises the real site","Typo'd top-level domains (.con, .cmo)","Tracking parameters (utm_*, fbclid…), stripped in one tap"],"limits":["Does not fetch the URL or check that it resolves or is safe","Lookalike detection covers common Cyrillic/Greek confusables","The tracking list is a starter set you can extend"],"a11y":["Visible <label>; aria-invalid and hints via aria-describedby","Hints and fixes in a polite live region","Fix buttons have text labels"]}},{"name":"phone-input","type":"registry:ui","title":"Phone Input","description":"Phone field that cleans pasted input (separators, a leading 0, a doubled +91), picks the country from the dial and area code (+1 416 is Canada), warns on wrong digit counts, checks ranges and line types with libphonenumber, and reads back the E.164 value.","categories":["Validators"],"files":[{"path":"registry/default/phone-input/phone-countries.ts","type":"registry:ui"},{"path":"registry/default/phone-input/phone-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react","libphonenumber-js","motion"],"meta":{"stops":"Unreachable phone number saved","mistakes":["indian phone validation","double +91 in number","wrong digit count mobile","landline in otp field","country code phone input"],"aliases":["+91","mobile number","otp","indian phone","phone number","country code","libphonenumber","e.164","whatsapp number","signup"],"flows":["signup-login","india","forms"],"fields":["phone"],"summary":"Cleans pasted phone numbers, picks the country from the dial code and checks the digits against libphonenumber ranges, so an unreachable number isn't saved.","catches":["Separators, a leading trunk 0 and doubled country codes in pasted numbers","Country picked from the dial code and NANP area code","Too few or too many digits for the country","Line type that doesn't fit (landline or premium-rate where a mobile is expected)","E.164 readback of exactly what will be saved"],"limits":["Format looks valid is not the same as reachable or owned; verify with an OTP","Numbering data comes from libphonenumber and needs updates as plans change","Country names are English unless you pass locale"],"a11y":["Visible label; country picker has an aria-label","Input has aria-invalid on a warning and is described by a polite hint region","Flag and dial code visuals are aria-hidden","Animations are skipped under reduced motion"]}},{"name":"char-budget","type":"registry:ui","title":"Character Budget","description":"Counts graphemes against several limits at once (X, SMS segments, LinkedIn, meta description), names the curly quote or emoji that forced UCS-2 SMS encoding, and can swap them for plain characters in one tap.","categories":["Validators"],"files":[{"path":"registry/default/char-budget/char-budget.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Post cut off or SMS costs double","mistakes":["character limit tweet","sms emoji costs double","ucs-2 sms","meta description length","character counter"],"aliases":["character count","char limit","tweet","x post","sms","linkedin","meta description","textarea","counter"],"flows":["messaging","forms"],"fields":["text"],"summary":"Counts text against X, SMS, LinkedIn and meta-description limits at once, so posts aren't cut off and SMS don't silently cost double.","catches":["Text over each platform's limit, counted as people see characters (emoji and flags count once)","X's weighting (URLs as 23, emoji and CJK as 2)","Characters that force UCS-2 and shrink an SMS segment from 160 to 70","Extra SMS segments"],"limits":["Platform limits are built in and can change; pass your own targets to override","Falls back to code points where Intl.Segmenter is unavailable","Real SMS cost also depends on the carrier and gateway"],"a11y":["Visible <label>; the field is described by an aria-live=\"polite\" summary","Buttons that find a problem character have aria-labels naming it","Finding a character moves focus to it in the field"]}},{"name":"code-input","type":"registry:ui","title":"Code Input","description":"Disables autocorrect, autocapitalise and spellcheck, catches smart quotes, en/em dashes, ellipses and hidden characters by line and column with a one-tap fix, and can validate JSON.","categories":["Validators"],"files":[{"path":"registry/default/code-input/code-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Smart quotes break a pasted command","mistakes":["smart quotes in code","autocorrect breaks command","curly quotes json","em dash in cli flag","hidden characters in code"],"aliases":["code input","command","terminal","json","smart quotes","autocorrect off","api key","snippet","monospace"],"flows":["devtools"],"fields":["code"],"summary":"A code field that turns off autocorrect and catches smart quotes, long dashes and hidden characters, so pasted commands and JSON don't fail invisibly.","catches":["Curly quotes that break shell commands and JSON","En/em dashes where -- or - was meant (—force → --force)","Ellipsis characters and invisible characters","Invalid JSON, with the parse error (json prop)"],"limits":["Fixes are character substitutions; it doesn't check that the command or config is right","Intentional typographic characters inside strings may be flagged"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" hint","Each issue lists its line and column in text","Fix button has visible text"]}},{"name":"inline-edit","type":"registry:ui","title":"Inline Edit","description":"Click-to-edit text: a cancelled edit can be restored, a failed save keeps your draft, and a value changed by someone else mid-edit is flagged instead of silently overwritten (clicking away then keeps the editor open rather than saving).","categories":["Validators"],"files":[{"path":"registry/default/inline-edit/inline-edit.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Edit lost or overwritten silently","mistakes":["click to edit lost changes","inline edit save failed","concurrent edit overwrite","cancel edit undo","editable text"],"aliases":["inline edit","click to edit","editable","contenteditable","rename","table cell edit","optimistic save","kanban title"],"flows":["forms"],"fields":["text"],"summary":"Click-to-edit text that keeps your draft when a save fails or you cancel, and warns when someone else changed the value mid-edit.","catches":["Edit thrown away by Escape: a Restore button brings it back","Save failure: the draft is kept and the error shown","Value changed elsewhere while you edit: conflict shown with Use theirs","Validation errors block saving"],"limits":["Conflict detection compares the value prop; it needs your app to push remote changes into it","Not a merge tool: you choose one version","No offline queue for failed saves"],"a11y":["The read view is a button whose aria-label names the field, current value and Edit","Input has an aria-label, aria-invalid on error and a hint in a polite live region","Enter saves and Escape cancels; focus returns to the trigger after closing","Saving spinner has an aria-label"]}},{"name":"checksum-input","type":"registry:ui","title":"Checksum Input","description":"Checks mod-97 (IBAN), Luhn (card) and ISBN check digits, IBAN country lengths and IFSC format as you type, then highlights the one swapped or doubled character that explains a failure instead of guessing a digit.","categories":["Validators"],"files":[{"path":"registry/default/checksum-input/checksum-input.tsx","type":"registry:ui"},{"path":"registry/default/checksum-input/checksum.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Money sent to a mistyped account","mistakes":["iban validation","luhn check card number","isbn check digit","ifsc validation","account number typo"],"aliases":["iban","card number","luhn","isbn","ifsc","mod 97","check digit","bank details","payment"],"flows":["checkout","forms","india"],"fields":["id-number"],"summary":"Checks IBAN, card and ISBN check digits as you type and points to the swapped or doubled character, so money and orders don't go to a mistyped number.","server":"Validate the account or card with the bank or payment processor before moving money.","catches":["IBAN mod-97 failures and wrong length for the country","Card numbers failing Luhn or with a wrong length for the brand","ISBN-10/13 check digit failures","IFSC codes in the wrong format, with known bank codes read back","The swapped or doubled character that explains a failure"],"limits":["A passing check digit means the number is well-formed, not that the account, card or book exists","IFSC has no check digit: only format and a few bank codes are checked","The IBAN length table needs updating when countries join the registry"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" status","Fix returns focus to the input","Grouped read-back is aria-hidden; status text carries the result"]}},{"name":"name-input","type":"registry:ui","title":"Name Input","description":"Name field that spots caps lock, all-lowercase, stray digits, doubled spaces and a first/last mix-up, and offers, never forces, a fix cased the way McDonald, O'Brien, van der Berg and de la Cruz are really written, with a Keep option.","categories":["Validators"],"files":[{"path":"registry/default/name-input/name-case.ts","type":"registry:ui"},{"path":"registry/default/name-input/name-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Names saved in caps or swapped","mistakes":["name all caps","first last name swapped","name capitalization","mcdonald o'brien casing","name validation"],"aliases":["name","full name","first name","last name","signup","profile","title case","customer name","form field"],"flows":["signup-login","forms"],"fields":["name"],"summary":"Spots caps lock, all-lowercase, stray digits and swapped first/last names, and offers (never forces) a correctly cased fix.","catches":["ALL CAPS or all-lowercase names","Digits and doubled spaces","First and last names swapped (from common-name lists)","A full name typed into one box: offers to split","Re-casing that respects Mc/Mac, O', particles and hyphens"],"limits":["Casing and swap lists are English/Western-centric; they can be wrong for other naming traditions","Only suggests; some people really write their name in capitals or lowercase","Mac names are a list, so unlisted ones aren't re-cased"],"a11y":["Visible label; input is described by a polite hint region","Warnings appear after a pause while typing","Keep button has an aria-label and title; icons are aria-hidden"]}},{"name":"near-duplicate-input","type":"registry:ui","title":"Near-Duplicate Input","description":"Create field that spots near-duplicates as you type (capitals, spacing, punctuation, plurals, one-letter typos, never different numbers like Q3 vs Q4) and offers Use existing, which is also what Enter does.","categories":["Validators"],"files":[{"path":"registry/default/near-duplicate-input/near-duplicate-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Duplicate tags with different spelling","mistakes":["duplicate tags case","marketing vs marketings","create tag duplicate","similar category exists","dedupe labels"],"aliases":["tags","labels","categories","create new","dedupe","folder name","tag input","taxonomy"],"flows":["forms"],"fields":["text"],"summary":"Spots near-duplicates of existing names as you type, so a second \"Marketing\" tag or folder isn't created by accident.","catches":["Same name in different capitals or spacing","Punctuation differences","Plural vs singular (Bugs vs bug)","One-letter typos and swapped letters","Leaves different numbers alone (Q3 vs Q4)"],"limits":["Compares against the items you pass; it doesn't search your database","Synonyms (Sales vs Revenue) aren't detected","English plural rules only"],"a11y":["Visible label; input is described by a polite hint region","Enter picks the existing item when a near-duplicate is shown","Use existing button's aria-label names the item; create shows aria-busy while pending"]}},{"name":"slug-input","type":"registry:ui","title":"Slug Input","description":"Slug field that follows the title with proper transliteration (Straße → strasse) until published, reads back the full URL, checks collisions as you type with the next free slug one tap away, and warns that editing a live slug breaks links, keeping a redirect by default.","categories":["Validators"],"files":[{"path":"registry/default/slug-input/slug-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Live URL slug breaks links","mistakes":["slug collision","change slug broke links","url slug generator","transliterate slug","duplicate slug"],"aliases":["slug","permalink","url slug","blog post","cms","seo url","handle","redirect","path"],"flows":["forms"],"fields":["url","text"],"summary":"Keeps URL slugs clean and stable: proper transliteration, a full-URL readback, collision checks, and a warning before editing a live slug.","server":"Enforce slug uniqueness with a database constraint and create the old→new redirect server-side.","catches":["Lossy slugify (ß, é and similar), by transliterating first","Slugs already taken, with the next free one suggested","Edits to a published slug that would break links","Removing the redirect from the old slug","Slugs too long, cut at a word boundary"],"limits":["Collision check depends on your checkAvailability","Transliteration covers Latin-script letters; other scripts are dropped","Creating the redirect is your server's job"],"a11y":["Visible <label> on the input; status via aria-describedby","Availability and warnings render in a polite live region","Keep-redirect is a labelled checkbox; icons are aria-hidden"]}},{"name":"homoglyph-guard","type":"registry:ui","title":"Homoglyph Guard","description":"Username and domain field that detects look-alike, mixed-script, styled (math bold, fullwidth) and zero-width characters following UTS #39 ideas, names each character's script and code point, flags imitations of protected names, and offers the plain-ASCII version in one tap.","categories":["Validators"],"files":[{"path":"registry/default/homoglyph-guard/homoglyph-guard.tsx","type":"registry:ui"},{"path":"registry/default/homoglyph-guard/homoglyph.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a spoofed username with Cyrillic letters","mistakes":["cyrillic lookalike domain","homograph attack","fake username lookalike","mixed script username","unicode spoofing"],"aliases":["homoglyph","homograph","lookalike","unicode spoofing","username","domain","punycode","impersonation","confusables"],"flows":["signup-login","devtools"],"fields":["text","url"],"summary":"Flags look-alike, mixed-script, styled and invisible characters in usernames and domains, such as pаypal.com with a Cyrillic а.","server":"Normalise and run confusable checks on the server at registration and before display; client-side checks are easy to bypass.","catches":["Cyrillic, Greek and other cross-script look-alikes mixed into Latin text","Styled Unicode letters that fold to ASCII (fullwidth, mathematical)","Zero-width and other invisible characters","Imitations of names you protect, such as paypa1 or rnicrosoft"],"limits":["Uses a curated look-alike table, not the full Unicode confusables.txt; rarer confusables are missed","Legitimate non-Latin names can be flagged as mixed-script","Advisory only; it does not reserve or block names"],"a11y":["Visible label tied to the input, described by the report","Report is a polite live region; flagged characters are <mark> elements with their Unicode name as title","Fix button's aria-label includes the ASCII value it will set"]}},{"name":"contrast-picker","type":"registry:ui","title":"Contrast Picker","description":"Shows the WCAG 2 contrast ratio truncated, never rounded up (#777 on white is 4.47:1 and fails), with AA/AAA marks for normal and large text, and a one-tap nearest passing colour that changes only lightness so the hue stays on brand.","categories":["Validators"],"files":[{"path":"registry/default/contrast-picker/contrast-picker.tsx","type":"registry:ui"},{"path":"registry/default/contrast-picker/contrast.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Unreadable text colour shipped","mistakes":["color contrast fails wcag","contrast ratio checker","text not readable","accessibility color","aa contrast"],"aliases":["contrast","wcag","color picker","accessibility","a11y","aa aaa","theme","brand color","design tokens"],"flows":["devtools","forms"],"fields":["code"],"summary":"Shows the WCAG contrast ratio of a text and background pair, never rounded up, and offers the nearest passing colour, so unreadable text doesn't ship.","catches":["Ratios just under a threshold that rounding would hide (4.499 shown as 4.49, a fail)","Failures for normal text versus large text, at AA and AAA","A nearest passing colour that keeps the hue"],"limits":["Uses the WCAG 2 formula; it is not an APCA (WCAG 3 draft) checker","Checks solid colours only, not text over images, gradients or transparency","Readability also depends on font weight and size, which it does not measure"],"a11y":["Labelled colour fields with aria-invalid on unreadable values; swatches have aria-labels","The ratio has an aria-label (\"Contrast ratio 4.47 to 1\")","Pass/fail is written in words (\"fails, needs 4.5:1\") plus icons","Warnings are aria-live=\"polite\""],"rules":{"verified":"2026-10-17","sources":[{"label":"WCAG 2.2: 1.4.3 Contrast (Minimum) and 1.4.6 Contrast (Enhanced)","url":"https://www.w3.org/TR/WCAG22/"}],"note":"Informational. Checked thresholds: AA 4.5:1 normal / 3:1 large; AAA 7:1 normal / 4.5:1 large."}}},{"name":"address-check","type":"registry:ui","title":"Address Check","description":"Catches a missing flat or house number, a postcode that doesn't fit the country (or fits another one), a city the postcode lookup places elsewhere and PO boxes couriers can't reach, with one-tap fixes and a live readback of the printed label.","categories":["Validators"],"files":[{"path":"registry/default/address-check/address-check.tsx","type":"registry:ui"},{"path":"registry/default/address-check/address-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Parcel returned for bad address","mistakes":["missing flat number address","postcode wrong country","po box courier","address validation","shipping address error"],"aliases":["address","shipping address","delivery address","postcode","zip","checkout","address form","house number","billing address"],"flows":["checkout","forms"],"fields":["address"],"summary":"Checks a shipping address for a missing house number, a postcode that doesn't fit the country or city, and PO boxes, so parcels aren't returned.","catches":["No flat or house number in the street line","Postcodes in the wrong shape for the country, or that fit another country","A city that differs from where your postcode lookup places it","PO boxes that couriers can't deliver to","Postcode formatting fixed to the canonical shape (e.g. SW1A 1AA)"],"limits":["Postcode formats are built in per country; it doesn't confirm the address exists","The city check needs a lookupPostcode function from you"],"a11y":["Each field has a hint in an aria-live=\"polite\" region","Fixes and dismissals are buttons with visible text","The highlight animation respects prefers-reduced-motion"]}},{"name":"tax-id-input","type":"registry:ui","title":"Tax ID Input","description":"Tax number field for GSTIN, PAN and EU VAT that recomputes the GSTIN's mod-36 check character, reads back state, holder type and country, and pinpoints the wrong character with a one-tap fix for look-alikes, swaps and GR instead of EL.","categories":["Validators"],"files":[{"path":"registry/default/tax-id-input/tax-id-input.tsx","type":"registry:ui"},{"path":"registry/default/tax-id-input/tax-id.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Invoice sent with a mistyped GSTIN","mistakes":["gstin validation","pan number format","vat number check","gst number typo","gstin checksum"],"aliases":["gstin","gst number","pan","vat number","tax id","invoice","billing","b2b checkout","company details"],"flows":["india","checkout","forms"],"fields":["id-number"],"summary":"Catches mistyped GSTIN, PAN and EU VAT numbers by checking their structure and the GSTIN check character, and pointing at the wrong character.","server":"Verify GSTIN/VAT registration with the official service server-side before issuing tax invoices.","catches":["GSTIN check character that doesn't match (mod-36)","Look-alike characters (O/0, I/1) and swapped neighbours","State code, PAN holder type and EU country read back","EU VAT body shape per country, and GR typed instead of EL","Wrong length for the chosen kind"],"limits":["Format looks valid ≠ registered: it does not confirm the number exists or is active","Use the GST portal or VIES to verify registration","EU VAT patterns check structure, not national check digits"],"a11y":["Visible <label>; aria-invalid with status via aria-describedby","Status and fixes in a polite live region","After a fix, focus returns to the input","External verify link has visible text"],"rules":{"verified":null,"note":"GSTIN check-character algorithm, GST state codes, PAN 4th-character holder types and EU VAT body patterns were not re-verified against a primary source this round. Informational; verify registration with the GST portal or VIES."}}},{"name":"filename-input","type":"registry:ui","title":"Filename Input","description":"Selects only the base name on focus, warns with a one-tap undo when the extension changes or disappears, and catches illegal characters, Windows reserved names (CON, NUL.txt), trailing dots and spaces, over-long names and duplicates differing only in case.","categories":["Validators"],"files":[{"path":"registry/default/filename-input/filename-input.tsx","type":"registry:ui"},{"path":"registry/default/filename-input/filename.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Rename strips the file extension","mistakes":["rename removes extension","invalid filename characters","windows reserved filename","con nul file name","filename too long"],"aliases":["filename","rename","file name","extension","upload","file manager","save as","download name"],"flows":["forms"],"fields":["file","text"],"summary":"Selects only the base name when renaming and catches lost extensions and names Windows can't store, so a rename doesn't break a file.","catches":["Extensions changed or removed, with undo","Characters Windows forbids","Reserved device names such as CON or NUL.txt","Trailing dots and spaces, and names over the byte limit","Names that differ from an existing file only in capitals"],"limits":["Follows Windows naming rules, the strictest common case; other systems allow more","Duplicate checks only cover the existingNames you pass"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" status","Escape restores the original name","Fixes return focus to the input"],"rules":{"verified":"2026-10-17","sources":[{"label":"Microsoft Learn: Naming Files, Paths, and Namespaces","url":"https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file"}],"note":"Informational. Checked the reserved device names (including superscript-digit COM/LPT) and the rule against trailing spaces and periods."}}},{"name":"coordinate-input","type":"registry:ui","title":"Coordinate Input","description":"Reads decimal, DMS and pasted map links (taking Google's pin, not the map centre), catches swapped latitude and longitude, flipped west/south signs against an expected area and 0, 0 defaults, and reads each point back with an OpenStreetMap link.","categories":["Validators"],"files":[{"path":"registry/default/coordinate-input/coordinate-input.tsx","type":"registry:ui"},{"path":"registry/default/coordinate-input/coordinates.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pin lands on the wrong continent","mistakes":["lat long swapped","coordinates wrong sign","google maps link coordinates","dms to decimal","0,0 coordinates"],"aliases":["latitude longitude","lat lng","coordinates","gps","google maps link","dms","location","geo","map pin"],"flows":["forms"],"fields":["address","number"],"summary":"Reads coordinates in any common format and catches swapped or sign-flipped values, so a pin doesn't land on the wrong continent.","catches":["Latitude and longitude swapped","West or south signs flipped, compared with the expected area","0, 0 left as a default","Map links pasted in, using the pin rather than the map centre","DMS and decimal formats, read back as one point"],"limits":["Sign and swap checks need an expectedArea to compare against","Assumes WGS 84 latitude/longitude; projected coordinates are not supported","The OpenStreetMap link opens an external site"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" status","Fix buttons have visible text","Icons are aria-hidden; the point is read back in text"]}},{"name":"zero-result-rescue","type":"registry:ui","title":"Zero Result Rescue","description":"Empty-search helper that explains what got in the way and offers only fixes that return results: did-you-mean from your vocabulary, the query retyped in the right layout or without Caps Lock, dropping the filter that hides everything, or all categories, each with a count.","categories":["Validators"],"files":[{"path":"registry/default/zero-result-rescue/rescue.ts","type":"registry:ui"},{"path":"registry/default/zero-result-rescue/zero-result-rescue.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Users quit on empty search results","mistakes":["no results found search","search empty state","did you mean search","filters hide all results","wrong keyboard layout search"],"aliases":["no results","empty state","search","did you mean","zero results","filters","search page","ecommerce search"],"flows":["forms","checkout"],"fields":["text"],"summary":"Turns an empty search into fixes that are proven to return results: a spelling fix, a layout fix, a dropped filter or all categories.","catches":["Misspellings against your own vocabulary","Wrong keyboard layout and Caps Lock","The single filter that hides every result","A category scope that's too narrow","Only fixes with a non-zero count are shown"],"limits":["Counts come from your count function; slow APIs delay suggestions","Did-you-mean is limited to the vocabulary you pass"],"a11y":["Region is role=status with aria-busy while counting","Fix buttons include their reason and count as text","Icons are aria-hidden"]}},{"name":"mixed-value-field","type":"registry:ui","title":"Mixed Value Field","description":"Bulk-edit field that shows \"Mixed (3 values)\" with the distribution instead of a blank or the first record's value, leaves each record alone unless you pick a value, and reads back exactly what saving changes, counting only records that actually change.","categories":["Validators"],"files":[{"path":"registry/default/mixed-value-field/mixed-value-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bulk edit overwrites every record","mistakes":["bulk edit overwrite","mixed values multi select","edit multiple records","batch update field","bulk edit blank"],"aliases":["bulk edit","batch edit","mixed values","multi select","table actions","admin","multiple records","mass update"],"flows":["forms","danger-zone"],"fields":["text"],"summary":"In a bulk edit, shows \"Mixed (3 values)\" with the spread instead of a blank, so saving doesn't silently overwrite every record.","catches":["Mixed values hidden behind a blank or the first record's value","Saving overwrites records nobody meant to touch: nothing changes unless a value is picked","Count of records that would really change, skipping those that already match","Clearing a field on every record: explicit Clear button"],"limits":["Works on one field at a time","The save itself is yours; it only reports the planned change"],"a11y":["Visible label; the control is described by the readback","Clear uses aria-pressed; Revert is a button","The distribution bar is aria-hidden and summarised in text"]}},{"name":"card-expiry-input","type":"registry:ui","title":"Card Expiry Input","description":"Masks MM/YY, reads the date back as valid through the month's last day, and catches month 13, the year typed first, a four-digit year, an expired card and one that runs out before the next renewal, with one-tap fixes.","categories":["Validators"],"files":[{"path":"registry/default/card-expiry-input/card-expiry-input.tsx","type":"registry:ui"},{"path":"registry/default/card-expiry-input/card-expiry.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Payment declined on card expiry","mistakes":["card expiry format mm yy","expired card checkout","expiry date input","card declined expiry","year typed first"],"aliases":["card expiry","expiry date","mm/yy","credit card","debit card","checkout","payment form","valid thru","billing"],"flows":["checkout"],"fields":["datetime"],"summary":"Masks a card expiry as MM/YY and reads it back as a date, catching month 13, a year typed first or a card that expires before the next renewal.","catches":["Impossible months (13, 00) and the year typed first","A four-digit year, and YYYY-MM autofill","An expired card","A card that expires before the next renewal date you pass","Expiries implausibly far in the future (maxYears)"],"limits":["Checks the date only; the issuer decides whether the card is valid","\"Today\" is the visitor's clock unless you pass `today`"],"a11y":["Visible <label>; aria-invalid when invalid and aria-describedby to an aria-live=\"polite\" status","Errors use role=\"alert\"","Fix buttons have visible text"]}},{"name":"username-input","type":"registry:ui","title":"Username Input","description":"Username field that reads back the exact stored handle, blocks reserved words even in disguise (adm1n) and case-only duplicates, flags lookalikes of existing names with a UTS #39-style skeleton, and checks availability after a pause with free alternatives.","categories":["Validators"],"files":[{"path":"registry/default/username-input/username-input.tsx","type":"registry:ui"},{"path":"registry/default/username-input/username.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Handle impersonates someone else","mistakes":["username already taken","reserved username admin","username lookalike","case duplicate username","handle availability"],"aliases":["username","handle","@handle","signup","profile url","availability check","reserved names","display name"],"flows":["signup-login","account-settings"],"fields":["text","name"],"summary":"Reads back the exact handle that will be registered and blocks reserved words, case-only duplicates and lookalikes of existing names.","server":"Enforce uniqueness on the normalised, skeleton form of the handle with a database constraint.","catches":["Reserved and staff words, including digit swaps (adm1n)","Case-only duplicates of existing handles","Lookalikes of existing names (Cyrillic/Greek homoglyphs, 0/o, 1/l)","Length outside minLength–maxLength","Taken handles, with free alternatives"],"limits":["Skeleton folding covers common confusables, not the full UTS #39 data","Availability depends on your checkAvailability; race conditions remain until the server reserves the name"],"a11y":["Visible <label>; aria-invalid on blocking issues and status via aria-describedby","Status in a polite live region; blocking errors use role=alert","Suggestions are buttons"]}},{"name":"ifsc-input","type":"registry:ui","title":"IFSC Input","description":"IFSC field that catches the letter O typed for the fixed zero, a missing 0, digits in the bank code and guessed prefixes (AXIS for UTIB), then reads the branch back from your lookup (Razorpay IFSC API shape) and warns when it belongs to a different bank than the one picked.","categories":["Validators"],"files":[{"path":"registry/default/ifsc-input/ifsc-input.tsx","type":"registry:ui"},{"path":"registry/default/ifsc-input/ifsc.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a transfer to the wrong branch","mistakes":["ifsc code validation","ifsc letter o vs zero","wrong ifsc bank","ifsc lookup branch","neft ifsc error"],"aliases":["ifsc","ifsc code","bank branch","neft","rtgs","imps","beneficiary","bank details","razorpay ifsc","payout"],"flows":["india","checkout"],"fields":["id-number"],"summary":"Catches IFSC typos like the letter O for the fixed zero, then reads the branch back so a transfer doesn't go to the wrong bank or branch.","server":"Re-validate the IFSC and run payee name verification on the server before initiating the transfer.","catches":["Letter O typed for the fifth character, which is always 0","Missing 0, wrong length, or digits in the 4-letter bank code","Prefixes guessed from the bank name (AXIS instead of UTIB)","Branch from the lookup belonging to a different bank than the one chosen","Branch that doesn't support a required rail (requiredRail)"],"limits":["Format looks valid is not the same as verified; only your lookup confirms the branch exists","Built-in bank prefix list is partial","Doesn't check the account number or the payee name"],"a11y":["Visible label tied to the input, described by a polite status region","aria-invalid when the lookup finds no branch","Lookup failures use role=alert with a retry button","Icons are aria-hidden; warnings are text"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI NEFT FAQ: IFSC is 11 characters, first 4 alpha bank code, 5th is 0, last 6 the branch","url":"https://www.rbi.org.in/Scripts/FAQView.aspx?Id=60"}],"note":"Informational; check the official source. Format rule confirmed. The guessed-prefix list and the Razorpay API shape were not re-verified."}}},{"name":"upi-id-input","type":"registry:ui","title":"UPI ID Input","description":"UPI ID field that cleans pasted upi://pay links and catches emails, typo'd handles (@okaxsi), app names as handles (@gpay), wrong-length or +91 phone IDs and older @paytm IDs, then reads back the registered name from your check and flags a mismatch.","categories":["Validators"],"files":[{"path":"registry/default/upi-id-input/upi-id-input.tsx","type":"registry:ui"},{"path":"registry/default/upi-id-input/upi.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"UPI payment to the wrong VPA","mistakes":["upi id validation","wrong upi handle","vpa typo","upi name mismatch","paytm handle retired"],"aliases":["upi","upi id","vpa","gpay","phonepe","paytm","upi://pay","payout","send money","@okaxis"],"flows":["india","checkout"],"fields":["id-number"],"summary":"Catches mistyped UPI IDs, cleans pasted upi://pay links and reads back the registered name, so money isn't sent to the wrong VPA.","server":"Resolve the payee name server-side through your PSP before payment, and show it from that response, not from the link.","catches":["Pasted upi://pay links, reduced to the pa address","Email addresses typed as UPI IDs","Typo'd handles, with one-tap corrections","App names typed as handles (@gpay → its real handles)","Phone-number IDs with the wrong length or a +91 prefix","Registered name that differs from the expected or link-claimed name"],"limits":["Format looks valid ≠ exists: only your verify callback can confirm the ID and its registered name","Handle list is common handles, not NPCI's full list","Name matching is approximate"],"a11y":["Visible <label>; aria-invalid and status via aria-describedby","Status and suggestions in a polite live region","Handle suggestions are buttons with aria-labels"]}},{"name":"shortcut-recorder","type":"registry:ui","title":"Shortcut Recorder","description":"Shortcut recorder that checks the combo against browser and OS reserved keys on every platform (Ctrl+W, ⌘Q, Ctrl+T), the app's own bindings and keys that need AltGr on some layouts, then reads it back as ⌘ on Mac and Ctrl on Windows/Linux.","categories":["Validators"],"files":[{"path":"registry/default/shortcut-recorder/shortcut-recorder.tsx","type":"registry:ui"},{"path":"registry/default/shortcut-recorder/shortcut-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Shortcut clashes with Ctrl+W","mistakes":["keyboard shortcut conflict","hotkey recorder","reserved browser shortcut","ctrl vs cmd shortcut","custom keybinding"],"aliases":["keyboard shortcut","hotkey","keybinding","shortcut recorder","cmd k","settings","accessibility","mac windows"],"flows":["account-settings","devtools"],"fields":["text"],"summary":"Checks a newly recorded keyboard shortcut against browser and OS reserved keys and the app's own bindings before it's saved.","catches":["Combos the browser or OS keeps for itself, blocked from saving","Clashes with the app's existing bindings, naming the action","Keys that need AltGr on some keyboard layouts","Read-back for both Mac and Windows/Linux"],"limits":["The reserved list covers common browsers and OSes; extensions and custom OS shortcuts are unknown","Layout warnings are heuristic"],"a11y":["Recorder is a button with aria-pressed while recording, labelled and described by its status","Tab or Escape (with no modifiers) stops recording; Backspace/Delete clears","Status in a polite live region; blocking issues use role=alert","Clear button has an aria-label"]}},{"name":"hashtag-input","type":"registry:ui","title":"Hashtag Input","description":"Hashtag field that flags characters ending a tag early (spaces, apostrophes), all-digit tags that never link, case-only duplicates, banned tags and posts over a platform's limit, and suggests CamelCase (#OpenSource) so screen readers say the words. One-tap fixes.","categories":["Validators"],"files":[{"path":"registry/default/hashtag-input/hashtag-input.tsx","type":"registry:ui"},{"path":"registry/default/hashtag-input/hashtag-words.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a hashtag cut short by a space","mistakes":["hashtag apostrophe breaks","too many hashtags","camelcase hashtags accessibility","banned hashtags","hashtag spaces"],"aliases":["hashtag","tags","social post","instagram","x post","linkedin","camelcase","caption","composer"],"flows":["messaging"],"fields":["text"],"summary":"Catches hashtags that quietly break, such as #don't posting as #don, and posts over a platform's tag limit.","catches":["Space, apostrophe or punctuation that cuts a tag short","All-digit tags that don't link","Duplicates that differ only by case","Tags on your banned list","More tags than a platform allows or recommends (Instagram: 5 per post or Reel)","All-lowercase multi-word tags, with a CamelCase suggestion"],"limits":["CamelCase split uses a small built-in word list; extend it with the words prop","Platform limits are baked in and change; pass your own platforms","Does not check whether a tag is shadow-banned or trending"],"a11y":["Visible label tied to the input; Enter or comma adds a tag, Backspace on an empty field removes the last","Each remove button has an aria-label naming the tag","Issues are in a polite live region; icons are aria-hidden","Animation is skipped under reduced motion"],"rules":{"verified":"2026-10-17","sources":[{"label":"Instagram @creators: up to 5 hashtags per reel or post (18 Dec 2025)","url":"https://www.instagram.com/p/DSalTolEbvf/"}],"note":"Informational; platform limits change. Only the Instagram limit was checked; LinkedIn and X counts are recommendations and were not re-verified."}}},{"name":"timezone-picker","type":"registry:ui","title":"Timezone Picker","description":"Time zone search by city, country or abbreviation, listing every meaning of ambiguous abbreviations (IST, CST) instead of guessing, with local time and UTC offset per result, warnings for an upcoming clock change and a zone that differs from the device's.","categories":["Validators"],"files":[{"path":"registry/default/timezone-picker/timezone-data.ts","type":"registry:ui"},{"path":"registry/default/timezone-picker/timezone-picker.tsx","type":"registry:ui"}],"dependencies":["@date-fns/tz","@floating-ui/react","lucide-react","motion"],"registryDependencies":["https://bollard-ui.vercel.app/r/timezone-mismatch.json"],"meta":{"stops":"Meeting scheduled in the wrong zone","mistakes":["ist ambiguous timezone","timezone select search","wrong timezone meeting","dst change timezone","utc offset picker"],"aliases":["timezone","time zone","ist","utc offset","tz select","iana","calendar","settings","scheduling"],"flows":["scheduling","account-settings"],"fields":["datetime"],"summary":"Finds time zones by city, country or abbreviation and lists every meaning of ambiguous ones like IST or CST instead of guessing.","catches":["Ambiguous abbreviations, with all their meanings","A clock change within dstWarningDays","A pick that differs from the device's zone, with use-device option","Local time and UTC offset for each result"],"limits":["Search covers popular zones and aliases; obscure zones may need the IANA name","Offsets come from the runtime's tz database"],"a11y":["Visible <label>; input is a combobox with aria-expanded, aria-controls and aria-activedescendant","Arrow keys move, Enter picks, Escape clears","Results are a listbox of options with aria-selected; status is role=status","Animations respect reduced motion"]}},{"name":"scope-picker","type":"registry:ui","title":"Scope Picker","description":"API-key scope picker that reads back in plain words what the key can do (destructive powers in red), flags write, delete and admin scopes, offers \"Only what you've used\" from real usage, and warns when the key never expires.","categories":["Validators"],"files":[{"path":"registry/default/scope-picker/scope-picker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"API key with admin powers leaks","mistakes":["api key too many permissions","least privilege scopes","token never expires","personal access token scopes","oauth scopes picker"],"aliases":["api key","scopes","permissions","access token","pat","least privilege","developer settings","oauth scopes","rbac"],"flows":["devtools","account-settings","danger-zone"],"fields":["secret"],"summary":"Reads back what an API key will be able to do in plain words, flags risky scopes and suggests trimming to what was actually used.","server":"Grant only the scopes the server validates, enforce expiry server-side, and check scopes on every API call.","catches":["Write, delete and admin scopes, called out","Scopes granted implicitly through implies chains","Scopes unused in the recent window, with a one-tap \"Only what you've used\"","Keys set to never expire, with a suggested expiry"],"limits":["Only knows the scope definitions and usage you pass","Usage-based trimming can drop a scope needed by a rare job","Does not create the key; the server decides what it grants"],"a11y":["Scope checkboxes use <label>s; scopes granted by a parent are marked","Expiry options are a radiogroup with an aria-label","Readback and warnings render in a polite live region; icons are aria-hidden","Highlight animation is skipped under reduced motion"]}},{"name":"brand-color-input","type":"registry:ui","title":"Brand Color Input","description":"Names the closest palette token with a perceptual (OKLab) distance and offers a one-tap snap when a value is slightly off (#1e40ae vs #1e40af); catches pasted junk, O for 0, wrong digit counts, alpha in 4/8-digit hex and Android #AARRGGBB order.","categories":["Validators"],"files":[{"path":"registry/default/brand-color-input/brand-color-input.tsx","type":"registry:ui"},{"path":"registry/default/brand-color-input/color.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Brand colour off by a hex digit","mistakes":["hex color typo","off brand color","argb vs rgba hex","hex with alpha","nearest design token"],"aliases":["hex color","brand color","color input","design tokens","theme","palette","oklab","tailwind colors","#rrggbb"],"flows":["forms","devtools"],"fields":["code"],"summary":"A colour field that names the nearest brand token and offers a snap when a value is a hair off, so near-miss brand colours don't ship.","catches":["Values a hair away from a palette token, with a snap to the token","The letter O typed for zero, and stray characters from a paste","A digit too many or too few","Alpha hidden in 4- and 8-digit hex","Android #AARRGGBB order, which CSS reads as a different colour"],"limits":["Only knows the palette you pass; with no palette it checks syntax only","\"Close\" is a perceptual distance threshold, so an intentional near-shade can be flagged"],"a11y":["Visible <label>; the hint is linked with aria-describedby and is aria-live=\"polite\"","aria-invalid when the value can't be read","Fix buttons have aria-labels naming the token and value; the swatch is aria-hidden","The flash on fix is skipped with prefers-reduced-motion"]}},{"name":"wrong-layout-input","type":"registry:ui","title":"Wrong Layout Input","description":"Text input that catches text typed in the wrong keyboard layout (Russian, Hebrew, Greek, Hindi InScript against US QWERTY) using letter-pair models, offers the retyped text as a one-tap fix with undo, and leaves names, acronyms, links and code alone.","categories":["Validators"],"files":[{"path":"registry/default/wrong-layout-input/layouts.ts","type":"registry:ui"},{"path":"registry/default/wrong-layout-input/wrong-layout-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Text typed in the wrong keyboard layout","mistakes":["wrong keyboard layout typing","ghbdtn privet","hindi inscript layout","typed in russian by mistake","keyboard switch gibberish"],"aliases":["keyboard layout","wrong layout","transliteration","hindi inscript","russian","search box","punto switcher","language switch"],"flows":["forms","messaging"],"fields":["text"],"summary":"Notices text typed with the wrong keyboard layout (\"ghbdtn\" for \"привет\") and offers the retyped text as a one-tap fix.","catches":["Latin gibberish that is a word in another layout (ghbdtn → привет)","Non-Latin gibberish that is English in QWERTY (руддщ → hello)","Words no real language would spell","Names, acronyms, links, code and your ignore list, left alone"],"limits":["Only the layouts you pass, each mapped against US QWERTY","Statistical: short words can be missed or wrongly flagged"],"a11y":["Visible <label>; hint via aria-describedby in a polite live region","Use button has an aria-label with the converted text; converted text gets a lang attribute","Keep and undo are labelled; focus returns to the input","Highlight animation respects reduced motion"]}},{"name":"fx-send-receive","type":"registry:ui","title":"FX Send Receive","description":"Send/receive pair for international transfers. Locks the side you typed, works out the other from rate and fees, compares what arrives with the invoice, and flags the invoice typed into \"You send\", a currency mismatch, SHA deductions and overpaying, with one-tap fixes.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/fx-send-receive/fx-send-receive.tsx","type":"registry:ui"},{"path":"registry/default/fx-send-receive/fx.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Helps prevent an invoice being paid short after fees","mistakes":["typed amount in you send instead of they receive","transfer arrived short","sha charges deducted from payment","wrong currency for invoice","international payment underpaid"],"aliases":["wire transfer","swift","fx","currency converter","remittance","send money","our sha ben","exchange rate","invoice payment","wise"],"flows":["checkout","forms"],"fields":["amount"],"summary":"Helps an international payment arrive in full: it measures what the recipient actually gets against the invoice, after fees and bank deductions.","server":"Re-quote the rate and fees on the server and charge against that server quote, with an idempotency key on the transfer.","catches":["Invoice amount typed into \"You send\" instead of \"They receive\"","Invoice in a different currency from the receiving side","SHA charges that intermediary banks may deduct on the way","Paying more than the invoice","Fixes: lock \"They receive\" at the exact amount, or switch charges to OUR"],"limits":["Arithmetic on the quote you pass in; it does not fetch live rates or fees","Intermediary deductions are an estimate from your quote's maximum, not what banks will actually take","Does not check the recipient's account details or sanctions screening"],"a11y":["Both amount fields have visible labels; the receive field gets aria-invalid when a warning applies","Charges choice is a radiogroup with roving tabindex and arrow-key navigation","Status line uses role=status; decorative icons are aria-hidden","Focus moves to the receive field after a fix that changes it","Animations are skipped under reduced motion"]}},{"name":"epoch-input","type":"registry:ui","title":"Epoch Input","description":"Reads the value back in the user's zone and UTC, and catches milliseconds pasted as seconds, micro- and nanoseconds, Excel serials, Windows FILETIME, .NET ticks, ISO strings without a zone, date-only strings read as UTC midnight and impossible dates like 30 Feb.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/epoch-input/epoch-input.tsx","type":"registry:ui"},{"path":"registry/default/epoch-input/epoch.ts","type":"registry:ui"}],"dependencies":["@date-fns/tz","lucide-react","motion"],"meta":{"stops":"Timestamp saved as year 57754","mistakes":["milliseconds vs seconds timestamp","unix time shows 1970","epoch converter wrong date","iso date timezone missing","2038 problem"],"aliases":["unix timestamp","epoch","epoch converter","milliseconds","iso 8601","utc","date-fns","created_at","unix time"],"flows":["devtools","forms"],"fields":["datetime","number"],"summary":"Reads a timestamp back as a date in your zone and UTC and catches unit mix-ups, so a value isn't saved as year 57,754 or the wrong hour.","catches":["Milliseconds where seconds belong (and micro/nanoseconds)","Excel serial dates, Windows FILETIME and .NET ticks","ISO strings with no time zone, which JavaScript and servers read differently","Date-only strings read as UTC midnight","Impossible dates (30 Feb) and values outside 32-bit range when int32 is set"],"limits":["Unit guesses rely on plausible ranges; very old or far-future dates can be misread","Time zones come from the browser's Intl data"],"a11y":["Visible <label>; aria-invalid and aria-describedby to the readback","After a fix, focus moves to the next fix or back to the input","Motion respects prefers-reduced-motion"]}},{"name":"notice-period-check","type":"registry:ui","title":"Notice Period Check","description":"Notice calculator that works out the last day from the resignation and policy (days, weeks or months; calendar or working days) and flags month-end arithmetic, weekend or holiday last days, HR's time zone dating the email differently, unpaid leave, leave set-off and buyout cost.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/notice-period-check/notice-period-check.tsx","type":"registry:ui"},{"path":"registry/default/notice-period-check/notice.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong last working day after resigning","mistakes":["notice period calculation","last working day wrong","resignation date calculator","notice buyout cost","month notice end date"],"aliases":["notice period","resignation","last working day","lwd","hr portal","exit","notice buyout","relieving","offboarding"],"flows":["forms","scheduling","india"],"fields":["datetime","number"],"summary":"Works out the real last working day from a resignation and the notice policy, so nobody resigns towards the wrong date.","catches":["Month-end arithmetic such as 31 Jan + 1 month","Last day on a weekend or holiday","Resignation email dated differently in HR's time zone","Unpaid leave pushing the date out, and leave set-off","Buyout cost of leaving early, plus a line to copy to HR"],"limits":["Your contract and policy decide; defaults (notice from the next day) are the common reading, not a rule","Holidays and leave balance come from your props","Not legal advice"],"a11y":["Section labelled by its heading; every input has a visible label","The sent-at field's label names the time zone","Focus moves to the first fix after a change"],"rules":{"verified":"2026-10-17","sources":[{"label":"GOV.UK: Handing in your notice","url":"https://www.gov.uk/handing-in-your-notice"}],"note":"Informational, not legal advice. Confirmed the UK reading that notice usually runs from the day after it is handed in. Indian contract practices mentioned in the code were not verified."}}},{"name":"tax-regime-compare","type":"registry:ui","title":"Tax Regime Compare","description":"Old vs new regime tax for salaried residents in India, tax year 2026-27, from salary, HRA (50% cities per rule 279), 80C, 80D, NPS and home-loan interest. Shows break-even deductions and flags a costlier choice, deductions the new regime ignores and over-limit claims.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/tax-regime-compare/tax-regime-compare.tsx","type":"registry:ui"},{"path":"registry/default/tax-regime-compare/tax.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Picked the costlier tax regime","mistakes":["old vs new tax regime","which tax regime is better","hra deduction calculator","80c not allowed new regime","income tax calculator india"],"aliases":["income tax","old regime","new regime","itr","hra","80c","80d","nps","salary slip","tax declaration"],"flows":["india","forms"],"fields":["amount","number"],"summary":"Compares India's old and new income-tax regimes for a salaried person and flags a choice that costs more or deductions that don't count.","catches":["The regime that costs more for these inputs","Break-even deductions where the old regime starts to win","Deductions that don't count under the new regime","Claims above their limits (80C ₹1.5 lakh, 80D, 80CCD(1B) ₹50,000, home loan)","HRA at 50% of salary in the eight listed cities, 40% elsewhere"],"limits":["Informational estimate for resident salaried individuals; not tax advice","Covers common deductions only: capital gains, business income, surcharge edge cases and marginal-relief nuances may be missing","Rules change every Budget; check the verified date and source"],"a11y":["Section labelled by its heading; every input has a <label>","Result status in a polite live region","After a fix, focus moves to the next fix","External verify link has visible text"],"rules":{"verified":"2026-10-17","sources":[{"label":"Memorandum to the Finance Bill, 2026 (rates for tax year 2026-27)","url":"https://www.indiabudget.gov.in/doc/memo.pdf"},{"label":"Income-tax Rules, 2026 – Rule 279 (HRA)","url":"https://www.incometaxindia.gov.in/w/rule-279-1"},{"label":"Income Tax Department – Salaried individuals (slabs, 87A rebate, 80C/80D/80CCD(1B))","url":"https://www.incometax.gov.in/iec/foportal/help/individual/return-applicable-1"}],"note":"Confirmed: no change in s.202 rates for tax year 2026-27; new-regime slabs 0–4L nil … above 24L 30%; rebate ₹60,000 up to ₹12 lakh (new) and ₹12,500 up to ₹5 lakh (old); 80C ₹1.5 lakh, 80D ₹25,000/₹50,000, 80CCD(1B) ₹50,000; HRA 50% for Mumbai, Kolkata, Delhi, Chennai, Hyderabad, Pune, Ahmedabad, Bengaluru. Standard deduction (₹75,000/₹50,000) and the ₹2 lakh home-loan limit were not re-checked on a primary page. Informational; verify with the official source or a professional."}}},{"name":"emi-readback","type":"registry:ui","title":"EMI Readback","description":"Reads back EMI, total interest, processing fee with GST, other upfront charges, the amount that reaches the account and the APR computed as in RBI's Key Facts Statement, catching tenure in the wrong unit, monthly rates typed as yearly, flat rates and heavy fees.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/emi-readback/emi-readback.tsx","type":"registry:ui"},{"path":"registry/default/emi-readback/emi.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Signed for a loan costing more","mistakes":["emi calculator wrong tenure","flat rate vs reducing rate","processing fee gst loan","loan apr hidden charges","months typed as years"],"aliases":["emi","loan","emi calculator","apr","key facts statement","personal loan","home loan","processing fee","nbfc","rbi"],"flows":["india","checkout","forms"],"fields":["amount","number"],"summary":"Reads back a loan's EMI, total interest, fees and real yearly cost before signing, so a borrower doesn't agree to a costlier loan than they think.","catches":["Tenure typed in months when years were meant, or the reverse","A monthly rate typed as yearly","Flat rates that hide a much higher reducing rate","Fees and GST that cut what actually reaches the account","The APR, including upfront charges"],"limits":["A reducing-balance estimate; lenders' day counts and rounding differ slightly","GST on the processing fee defaults to 18%","Not financial advice; the lender's KFS is the binding document"],"a11y":["Each term has a <label>, aria-invalid and aria-describedby to its issues","Focus moves to the first fix","Issues use icons plus text"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI circular: Key Facts Statement (KFS) for Loans & Advances (15 Apr 2024)","url":"https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=12663&Mode=0"}],"note":"Informational; check the official source. Checked that APR must include all charges levied by the lender. The Annex B worked example (17.07%) was not re-checked: the annex PDF could not be fetched. GST rate on fees not checked."}}},{"name":"leave-request-check","type":"registry:ui","title":"Leave Request Check","description":"Leave request field that reads the range as calendar days, working days and leave really used, and flags weekend or holiday ends, a sandwich rule charging days between, half days with no half or a gap, a negative balance and teammates away, with fixes.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/leave-request-check/leave-request-check.tsx","type":"registry:ui"},{"path":"registry/default/leave-request-check/leave.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Leave request burns extra days","mistakes":["sandwich leave rule","leave balance negative","half day leave wrong","leave days calculation","holiday in leave range"],"aliases":["leave","time off","pto","leave application","hrms","sandwich leave","holiday calendar","half day","out of office"],"flows":["scheduling","forms"],"fields":["datetime"],"summary":"Reads a time-off request back day by day, so it doesn't use more leave than the person thinks or leave a gap they didn't intend.","catches":["Weekends or holidays at either end of the range","Your policy's sandwich rule charging weekends or holidays in between","Half day with no half chosen, or one that leaves a working gap","Balance going negative","Too many teammates away at once","Start after end, and the actual back-at-work date"],"limits":["Holidays, policy and balance come from your props; it doesn't know your HR system","The OSH Code note is shown for en-IN by default; whether it applies depends on the worker and establishment","Not legal advice"],"a11y":["Start and end dates have visible labels; half-day selects have aria-labels","Date inputs get aria-invalid for an invalid or reversed range","Day-by-day readback is a labelled section with a labelled list","Focus moves to the first fix after a change"],"rules":{"verified":"2026-10-17","sources":[{"label":"OSH Code 2020, s.32(1)(iv) (PRS Legislative Research copy of the Act)","url":"https://prsindia.org/files/bills_acts/bills_parliament/2020/Occupational%20Safety,%20Health%20And%20Working%20Conditions%20Code,%202020.pdf"},{"label":"India Code: OSH Code 2020","url":"https://www.indiacode.nic.in/bitstream/123456789/22041/1/a2020-37.pdf"}],"note":"Informational, not legal advice. s.32(1)(iv) wording confirmed. The 21 Nov 2025 commencement (S.O. 5321(E)) was seen in secondary sources only, and the Maharashtra Shops & Establishments s.18(9) reference was not checked."}}},{"name":"ctc-breakdown-input","type":"registry:ui","title":"CTC Breakdown Input","description":"Splits CTC into fixed pay, variable, ESOPs, joining bonus, gratuity and PF, estimates gratuity and PF in one tap, works out monthly in-hand pay under India's new tax regime with every assumption written down, and flags offers where in-hand is far below the headline.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/ctc-breakdown-input/ctc-breakdown-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"CTC mistaken for take-home pay","mistakes":["ctc to in hand salary","in hand salary calculator","offer letter ctc breakup","variable pay in ctc","lpa vs monthly salary"],"aliases":["ctc","in-hand salary","take home","lpa","salary breakup","offer letter","esop","pf","gratuity","job offer"],"flows":["india","forms"],"fields":["amount"],"summary":"Splits a job offer's CTC into what is actually paid monthly, so the headline number isn't mistaken for take-home pay.","catches":["Variable pay, ESOPs and one-time bonuses counted in the headline","Gratuity and employer PF that never reach the bank account","In-hand pay far below the headline (minInHandRatio)","Parts that add up to more than the CTC"],"limits":["An estimate under India's new tax regime only; old-regime deductions, HRA and other income are not modelled","Assumes basic is 50% of fixed pay, PF capped at the ₹15,000/month wage ceiling and ₹2,500 professional tax unless you change the assumptions","Tax rules are built in as of the verification date and need updating each budget"],"a11y":["Each amount has a <label>; aria-invalid and aria-describedby to its error","Colour swatches are aria-hidden; parts are named in text","Results are announced in a visually hidden aria-live=\"polite\" region","Estimate actions move focus to the field they fill"],"rules":{"verified":"2026-10-17","sources":[{"label":"Income Tax Department: Salaried individuals for AY 2026-27 (new regime slabs, 87A)","url":"https://www.incometax.gov.in/iec/foportal/help/individual/return-applicable-1"}],"note":"Informational; not tax advice. Checked new-regime slabs (₹4 L nil up to 30% above ₹24 L) and the ₹60,000 87A rebate up to ₹12 L for AY 2026-27. Not checked: that FY 2026-27 is unchanged, surcharge bands, the 4.81% gratuity factor, the PF wage ceiling and professional tax."}}},{"name":"guest-count-picker","type":"registry:ui","title":"Guest Count Picker","description":"Guest picker for adults, children and infants that asks each child's age, then flags a child over the age limit (or young enough to be a free infant), infants with no adult, rooms over capacity or out of cots, and a room split that doesn't match the party, with one-tap fixes.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/guest-count-picker/guest-count-picker.tsx","type":"registry:ui"},{"path":"registry/default/guest-count-picker/guest-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Hotel booking bounced over guests","mistakes":["child age hotel booking","too many guests per room","infant without adult","rooms and guests picker","hotel occupancy limit"],"aliases":["guests","rooms","adults children","hotel booking","occupancy","travellers","child age","booking widget","airbnb"],"flows":["checkout","forms"],"fields":["number"],"summary":"Asks each child's age and checks the room split, so a hotel booking isn't bounced for an over-age child, a lone infant or an over-full room.","catches":["Child older than the hotel's child age limit","Child young enough to count as a free infant","Infant or child in a room with no adult","Room over capacity or more infants than cots","Room split that doesn't add up to the party"],"limits":["Defaults (child to 17, infant under 2, 4 per room, 1 cot) are typical, not any hotel's policy; pass your property's limits","Does not check live availability or pricing"],"a11y":["Each counter is a spinbutton with min, max and now values, labelled, and works with arrow keys","The +/- buttons have aria-labels and are skipped in tab order (keyboard uses the spinbutton)","Age selects are labelled, with aria-invalid until chosen","Room split uses table roles with row and column headers"]}},{"name":"salary-input","type":"registry:ui","title":"Salary Input","description":"Salary field that reads \"12 LPA\", \"85k/month\" or \"1.2 Cr\", catches monthly entered as yearly (and back), a bare number meant as lakhs, and an extra zero against the role's range, reads amounts back per month and per year, and offers one-tap fixes.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/salary-input/salary-input.tsx","type":"registry:ui"},{"path":"registry/default/salary-input/salary.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Monthly salary entered as yearly","mistakes":["salary monthly vs yearly","12 lpa entered as 12","extra zero salary","expected ctc field","lakh crore salary"],"aliases":["salary","expected ctc","current ctc","lpa","lakh","job application","naukri","monthly salary","annual"],"flows":["india","forms"],"fields":["amount"],"summary":"Catches salaries entered in the wrong period or scale, like monthly typed as yearly or a bare \"12\" meant as 12 LPA.","catches":["Monthly figure entered as yearly, and the reverse","Bare \"12\" likely meant as 12 LPA (or 85 as 85k outside India)","An extra or missing zero against the expected range","Units written in the text (LPA, k, lakh, crore, million) overriding the period picker","Read-back per month and per year, in lakh/crore for INR"],"limits":["Checks plausibility against the ranges you pass; it can't know the real offer","Parses common English and Indian shorthand only","Does not handle tax, CTC components or variable pay"],"a11y":["Visible <label> on the input; read-back and status linked via aria-describedby","Period picker is a roving-tabindex radiogroup with arrow keys","Status in a polite live region; dismiss has an aria-label","After a fix, focus returns to the input"]}},{"name":"size-fit-picker","type":"registry:ui","title":"Size Fit Picker","description":"Size picker that converts UK, US, EU and IN sizes live without losing the pick, reads back the shopper's usual size adjusted for brands that run small or large, and catches their usual number clicked in the wrong system with a one-tap fix.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/size-fit-picker/size-fit-picker.tsx","type":"registry:ui"},{"path":"registry/default/size-fit-picker/sizes.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong shoe or clothing size ordered","mistakes":["uk us size conversion","shoe size chart india","wrong size ordered","size guide","brand runs small"],"aliases":["size chart","size guide","shoe size","uk us eu in","fit","myntra","ecommerce","product page","variant picker"],"flows":["checkout"],"fields":["number"],"summary":"Converts UK, US, EU and IN sizes live and catches a usual size clicked in the wrong system, like US 8 picked as UK 8.","catches":["The usual size number picked in the wrong system","Brand fit offsets (runs small or large)","Switching display systems without changing the pick","Sizes without an equivalent in a system"],"limits":["Ships with typical men's shoe, women's shoe and women's clothing charts; brands vary","Fit offsets come from your data"],"a11y":["Fieldset labelled by its legend","System and size pickers are radiogroups with arrow, Home and End keys","Suggestion is a labelled button"],"rules":{"verified":null,"note":"Built-in size charts are general conversions, not an official standard; not verified against a primary source. Use your brand's own chart."}}},{"name":"unit-input","type":"registry:ui","title":"Unit Input","description":"Measurement input that notices when a value is far more plausible in another unit (70 cm is probably inches, 98.6 °C is °F) or is typed as 5'9\", and offers a one-tap conversion at the right precision or a unit switch.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/unit-input/unit-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Height saved in the wrong unit","mistakes":["cm vs inches","fahrenheit celsius wrong","unit conversion input","5 foot 9 input","wrong unit stored"],"aliases":["units","unit converter","cm inches","kg lbs","height","weight","temperature","measurements","metric imperial"],"flows":["forms"],"fields":["number"],"summary":"Notices when a number makes far more sense in another unit (70 cm is likely inches) and offers a one-tap conversion.","catches":["Values outside the usual range for the unit but inside another's","Feet-and-inches notation (5'9\", 5 ft 9 in, 5.9)","Body temperature in the wrong scale","Conversion rounded to the target unit's precision"],"limits":["Plausibility ranges are presets you can override; unusual real values can be flagged","Converts between the units in the preset only"],"a11y":["Visible <label>; unit picker has an aria-label","aria-invalid with suggestions in a polite live region via aria-describedby","Keep-as-typed dismiss has an aria-label"]}},{"name":"time-confirm","type":"registry:ui","title":"Time Confirm","description":"Time input that asks AM or PM when a time is ambiguous, spells out noon and midnight, catches times a DST change skips (one-tap move to the real time) or repeats (pick which), and shows the moment in other zones with day-shift flags.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/time-confirm/time-confirm.tsx","type":"registry:ui"}],"dependencies":["@date-fns/tz","lucide-react","motion"],"meta":{"stops":"Meeting booked 12 hours off","mistakes":["am pm confusion","dst skipped time","noon midnight ambiguity","time zone meeting wrong","12 hour time input"],"aliases":["time picker","time input","am pm","meeting time","dst","time zone","booking","appointment","calendar invite"],"flows":["scheduling"],"fields":["datetime"],"summary":"Asks AM or PM when a typed time is ambiguous and catches times a clock change skips or repeats, so meetings aren't 12 hours off.","catches":["Hours 1–12 typed without AM/PM","Noon and midnight, spelled out","Times skipped by a DST change, with a move to the first real time","Times repeated by a DST change, with a choice of which","Other zones' times, flagged when they fall on another day"],"limits":["Needs a date and IANA time zone for DST checks","Parses common English time formats only","assume24h changes how bare hours are read"],"a11y":["Visible <label>; aria-invalid and hint via aria-describedby","AM/PM choice is a roving-tabindex radiogroup with arrow keys","Hint in a polite live region","Animations respect reduced motion"]}},{"name":"quantity-stepper","type":"registry:ui","title":"Quantity Stepper","description":"Quantity stepper that clamps to min, max and stock with an explanation, rounds to pack sizes, holds big typed jumps for a \"Did you mean 10?\" confirmation, ignores the scroll wheel and accelerates on long-press.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/quantity-stepper/quantity-stepper.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Accidental bulk order","mistakes":["quantity typo 100 instead of 10","scroll changes number input","quantity exceeds stock","plus minus stepper","max quantity cart"],"aliases":["quantity","stepper","qty","add to cart","cart","plus minus","number input","stock","checkout"],"flows":["checkout"],"fields":["number"],"summary":"Holds suspicious quantity jumps for a \"Did you mean 10?\" check and clamps to stock with a reason, so an accidental bulk order isn't placed.","server":"Validate quantity against live stock and order limits on the server.","catches":["Typed jumps far above the typical quantity (100 when people order 2)","Values over max or stock, clamped with a reason","Quantities that aren't a whole pack (step)","Scroll-wheel changes while scrolling the page"],"limits":["\"Suspicious\" is relative to your typical prop","Stock shown is what you pass; it may be stale","Doesn't reserve stock"],"a11y":["Visible label; the input is a spinbutton with min, max and now values, described by a hint","Enter commits a typed value","+/- buttons have aria-labels and aria-controls, and are skipped in tab order"]}},{"name":"percent-input","type":"registry:ui","title":"Percent Input","description":"Percent field that catches 0.15 typed for 15%, 0.25 basis points meant as 25, and values outside the allowed range (150% where 100% is the cap), reads the result back in money (15% of ₹2,000 = ₹300) and offers a one-tap fix.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/percent-input/percent-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Rate entered 100 times too small","mistakes":["0.15 vs 15 percent","basis points confusion","percent over 100","discount percent input","interest rate wrong"],"aliases":["percent","percentage","rate","discount","interest rate","commission","gst rate","bps","tax rate"],"flows":["forms","checkout"],"fields":["number","amount"],"summary":"Catches 0.15 typed for 15%, basis-point slips and rates over the cap, and reads the result back in money, so a rate isn't 100 times off.","catches":["Fraction typed instead of percent (0.15 for 15%)","Tiny basis-point values (0.25 bps for 25)","Over max or under min, with a dropped-zero fix (150 → 15)","Money readback against a base amount"],"limits":["\"Likely\" ranges are heuristics; set likelyMin, min and max for your domain","Locale defaults to en-IN; pass locale and currency for other markets"],"a11y":["Visible label; input has aria-invalid on error and is described by a polite status","Errors use role=alert","Unit switch is a labelled group of aria-pressed buttons"]}},{"name":"range-pair","type":"registry:ui","title":"Range Pair","description":"From/to field pair that catches reversed ends (moving a 28 Dec → 3 Jan end into next year rather than just swapping), equal exclusive ends, and year or extra-zero typos that make a range huge, each with a one-tap fix.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/range-pair/range-pair.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Backwards date or price range","mistakes":["end date before start","min greater than max","date range across year","price range filter wrong","reversed range"],"aliases":["date range","min max","price range","from to","filter","check in check out","range picker","start end"],"flows":["forms","scheduling"],"fields":["datetime","number"],"summary":"Catches backwards, empty or wildly oversized date and number ranges, and repairs them the likely way (next year for Dec → Jan).","catches":["Reversed ends, with the likelier repair (next year for Dec → Jan)","Empty ranges when the end is exclusive","Oversized ranges from a year or extra-zero typo (maxSpan)","Plain swap as an alternative fix"],"limits":["Dates are YYYY-MM-DD in UTC; times of day aren't handled","maxSpan has to be set for size checks to mean anything"],"a11y":["Both ends have visible labels and are described by a polite status region","Fix and swap are buttons; icons are aria-hidden","Focus returns to the From field after a fix"]}},{"name":"schedule-readback","type":"registry:ui","title":"Schedule Readback","description":"Cron input that reads the expression back in plain English, lists the next runs in your time zone beside the scheduler's, and catches every-minute stars, day-of-month OR day-of-week, the 31st skipping months and 30 February, with one-tap fixes.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/schedule-readback/cron.ts","type":"registry:ui"},{"path":"registry/default/schedule-readback/schedule-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Cron job fires at the wrong time","mistakes":["cron expression wrong time","cron every minute mistake","cron timezone server","crontab explain","cron day of week or month"],"aliases":["cron","crontab","cron expression","scheduler","job schedule","github actions schedule","vercel cron","utc","next run"],"flows":["devtools","scheduling"],"fields":["code","datetime"],"summary":"Reads a cron expression back in plain English with the next runs in your zone and the server's, catching classic cron traps.","catches":["A * in the minute field that runs every minute","Day-of-month and day-of-week both set, which runs on either (OR)","Day 31 (or 29–30) skipping shorter months, and impossible dates like 30 Feb","Scheduler zone differing from yours, with runs shown in both","Invalid expressions, with the reason"],"limits":["5-field Vixie/POSIX cron only; Quartz seconds/years, Jenkins H and cloud-specific syntaxes are not supported","Next runs are computed in the browser; DST handling depends on the zones you pass"],"a11y":["Visible <label>; aria-invalid with read-back via aria-describedby","Parse errors use role=alert; read-back and warnings render in polite live regions","Each fix button has an aria-label with the corrected expression; focus returns to the input after a fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"crontab(5) – Linux manual page","url":"https://man7.org/linux/man-pages/man5/crontab.5.html"}],"note":"Confirmed: when both day-of-month and day-of-week are restricted, the job runs when either matches. Other schedulers (systemd, Quartz, cloud cron) differ."}}},{"name":"split-allocator","type":"registry:ui","title":"Split Allocator","description":"Split editor that shows what's left as you type, gives the rest to any row in one tap, flags rows on 0, and rounds every automatic split with largest remainder so it sums to the total exactly (100 / 3 → 34, 33, 33).","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/split-allocator/split-allocator.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Split adds up to 101%","mistakes":["percentages dont add to 100","split bill rounding","allocation total mismatch","revenue split percent","share split"],"aliases":["split","allocation","split bill","percentages","splitwise","revenue share","budget allocation","rounding","equity"],"flows":["forms","checkout"],"fields":["amount","number"],"summary":"Keeps splits adding up to exactly the total, showing what's left as you type and rounding automatic splits so nothing is off by one.","catches":["Totals over or under (99%, 101%)","Rows left on 0","Remainder, assignable to any row in one tap","Automatic splits that would drift by rounding, fixed with largest-remainder"],"limits":["Even and proportional splits only; custom rules are yours","Uses decimal precision you set; currency minor units must match"],"a11y":["Group labelled by its label; each row input has a <label>","aria-invalid when over the total; status in a polite live region via aria-describedby","Give-rest/take-over buttons have aria-labels with the amount","Progress bar is aria-hidden; the numbers are in text"]}},{"name":"duration-input","type":"registry:ui","title":"Duration Input","description":"Parses 1h30, 90m, 1.5h, 1:30 and 2d 4h into one duration read back in words, catches 1.50 typed for 1 h 50 min, and asks whether 1:30 is h:mm or m:ss when the limits can't decide.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/duration-input/duration-input.tsx","type":"registry:ui"},{"path":"registry/default/duration-input/duration.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Timesheet logs 1.5 as 1h50","mistakes":["1.5 hours vs 1:30","duration format parse","timesheet hours wrong","estimate input","hh:mm vs mm:ss"],"aliases":["duration","timesheet","hours","time tracking","estimate","jira","minutes","1h30m","time spent"],"flows":["forms","scheduling"],"fields":["number","datetime"],"summary":"Parses durations typed many ways and reads them back in words, so 1.5 isn't logged as 1 h 50 min on a timesheet.","catches":["1.50 meant as 1 h 50 min versus 1.5 hours","Ambiguous 1:30 (h:mm or m:ss), asked when limits can't decide","Values outside min and max, with the limit as a fix","Days converted using hoursPerDay (24 by default)"],"limits":["Parses English unit words and abbreviations only","Days are 24 hours by default; set hoursPerDay (e.g. 8) for working days"],"a11y":["Labelled input with the result read back in words","Each interpretation is a button with visible text"]}},{"name":"billing-period-input","type":"registry:ui","title":"Billing Period Input","description":"Reads a recurring price back per period (₹499/month = ₹5,988/year) and, against a reference price, catches a yearly price in the monthly field, a team total entered per seat and other implausible jumps, each with a one-tap fix.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/billing-period-input/billing-period-input.tsx","type":"registry:ui"},{"path":"registry/default/billing-period-input/billing-period.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Yearly price typed into monthly plan","mistakes":["monthly vs yearly price","per seat price wrong","pricing plan typo","annual price input","subscription price"],"aliases":["pricing","plan","subscription","billing period","monthly yearly","per seat","dodo payments","stripe price","saas"],"flows":["checkout","account-settings"],"fields":["amount"],"summary":"Reads a recurring price back in every billing period and catches a yearly price typed into the monthly field.","catches":["A yearly price entered as monthly, or the reverse","A team total entered as a per-seat price","Large jumps from the reference price (current plan, plan below or last year's)","Misreading the rate, by showing it in every period"],"limits":["The mix-up checks need a reference price; without one it only reads back","Weeks are converted as 12/52 of a month, so readbacks are approximate for weekly plans"],"a11y":["Visible <label> tied to the input, described by an aria-live=\"polite\" status","Price basis is a role=\"radiogroup\" of role=\"radio\" buttons","The period selector has an aria-label","Warnings use an icon plus text"]}},{"name":"replace-preview","type":"registry:ui","title":"Replace Preview","description":"Find-and-replace that previews every match in context with a count, names matches inside longer words with a one-tap whole-word fix, preserves case, lets you skip single matches, and undoes the replace.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/replace-preview/replace-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Replace-all mangles other words","mistakes":["find replace whole word","replace all broke text","replace inside words","find and replace preview","case preserving replace"],"aliases":["find and replace","replace all","search replace","bulk rename","editor","whole word","preview changes","text tool"],"flows":["forms","devtools"],"fields":["text"],"summary":"Previews every find-and-replace match in context before applying, so \"cat\" → \"dog\" doesn't turn \"category\" into \"dogegory\".","catches":["Matches that sit inside longer words, with a whole-word fix","Total match count before applying","Individual matches you want to skip","Case of each match, preserved in the replacement (CAT → DOG)"],"limits":["Plain-text matching, not regex","Word boundaries follow simple letter rules and may differ for some scripts","Undo covers the last replace only"],"a11y":["Find and replace inputs have labels","Match-case, whole-word and preserve-case are aria-pressed toggles in a labelled group","Match list has an aria-label with the count; each include/skip control has its own label","Warnings and the result render in polite live regions"]}},{"name":"shift-time-input","type":"registry:ui","title":"Shift Time Input","description":"Shift time input that treats 22:00–06:00 as an 8 h overnight shift, asks whether 9:00–5:00 meant 5 PM, questions 0 h, over-long and 26:00-style entries, deducts breaks (catching one typed in hours) and counts real hours across a DST change.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/shift-time-input/shift-time-input.tsx","type":"registry:ui"},{"path":"registry/default/shift-time-input/shift.ts","type":"registry:ui"}],"dependencies":["lucide-react","@date-fns/tz","@number-flow/react"],"meta":{"stops":"Night shift logged as 20 hours","mistakes":["overnight shift hours","shift time am pm","night shift calculation","break deduction hours","roster time wrong"],"aliases":["shift","roster","timesheet","attendance","night shift","clock in clock out","rota","hours worked","payroll"],"flows":["scheduling","forms"],"fields":["datetime"],"summary":"Reads shift times the way they're meant, so 22:00–06:00 is 8 h overnight and 9–5 isn't logged as 20 h.","catches":["Overnight shifts ending the next day","Ambiguous 1–12 times with no AM/PM (9–5, 10–6)","Zero-length, over-long (maxMinutes) and 26:00-style entries","A break typed in hours instead of minutes","Real hours when clocks change mid-shift, given date and time zone"],"limits":["Break rules and maximums are what you configure; labour law is not built in","DST correction needs date and timeZone props"],"a11y":["Start, end and break inputs have <label>s; aria-invalid on unparseable times","Readback and status linked via aria-describedby and a polite live region","Errors use role=alert; warnings are text with icons","Length has an aria-label in words"]}},{"name":"dimension-input","type":"registry:ui","title":"Dimension Input","description":"Labels each side, fills all three from a pasted \"16 x 12 x 8 in\", checks them against what is being measured to catch the wrong unit, an extra digit and sides in the wrong order, and reads the size back in metric and imperial with volume and volumetric weight.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/dimension-input/dimension-input.tsx","type":"registry:ui"},{"path":"registry/default/dimension-input/dimension.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong package size ships","mistakes":["length width height wrong unit","volumetric weight calculator","parcel dimensions typo","dimensions cm vs inch","lxwxh input"],"aliases":["dimensions","lxwxh","length width height","package size","shipping","volumetric weight","shiprocket","listing","courier"],"flows":["forms","checkout"],"fields":["number"],"summary":"Labels each side of a size, fills all three from a pasted \"16 x 12 x 8 in\" and catches wrong units or an extra digit, so the wrong package size doesn't ship.","catches":["The wrong unit for the item category (cm entered as inches)","A side with an extra digit","Sides entered in an unexpected order","Pasted sizes like \"16 x 12 x 8 in\", split into fields","Courier volumetric weight, using your divisor"],"limits":["Plausibility checks need a category; without one it only reads back","The default volumetric divisor may not match your courier"],"a11y":["Each side and the unit have their own <label>","Fields are described by an aria-live=\"polite\" status","Highlight animation respects prefers-reduced-motion"]}},{"name":"tax-inclusive-input","type":"registry:ui","title":"Tax-Inclusive Input","description":"Price field with an explicit includes/excludes-tax toggle, a net + tax = gross readback, and flags for an \"excludes\" price that divides back to a round number, a mode switch that silently changed the total, and a typo'd rate like 0.18 or 180.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/tax-inclusive-input/tax-inclusive-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"GST added twice to a price","mistakes":["gst inclusive exclusive","tax added twice","price including gst","18 percent gst calculation","gst rate typo"],"aliases":["gst","tax inclusive","tax exclusive","mrp","price","invoice","vat","net gross","product listing"],"flows":["india","checkout","forms"],"fields":["amount"],"summary":"Makes \"includes / excludes GST\" an explicit choice and reads back net + tax = gross, so tax isn't added twice.","catches":["An \"excludes tax\" price that looks already taxed (divides back to a round number)","Switching mode that changed the total, with keep-the-total fix","Rates typed as a fraction (0.18), multiplier (1.18) or slipped digit (180)","Rates above 50%"],"limits":["Default rate list is India's main GST slabs (0, 5, 18, 40); special rates (e.g. 3% on gold, 0.25%) and tobacco still on old rates are not included","Rounds to the currency's minor unit; invoice rounding rules may differ","Does not decide which rate applies to an item"],"a11y":["Visible <label> on the amount; readback via aria-describedby","Rate input has an aria-label; includes/excludes is a radiogroup","Warnings and fixes in a polite live region","Total highlight respects reduced motion"],"rules":{"verified":"2026-10-17","sources":[{"label":"PIB – GST rate rationalisation (56th GST Council), effective 22 Sep 2025","url":"https://pib.gov.in/PressReleasePage.aspx?PRID=2164586"}],"note":"Re-checked 2026-10-17 against PIB release 2164586 (8 Sep 2025): the 56th GST Council moved GST from four slabs (5/12/18/28%) to two main rates, 5% (merit) and 18% (standard), plus a 40% special rate for sin/luxury goods, effective 22 Sep 2025. The default list [0, 5, 18, 40] (0 = nil-rated) matches; special rates such as 3% on gold and 0.25% on rough diamonds, and tobacco items still on 28% plus cess, are not in it. Informational; check CBIC rate notifications for an item's rate."}}},{"name":"version-bump","type":"registry:ui","title":"Version Bump","description":"Release picker that reads changes (Conventional Commits or tagged), marks the bump they need, previews each option's version, warns when the pick is too small (or a needless major), and reads back \"1.4.2 → 2.0.0\", handling 0.x, pre-release tags and their ordering.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/version-bump/semver.ts","type":"registry:ui"},{"path":"registry/default/version-bump/version-bump.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Breaking change shipped as a minor","mistakes":["semver major minor patch","breaking change minor release","conventional commits version","npm version bump","pre-release version order"],"aliases":["semver","version bump","release","changelog","conventional commits","npm publish","changesets","major minor patch","tag"],"flows":["devtools"],"fields":["code"],"summary":"Suggests the semver bump a set of changes needs and warns when the pick is too small, so breaking changes don't ship as minor or patch.","catches":["A minor or patch for breaking changes, which ^ranges would install","A major with no breaking changes","0.x versions, where the minor slot carries breaking changes in npm ranges","Pre-release tags dropped on release and tags that sort backwards (beta < rc)","Invalid current versions"],"limits":["Only as good as the change list: commits without type or ! markers are missed","Semver says 0.y.z may change anything; the 0.x handling follows npm caret behaviour"],"a11y":["Bump options are a roving-tabindex radiogroup with arrow keys","Needed bump marked with text, not colour alone","Readback and warnings in polite live regions; invalid input uses role=alert"],"rules":{"verified":"2026-10-17","sources":[{"label":"Semantic Versioning 2.0.0","url":"https://semver.org/"}],"note":"Confirmed §11 pre-release precedence (1.0.0-alpha < … < 1.0.0-rc.1 < 1.0.0) and that 0.y.z is for initial development."}}},{"name":"dose-input","type":"registry:ui","title":"Dose Input","description":"Flags ISMP error-prone dose designations as typed (trailing zeros, missing leading zero, U and IU, µg, cc, a unit flush against the number, uncommaed large numbers), rewrites the dose safely, offers the likely value when 10× outside the usual range and spells it back.","categories":["Readbacks and calculators"],"files":[{"path":"registry/default/dose-input/dose-input.tsx","type":"registry:ui"},{"path":"registry/default/dose-input/dose.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Ten-fold medication dose error","mistakes":["trailing zero dose","mg vs mcg","medication dose typo","ismp error prone abbreviations","decimal point dose"],"aliases":["dose","dosage","medication","prescription","mg mcg","ehr","pharmacy","healthtech","units"],"flows":["forms"],"fields":["number"],"summary":"Flags error-prone dose notation as it is typed and doses ten times outside the usual range, so a slipped decimal isn't read as a ten-fold dose.","server":"Validate doses against the prescribing system's drug database and require clinical review; this field is a typing aid.","catches":["Trailing zeros (1.0 mg) and missing leading zeros (.5 mg)","Error-prone abbreviations: U, IU, µg, cc","A unit typed flush against the number, and large numbers without separators","Doses 10× outside the range you pass (mg typed for mcg, a slipped decimal)","The dose spelled back in words"],"limits":["Not a clinical decision tool: it does not know the patient, the drug or safe dosing","Range checks only work with the range you supply","Not a medical device; dosing must be checked by a qualified professional"],"a11y":["Visible <label>; aria-describedby to an aria-live=\"polite\" hint","aria-invalid when the dose can't be read","Fixes and suggested values are buttons with visible text, returning focus to the input"],"rules":{"verified":"2026-10-17","sources":[{"label":"ISMP List of Error-Prone Abbreviations, Symbols, and Dose Designations","url":"https://online.ecri.org/hubfs/ISMP/Resources/ISMP_ErrorProneAbbreviation_List.pdf"}],"note":"Informational, not medical advice. Checked that the ISMP list covers trailing and missing leading zeros, U, IU and µg. Not every flagged pattern was matched line by line."}}},{"name":"leftover-text-check","type":"registry:ui","title":"Leftover Text Check","description":"Text check that flags chatbot chatter and refusals, unfilled placeholders ([product], XX%), TK and TODO markers and paragraphs pasted twice, without false alarms on [1], [sic] or Markdown links, and asks twice before publishing with leftovers.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/leftover-text-check/detect.ts","type":"registry:ui"},{"path":"registry/default/leftover-text-check/leftover-text-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Chatbot reply published as listing","mistakes":["chatgpt text left in post","placeholder published by mistake","as an ai language model in content","forgot to fill in template brackets","paragraph pasted twice"],"aliases":["ai generated text","chatgpt","product description","blog post","cms editor","textarea","lorem ipsum","tk","todo","publish"],"flows":["forms","messaging"],"fields":["text"],"summary":"Catches chatbot chatter, refusals, unfilled placeholders and TODOs left in text before it is published.","catches":["Chatbot chatter and refusals (\"Certainly! Here's…\", \"I cannot fulfill that request\")","Unfilled placeholders such as [product], [task 1], XX%","TK and TODO markers","A paragraph pasted twice","Publishing with leftovers: asks for a second press"],"limits":["Phrase lists are English-first; extend them with the phrases prop","Can't tell intentional brackets or repetition in every case","Doesn't check facts or tone"],"a11y":["Textarea gets aria-invalid when leftovers are found and is described by the summary","Each finding has text and an icon that is aria-hidden","Focus moves to the next finding or back to the field after a fix","Animations are skipped under reduced motion"]}},{"name":"ai-citation-check","type":"registry:ui","title":"AI Citation Check","description":"Flags tells of made-up references (DOI prefix not matching the journal, a dash in a DOI, impossible years, leftover oaicite or 【4†source】 markers, unattributed quotes), looks DOIs, cases and links up via a pluggable verify, and marks anything unconfirmed Unverified.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/ai-citation-check/ai-citation-check.tsx","type":"registry:ui"},{"path":"registry/default/ai-citation-check/citations.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Unverified references slip into AI text","mistakes":["chatgpt made up citations","hallucinated references","fake doi in paper","ai invented court cases","oaicite left in text"],"aliases":["citations","references","bibliography","doi","crossref","courtlistener","legal brief","hallucination","fact check","ai writing"],"flows":["forms"],"fields":["text","url"],"summary":"Flags references in AI-written text that look made up and looks each one up, so fake cases and DOIs are caught before they go out.","catches":["DOI prefixes that do not belong to the publisher of the journal named","Long dashes inside a DOI and leftover AI citation markers, with one-tap fixes","Case reporter/year combinations that cannot exist, and years in the future","Long quotes with no attribution","References your verify function could not confirm, shown as Unverified rather than fine"],"limits":["Without a verify function it only checks the text for tells; it cannot confirm anything exists","A reference that exists may still be misquoted or not support the claim","Publisher-prefix and reporter heuristics cover common cases, not every journal or court","Submitting is still possible after a second press"],"a11y":["Textarea gets aria-invalid and aria-describedby pointing at the summary","Lookup progress is announced in a role=\"status\" region; the submit confirmation uses role=\"alert\"","\"Show\" moves focus to the reference in the list","Statuses use text and icons, not colour alone; motion respects prefers-reduced-motion"]}},{"name":"k8s-resource-input","type":"registry:ui","title":"K8s Resource Input","description":"Requests/limits fields that read values in plain units and catch CPU 500 meaning 500 cores, 400m memory meaning 0.4 bytes, unit-case slips, a limit under its request, CPU finer than 1m and requests above any node, with fixes, the QoS class and resources: paste.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/k8s-resource-input/k8s-quantity.ts","type":"registry:ui"},{"path":"registry/default/k8s-resource-input/k8s-resource-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pod asks for 500 CPU cores","mistakes":["kubernetes cpu limit wrong unit","400m memory means bytes","mi vs mb in k8s yaml","pod stuck pending resources","limit lower than request"],"aliases":["kubernetes","k8s","resources requests limits","cpu memory","millicores","Mi Gi","deployment yaml","qos class","helm values"],"flows":["devtools","forms"],"fields":["number","code"],"summary":"Reads Kubernetes CPU and memory requests back in plain units, so 500 meaning 500 cores or 400m meaning 0.4 bytes is caught before deploy.","catches":["CPU 500 meaning 500 cores","Memory 400m meaning 0.4 bytes, and MB/Gb style unit slips","A limit below its request (1G vs 1Gi)","CPU finer than 1m, which the API rounds","Requests larger than your biggest node"],"limits":["Node size comes from the node prop; it doesn't query the cluster","Doesn't check LimitRanges, ResourceQuotas or ephemeral storage","QoS class is computed for this container only, not the whole pod"],"a11y":["Each field has a visible label and is described by its plain-units readback","aria-invalid on flagged fields; status uses role=status","Focus returns to a nearby control after a fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"Kubernetes: Resource Management for Pods and Containers","url":"https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"}],"note":"Informational. Confirmed the case-sensitive suffix warning (400m of memory is 0.4 bytes). Rounding and QoS rules were not re-checked this round."}}},{"name":"connection-string-input","type":"registry:ui","title":"Connection String Input","description":"Masks the password when not editing, reads back user, host, database and TLS mode, and catches unencoded @ # / % in passwords, localhost in production, missing or weak sslmode, unknown libpq parameters, wrong-database ports, Redis/MongoDB without TLS and .env quoting.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/connection-string-input/connection-string-input.tsx","type":"registry:ui"},{"path":"registry/default/connection-string-input/connection-string.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Database URL leaks or skips TLS","mistakes":["database url wont connect","special character in db password","sslmode disable in production","postgres connection string password leaked","localhost database in prod"],"aliases":["database url","DATABASE_URL","postgres uri","mongodb+srv","redis url","dsn","sslmode","connection string","prisma","supabase"],"flows":["devtools","forms"],"fields":["url","secret"],"summary":"Masks a database URL's password and reads back the host, user and TLS mode the driver will actually use, so it doesn't fail or connect insecurely.","server":"Keep connection strings in a secrets manager and enforce TLS on the database server itself.","catches":["Unencoded @, #, / or % in the password, naming the host a strict parser would use","Missing, weak or misspelled sslmode, and sslrootcert=system with a weaker mode","localhost in production and a port that belongs to another database","redis:// and MongoDB URLs without TLS (including mongodb+srv with tls=false)","Quotes or KEY= pasted from a .env file"],"limits":["Parses the URL text; it doesn't try to connect or check credentials","Covers PostgreSQL (libpq), MySQL, MongoDB and Redis URL forms; other drivers may parse differently","Masking is visual: the full password is still in the value you receive"],"a11y":["Visible <label>; aria-invalid and aria-describedby to a role=\"status\" summary","Readback is a section labelled by its heading","Copy-masked button announces \"Copied\" in an aria-live=\"polite\" span","After a fix, focus moves to the next fix or back to the field"],"rules":{"verified":"2026-10-17","sources":[{"label":"PostgreSQL libpq: connection strings and sslmode","url":"https://www.postgresql.org/docs/current/libpq-connect.html"}],"note":"Informational. Checked libpq sslmode values (prefer is the default) and that sslrootcert=system requires verify-full. MongoDB, MySQL and Redis conventions were not re-checked."}}},{"name":"package-name-check","type":"registry:ui","title":"Package Name Check","description":"npm and pip install-command check that flags lookalikes of popular packages (loadsh, crossenv, jeIlyfish), import names that aren't the package (sklearn, cv2, bs4, PIL), built-ins from the registry, unpublishable names, and via lookup, missing or brand-new names.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/package-name-check/package-name-check.tsx","type":"registry:ui"},{"path":"registry/default/package-name-check/package-names.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a typosquatted package before install","mistakes":["npm typosquat package","ai hallucinated package name","pip install wrong package","malicious npm package lookalike","sklearn vs scikit-learn"],"aliases":["npm install","pip install","requirements.txt","package.json","typosquatting","supply chain","slopsquatting","dependency check","yarn add"],"flows":["devtools","danger-zone"],"fields":["code","text"],"summary":"Reads an npm or pip install command package by package and flags typosquats, wrong import names and packages that don't exist yet.","server":"Pin dependencies with a lockfile and scan installs in CI; a client check is advisory.","catches":["Lookalikes of popular packages, including capital I for l","Import names used as install names (sklearn, cv2, bs4, PIL)","Node or Python built-ins installed from the registry (fs, crypto, json)","Names npm or PyPI can't publish","Names that don't exist (AI-hallucinated) or appeared days ago, through your lookup"],"limits":["The popular-package list is a curated sample; pass your own","Existence and age checks need your registry lookup","Doesn't scan package code or check maintainers"],"a11y":["Visible label; ecosystem picker is a labelled radiogroup","Textarea is described by the status (role=status)","Focus returns to a nearby control after a fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"npm: package.json name rules","url":"https://docs.npmjs.com/cli/v10/configuring-npm/package-json"},{"label":"PyPA: name normalization","url":"https://packaging.python.org/en/latest/specifications/name-normalization/"}],"note":"Informational. Confirmed npm's 214-character and no-uppercase rules and PyPI's normalization of runs of . - _ to a single -. The incident examples were not re-verified."}}},{"name":"cache-control-input","type":"registry:ui","title":"Cache Control Input","description":"Reads back who stores the response and for how long (browser, CDN via CDN-Cache-Control or Surrogate-Control, other shared caches) and flags per-user pages a CDN can cache, public cookie-setting responses, no-cache mistaken for no-store, unsafe immutable and missing Vary.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/cache-control-input/cache-control-input.tsx","type":"registry:ui"},{"path":"registry/default/cache-control-input/cache-control.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Private page cached by the CDN","mistakes":["cdn caching user data","no-cache vs no-store","cache control header wrong","stale html after deploy","vary header missing"],"aliases":["cache-control","http headers","cdn","cloudflare","vercel","s-maxage","max-age","immutable","surrogate-control"],"flows":["devtools"],"fields":["code","text"],"summary":"Reads back who will store a response and for how long, so a per-user page isn't cached by the CDN and HTML isn't stuck for days.","catches":["Personalised pages that a CDN or shared cache may store","Responses that set a cookie but are marked public","no-cache used where no-store was meant","immutable on URLs without a content hash, and long max-age on HTML","A missing Vary for compressed or negotiated bodies; one-tap fixes for each"],"limits":["Reads the header text you give it; real CDN behaviour also depends on its own configuration","Whether a page is personalised and the URL path are inputs you provide","Vendor-specific headers beyond CDN-Cache-Control and Surrogate-Control are not modelled"],"a11y":["Labelled textarea with aria-invalid and aria-describedby to a role=\"status\" summary","Readback is a section labelled by its heading","After a fix, focus moves to the next fix button or back to the field","Severity is shown with icons and text, not colour alone"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 9111: HTTP Caching","url":"https://www.rfc-editor.org/rfc/rfc9111"},{"label":"RFC 9213: Targeted HTTP Cache Control (CDN-Cache-Control)","url":"https://www.rfc-editor.org/rfc/rfc9213"},{"label":"RFC 8246: HTTP Immutable Responses","url":"https://www.rfc-editor.org/rfc/rfc8246"}],"note":"Informational; check the official source and your CDN's documentation. Surrogate-Control handling follows vendor convention and was not checked against a standard."}}},{"name":"mrz-paste-input","type":"registry:ui","title":"MRZ Paste Input","description":"Passport MRZ field (ICAO 9303 TD3) that validates each check digit and the composite, catches OCR slips like O for 0 and K for <, swapped lines, CRLF and stray spaces, proposes the likeliest fix for a bad digit, flags expired or soon-expiring passports, then fills the fields.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/mrz-paste-input/mrz-paste-input.tsx","type":"registry:ui"},{"path":"registry/default/mrz-paste-input/mrz.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch misread passport details before booking","mistakes":["passport mrz check digit","ocr passport wrong number","passport expiry check","mrz parse error","passport details typo"],"aliases":["passport","mrz","icao 9303","td3","passport scan","kyc","flight booking","visa form","ocr"],"flows":["forms","checkout"],"fields":["id-number","name","datetime"],"summary":"Validates a passport MRZ (ICAO 9303 TD3) check digit by check digit and catches OCR slips before details reach a booking or KYC form.","server":"Re-validate the MRZ and verify identity through your KYC provider on the server; don't log full MRZ lines.","catches":["Wrong check digits for number, birth date, expiry, personal number and composite","OCR slips such as O for 0 and K for <","Swapped lines, CRLF and stray spaces","Likeliest fix for a wrong digit, cross-checked against the composite","Expired or soon-expiring passport"],"limits":["Passports (TD3) only, not ID cards (TD1/TD2) or visas","Valid check digits don't prove the passport is genuine","Names in the MRZ are transliterated and may be truncated"],"a11y":["Visible label; textarea has aria-invalid when invalid and is described by the result","Each check has an icon with role=img and an aria-label (checked, pending, failed)","Focus moves to the first fix after parsing"],"rules":{"verified":"2026-10-17","sources":[{"label":"ICAO Doc 9303 Part 3 (check digits, weights 7-3-1, modulus 10)","url":"https://www.icao.int/sites/default/files/publications/DocSeries/9303_p3_cons_en.pdf"},{"label":"ICAO Doc 9303 Part 4 (TD3 lower line and composite check digit)","url":"https://www.icao.int/sites/default/files/publications/DocSeries/9303_p4_cons_en.pdf"}],"note":"Informational. Confirmed the 7-3-1 weighting and the composite over positions 1–10, 14–20 and 22–43. The soon-expiring threshold is a product choice, not ICAO."}}},{"name":"curl-pipe-guard","type":"registry:ui","title":"Curl Pipe Guard","description":"Before a curl | sh (or wget, or iwr | iex) one-liner is copied or run, shows the script's real host and flags plain http, raw IPs, shortened links, a host off the product's docs domain, sudo, -k/--insecure and missing checksum or -f; offers safer step-by-step commands.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/curl-pipe-guard/curl-pipe-guard.tsx","type":"registry:ui"},{"path":"registry/default/curl-pipe-guard/curl-pipe.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Ran an untrusted install script","mistakes":["curl pipe bash unsafe","install script from wrong domain","curl sh security","iwr iex powershell","curl -k insecure"],"aliases":["curl | sh","curl bash","install script","one-liner","wget","powershell iex","terminal","copy command","docs code block"],"flows":["devtools","danger-zone"],"fields":["code","url"],"summary":"Shows where a curl | sh install script really comes from and offers download, inspect and verify steps instead, before an untrusted script runs.","catches":["Plain http:// and raw IP addresses","Link shorteners and hosts that aren't the product's own domain","sudo and -k/--insecure","No checksum and no -f (fail on HTTP errors)","A safer download, inspect, verify and run sequence to copy instead"],"limits":["Parses the command text; it cannot tell whether the script itself is safe","Command forms it can't parse are flagged as unparsed rather than analysed","A matching domain can still serve a compromised script"],"a11y":["Copy mode is a role=\"radiogroup\" with roving tabIndex and arrow keys","HTTPS vs HTTP is shown with icons and text","Findings are written out in text"]}},{"name":"port-expose-guard","type":"registry:ui","title":"Port Expose Guard","description":"Port mapping check for docker run, compose and Kubernetes Services that says who can reach each port and flags databases on 0.0.0.0 (which Docker opens ahead of ufw), host networking, -P, NodePort or public LoadBalancers, wrong container ports and unquoted 22:22 YAML, with fixes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/port-expose-guard/parse-ports.ts","type":"registry:ui"},{"path":"registry/default/port-expose-guard/port-expose-guard.tsx","type":"registry:ui"},{"path":"registry/default/port-expose-guard/ports.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a database port being opened to the internet","mistakes":["docker port exposed publicly","docker bypasses ufw","postgres open to internet","nodeport exposed","compose ports wrong"],"aliases":["docker run -p","docker compose","ports","0.0.0.0","kubernetes service","loadbalancer","nodeport","firewall","ufw"],"flows":["devtools","danger-zone"],"fields":["code","number"],"summary":"Reads docker run, compose ports or a Kubernetes Service back as who can reach each port, so a database isn't published to the internet by accident.","server":"Enforce network policy and firewalls at the host or cloud level; never rely on a config review alone.","catches":["Database or admin ports bound to every interface","Docker's iptables rules bypassing ufw","Host networking, -P, NodePort or public LoadBalancer for internal services","Container port the image doesn't listen on (5432:5433)","Unquoted 22:22 that YAML 1.1 reads as a number","On Docker older than 28, 127.0.0.1 binds reachable from the LAN"],"limits":["Reads the config you paste; it can't see cloud security groups or host firewalls","${VAR} binds without defaults can't be judged until run time","Sensitive-port list is a default; pass your own"],"a11y":["Visible label; textarea is described by the readback","Announcements go to a polite live region","Reach is shown as text with icons aria-hidden; focus moves to the first fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"Docker: Port publishing and mapping","url":"https://docs.docker.com/engine/network/port-publishing/"},{"label":"Docker: Packet filtering and firewalls (Docker and ufw)","url":"https://docs.docker.com/engine/network/packet-filtering-firewalls/"}],"note":"Informational. Confirmed: before 28.0.0, hosts on the same L2 segment can reach ports published to localhost; Docker's rules divert traffic before ufw. The YAML 1.1 sexagesimal reading was not re-checked."}}},{"name":"photo-spec-check","type":"registry:ui","title":"Photo Spec Check","description":"Photo upload check against portal presets (UPSC, SSC, NTA, NEET, US visa) or your spec: real file type, KB, pixels, shape, print size, over-compression, face size and centring, background and colour, with a canvas fix that crops, resizes and re-encodes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/photo-spec-check/photo-canvas.ts","type":"registry:ui"},{"path":"registry/default/photo-spec-check/photo-spec-check.tsx","type":"registry:ui"},{"path":"registry/default/photo-spec-check/photo-spec.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent an exam photo being rejected after upload","mistakes":["photo size kb exam form","passport photo rejected","image dimensions wrong upload","background not white photo","resize photo under 50kb"],"aliases":["photo upload","upsc","ssc","neet","nta","passport photo","visa photo","image resize","compress jpg","signature upload"],"flows":["india","forms"],"fields":["file"],"summary":"Checks an exam, passport or visa photo against the portal's rules before upload and fixes size, crop and file weight in the browser.","catches":["File type that doesn't match its name","File size outside the KB window, and over-compression","Pixel size, aspect ratio or print size out of spec","Face too small or off-centre, when you pass a face box","Background that isn't white, or a black-and-white photo"],"limits":["Presets reflect the sources below and change often; portals may also check things this can't (glasses, expression)","Face checks need a face box from your detector","The canvas fix re-encodes the photo"],"a11y":["Choose button is described by the spec; the hidden file input is removed from tab order","Status lines have text; icons are aria-hidden","Focus moves to the fix or choose button after reading"],"rules":{"verified":"2026-10-17","sources":[{"label":"US visa: Digital Image Requirements (travel.state.gov)","url":"https://travel.state.gov/content/travel/en/us-visas/visa-information-resources/photos/digital-image-requirements.html"},{"label":"UPSC: photo and signature upload instructions","url":"https://upsconline.nic.in/caf/assets/PDF/instruction-photo-signature-upload-upsc.pdf"}],"note":"Informational; check the official source. Confirmed: US visa JPEG, square 600–1200 px, ≤ 240 kB, compression ≤ 20:1; UPSC colour JPG named photo, 20–200 KB. SSC, NTA and NEET presets were not re-checked this round."}}},{"name":"doc-upload-match","type":"registry:ui","title":"Doc Upload Match","description":"Checks a file against the document asked for (Aadhaar back instead of front, Aadhaar in the PAN slot, a password-locked e-Aadhaar PDF, a multi-page PDF, a tiny, blurry or HEIC photo, a screenshot of a screenshot) using in-browser signals plus an optional classify hook.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/doc-upload-match/doc-signals.ts","type":"registry:ui"},{"path":"registry/default/doc-upload-match/doc-upload-match.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong document uploaded for KYC","mistakes":["aadhaar back uploaded instead","password protected e-aadhaar","kyc document rejected","blurry document upload","heic upload not supported"],"aliases":["kyc upload","document upload","e-aadhaar pdf","pan upload","file upload","dropzone","id proof","address proof","onboarding"],"flows":["india","signup-login","forms"],"fields":["file"],"summary":"Checks an uploaded file against the document asked for, so a KYC upload isn't rejected days later for being the wrong side, locked or blurry.","server":"Re-validate document type, size and content on the server or with your KYC provider; client checks are hints.","catches":["Password-locked PDFs, with how the e-Aadhaar password is formed","Multi-page PDFs where one page is expected","Images too small, blurry or in HEIC format","Screenshots of screenshots","The wrong document or side, when you provide a classify function"],"limits":["Without a classify hook it can't tell which document is in the image","Blur and size checks are heuristics and can misjudge some photos","Runs in the browser; the file still goes wherever your onAccept sends it"],"a11y":["Labelled file picker; focus returns to it after a rejection","The preview image has empty alt (decorative); status is given in text","Checking and result messages use icons plus text"],"rules":{"verified":null,"sources":[{"label":"UIDAI e-Aadhaar FAQs","url":"https://uidai.gov.in/en/contact-support/have-any-question/284-faqs/aadhaar-online-services/e-aadhaar.html"}],"note":"Not verified on 2026-10-17: the UIDAI site did not respond. The e-Aadhaar password rule (first 4 letters of the name in capitals + birth year) is unchecked."}}},{"name":"ai-paste-guard","type":"registry:ui","title":"AI Paste Guard","description":"Reads the HTML clipboard data pasted into an AI prompt box and holds it when it carries invisible text (same-colour, zero-size, display:none, off-screen, comments, alt/title instructions, Unicode tag payloads), shows each hidden passage and can paste only the visible text.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/ai-paste-guard/ai-paste-guard.tsx","type":"registry:ui"},{"path":"registry/default/ai-paste-guard/paste-scan.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Hidden prompt injection in a paste","mistakes":["prompt injection hidden text","white text in pasted content","llm prompt injection paste","invisible instructions chatbot","unicode tag characters"],"aliases":["prompt injection","ai chat","chatbot input","llm","prompt box","clipboard","paste","ai safety","chat composer"],"flows":["messaging","devtools"],"fields":["text"],"summary":"Holds a paste into an AI prompt box when it carries text the person could not see, so hidden prompt injections are noticed before they are sent.","server":"Treat all user and retrieved content as untrusted on the model side; this paste check does not replace server-side prompt-injection defences or tool permissions.","catches":["White-on-white, transparent, zero-size, display:none, off-screen and clipped text","HTML comments and alt/title text that contains instructions","Unicode tag-character payloads, decoded back to readable text","Phrasing that reads like a prompt injection (\"ignore previous instructions\")","Whether each hidden passage would also reach a plain-text box"],"limits":["Only sees pastes with an HTML clipboard flavour; plain-text pastes and typed text are not analysed","Ignores known noise (clipboard and framework comments, screen-reader-only labels), so text hidden that way can be missed","Light text from dark-mode pages is inferred heuristically and can be misjudged","Does not protect the model from injections in documents, tools or retrieved pages"],"a11y":["Visible <label> tied to the field; the held panel is a role=\"group\" labelled by its heading","Escape cancels a held paste; focus moves to the primary action and back to the field","Status is announced through a role=\"status\" region","Icons are aria-hidden; every state is described in text"]}},{"name":"ssh-key-input","type":"registry:ui","title":"SSH Key Input","description":"Public-key field that flags a pasted private key loudly (rotate it; extract the public half in one tap), catches DSA and short RSA keys, wrapped or cut-off keys, wrong type prefixes, SSH2 export format and duplicates, and shows the SHA256 fingerprint as ssh-keygen does.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/ssh-key-input/ssh-key-input.tsx","type":"registry:ui"},{"path":"registry/default/ssh-key-input/ssh-key.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Private SSH key pasted where a public key belongs","mistakes":["pasted private key instead of public","ssh key invalid format","rsa key too short","id_rsa vs id_rsa.pub","ssh key fingerprint"],"aliases":["ssh key","public key","id_ed25519.pub","authorized_keys","deploy key","github ssh","fingerprint","ssh-keygen","settings"],"flows":["devtools","account-settings"],"fields":["secret","code"],"summary":"Catches a private SSH key pasted where a public key belongs, plus weak, truncated or malformed public keys, before they're saved.","server":"Re-validate the key server-side, reject private keys and weak types, and never log submitted key material.","catches":["A private key pasted instead of a public one, with rotate advice and the public half extracted when possible","DSA keys and RSA keys under minRsaBits (default 2048; 3072 recommended)","Keys wrapped across lines or cut off","Missing or mismatched type prefix","RFC 4716 (SSH2) export format, converted","A key already on the account"],"limits":["Checks key format and size, not who owns the key","A pasted private key has already been exposed to this page; advise rotation regardless","Certificate and hardware-key (sk-) variants may be reported generically"],"a11y":["Visible <label>; aria-invalid on blocking issues; hint and report via aria-describedby","Private-key warning uses role=alert; summary is role=status","After a fix, focus moves to the next fix or back to the field"]}},{"name":"jwt-inspect","type":"registry:ui","title":"JWT Inspect","description":"Local JWT decoder that never sends the token anywhere and flags alg none, HS vs RS key confusion, an expired token or future nbf (in the reader's zone), millisecond timestamps, a missing aud or iss, very long lifetimes, and secrets or personal data readable in the payload.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/jwt-inspect/jwt-inspect.tsx","type":"registry:ui"},{"path":"registry/default/jwt-inspect/jwt.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps keep live tokens out of online decoders","mistakes":["jwt decode online unsafe","jwt expired token","alg none jwt","jwt exp milliseconds","secrets in jwt payload"],"aliases":["jwt","jwt decoder","jwt.io","bearer token","access token","id token","claims","exp","auth"],"flows":["devtools"],"fields":["secret","code"],"summary":"Decodes a JWT on the page, without sending it anywhere, and flags alg none, key confusion, expiry and readable secrets.","server":"Verify the signature, algorithm, issuer, audience and expiry on the server with a JWT library and a pinned key.","catches":["alg none, and algorithms outside your allow-list","HS/RS confusion (HMAC where an RSA key is expected)","Expired token or nbf in the future, shown in local time","Timestamps in milliseconds instead of seconds","Missing or wrong aud/iss, very long lifetime","Secrets or personal data readable in the payload"],"limits":["Decodes only; the signature is never verified","Secret and PII detection is pattern-based","Doesn't make a token safe to use; your server must verify it"],"a11y":["Visible label; input has aria-invalid and is described by the local-only note and the report","Report uses role=status; icons are aria-hidden","Clear button has an aria-label and returns focus to the field"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 8725: JSON Web Token Best Current Practices","url":"https://www.rfc-editor.org/rfc/rfc8725"},{"label":"RFC 7519: JSON Web Token","url":"https://www.rfc-editor.org/rfc/rfc7519"}],"note":"Informational. Checked RFC 8725's guidance on alg none and algorithm substitution; individual thresholds (lifetime, leeway) are product defaults, not standards."}}},{"name":"cert-paste-check","type":"registry:ui","title":"Cert Paste Check","description":"Identifies certificates, private keys and CSRs in a paste and catches a chain out of order or missing its intermediate, expired or expiring certificates, a hostname the SANs don't cover, self-signed certificates in production and a non-matching key.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/cert-paste-check/cert-paste-check.tsx","type":"registry:ui"},{"path":"registry/default/cert-paste-check/pem.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Broken TLS chain in production","mistakes":["ssl certificate chain wrong order","missing intermediate certificate","private key does not match cert","certificate expired","san doesnt cover domain"],"aliases":["ssl certificate","tls","pem","csr","private key","fullchain","intermediate","lets encrypt","https"],"flows":["devtools"],"fields":["secret","code"],"summary":"Tells a certificate, key and CSR apart as they're pasted and catches chain, expiry, hostname and key-mismatch mistakes before a TLS install breaks.","server":"Validate the full chain and key on the server or in your TLS tooling before deploying; this is a paste-time check.","catches":["A private key or CSR pasted where a certificate was expected","A chain out of order or missing its intermediate","Expired or soon-expiring certificates (warnDays)","A hostname the SANs don't cover, including wildcard and CN-only traps","A self-signed certificate in production, and a private key that doesn't match"],"limits":["Parses the DER structure only; signatures are not verified and revocation is not checked","Does not check that the root is trusted by browsers","Everything runs in the browser, but a pasted private key is still sensitive: avoid logging the field"],"a11y":["Labelled textarea with aria-invalid and aria-describedby to the hint and report","Summary in a role=\"status\" region; serious problems use role=\"alert\"","After a fix, focus moves to the next fix or back to the field","Severity shown with icons and text"]}},{"name":"chmod-input","type":"registry:ui","title":"Chmod Input","description":"Reads any mode (644, 0755, u+x, -rwxr-xr-x) back as rwx and plain words, and flags world-writable files, setuid/setgid, keys and .env files others can read, a writable ~/.ssh or authorized_keys, execute bits on data, 775 typed for 755 and short or decimal modes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/chmod-input/chmod-input.tsx","type":"registry:ui"},{"path":"registry/default/chmod-input/chmod.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"World-writable file or exposed key","mistakes":["chmod 777 bad","ssh key permissions too open","file permissions wrong","775 vs 755","env file readable"],"aliases":["chmod","file permissions","rwx","unix permissions","octal mode","755","644","linux","setuid"],"flows":["devtools","danger-zone"],"fields":["code","number"],"summary":"Reads a Unix file mode back in plain words and flags permissions that leak data or lock people out, like a world-readable key.","server":"Set and audit permissions in your deployment tooling; this field only advises.","catches":["World-writable files and directories, and setuid/setgid bits","Private keys and .env files readable by group or others","~/.ssh or authorized_keys writable by others (sshd StrictModes rejects these)","Execute bits on data files, and 775 typed where 755 was likely meant","Short, symbolic or decimal-looking modes, read back exactly"],"limits":["Judges by the path and kind you pass; it can't see the real file, owner or ACLs","Advice assumes a typical Linux/OpenSSH setup; shared-group setups may be intentional (groupShared)"],"a11y":["Visible <label>; aria-invalid and aria-describedby to the readback and report","Summary in a role=\"status\" region; an unreadable mode uses role=\"alert\"","Permission grid checkboxes have aria-labels naming who and which right","After a fix, focus moves to the next fix or back to the field"],"rules":{"verified":"2026-10-17","sources":[{"label":"OpenBSD sshd_config(5): StrictModes","url":"https://man.openbsd.org/sshd_config"},{"label":"OpenBSD ssh(1): file permissions","url":"https://man.openbsd.org/ssh.1"}],"note":"Informational. Checked: StrictModes (default yes) checks file modes and ownership before login; authorized_keys must not be writable by others. Other per-file recommendations are conventions, not checked against a standard."}}},{"name":"redirect-uri-input","type":"registry:ui","title":"Redirect URI Input","description":"Checks a redirect URI the way an OAuth server does (exact string match), highlighting a differing trailing slash, scheme, case, www or port against the registered list with a one-tap swap, plus localhost in production, wildcards, fragments and stray whitespace.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/redirect-uri-input/redirect-uri-check.ts","type":"registry:ui"},{"path":"registry/default/redirect-uri-input/redirect-uri-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"OAuth redirect_uri_mismatch error","mistakes":["redirect_uri_mismatch","oauth callback url wrong","trailing slash redirect uri","google oauth redirect error","localhost redirect production"],"aliases":["oauth","redirect uri","callback url","google sign in","nextauth","auth0","openid","sso","login with google"],"flows":["devtools","signup-login"],"fields":["url"],"summary":"Catches OAuth redirect URIs that are close to but not exactly the registered one, plus unsafe ones like localhost in production or wildcards.","server":"The authorization server must compare redirect_uri by exact string match against the registered list.","catches":["Near-misses against registered URIs: trailing slash, http vs https, case, www, port","Side-by-side highlight of the differing characters, with a one-tap swap","localhost or loopback in a production client","Wildcards, a #fragment, default :443 port, empty ? and stray whitespace","Short custom schemes for native apps"],"limits":["Checks the string only; it cannot see what your provider actually has registered unless you pass it","Providers differ (some allow loopback port variation for native apps); follow your provider's rules","Not a substitute for server-side exact matching"],"a11y":["Visible <label> tied to the input; aria-invalid on errors","Status summary in a polite live region linked by aria-describedby","After a one-tap fix, focus returns to the input","Match state shown with icon and text"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 9700 – OAuth 2.0 Security Best Current Practice","url":"https://www.rfc-editor.org/rfc/rfc9700.html"}],"note":"Confirmed: RFC 9700 requires exact string matching of redirect URIs, except variable port numbers for localhost redirect URIs of native apps."}}},{"name":"csp-input","type":"registry:ui","title":"CSP Input","description":"Flags 'unsafe-inline' without a nonce, 'unsafe-eval', * / data: / https: in script-src, missing default-src, object-src or base-uri, unquoted keywords, a stray comma that splits the policy, JSONP-hosting allowlisted hosts, typos and report-only traps.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/csp-input/csp-check.ts","type":"registry:ui"},{"path":"registry/default/csp-input/csp-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"CSP that blocks nothing","mistakes":["content security policy unsafe-inline","csp not working","csp header syntax","csp report-only","xss csp bypass"],"aliases":["csp","content-security-policy","security headers","script-src","nonce","helmet","next.config headers","xss"],"flows":["devtools"],"fields":["code","text"],"summary":"Reads a Content-Security-Policy the way a browser does, so a policy that looks strict but blocks nothing doesn't ship.","server":"Send the CSP as a response header from your server and monitor violation reports; the policy is enforced by browsers, not by this field.","catches":["'unsafe-inline' without a nonce or hash, and 'unsafe-eval'","Wildcards, data: and https: in script-src","Missing default-src, object-src or base-uri","Unquoted keywords, missing semicolons and commas that split one header into two policies","Allowlisted hosts known to serve JSONP or AngularJS, and report-only mistakes"],"limits":["Reads the policy text; it does not see your pages or test real script loads","The JSONP/AngularJS host list is a snapshot and may be incomplete","A clean result doesn't mean the site is free of XSS"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" status","After a fix, focus moves to the next fix or back to the field","Severity uses icons and text, not colour alone"]}},{"name":"public-env-guard","type":"registry:ui","title":"Public Env Guard","description":".env check that lists every NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_ or PUBLIC_ variable reaching the browser and flags secret key shapes, Supabase service_role JWTs, passwords in URLs, secret names and high-entropy values, with a server-only rename.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/public-env-guard/public-env-guard.tsx","type":"registry:ui"},{"path":"registry/default/public-env-guard/scan-public-env.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a secret key shipped in the client bundle","mistakes":["next_public secret exposed","api key in frontend","vite env leaked","service role key client","stripe secret in browser"],"aliases":["next_public","vite_","react_app_",".env","env vars","supabase service_role","api key","client bundle","nextjs","expo"],"flows":["devtools","danger-zone"],"fields":["secret","code"],"summary":"Reads a .env the way the bundler does and flags secrets in public variables, so a secret key isn't shipped in the client bundle.","server":"Scan built bundles in CI for secrets and keep secret keys only in server-side environment variables.","catches":["Public-prefixed variables that reach the browser, per framework","Secret key shapes (for example Stripe secret keys) in public variables","Supabase service_role JWTs","Passwords inside connection URLs","Secret-sounding names and high-entropy values, while letting publishable keys through"],"limits":["Reads the .env text you paste; secrets set elsewhere (CI, hosting dashboard) aren't seen","High-entropy detection can flag harmless IDs","Renaming doesn't un-leak a key already shipped; rotate it"],"a11y":["Visible label; textarea has aria-invalid when errors exist and is described by the status","Status uses role=status; icons are aria-hidden","Focus returns to a nearby control after a fix"]}},{"name":"robots-txt-guard","type":"registry:ui","title":"Robots.txt Guard","description":"robots.txt checker that flags Disallow: / for production, staging leftovers, rules blocking CSS/JS Google renders with, relative or wrong-host sitemaps, typos and fields Google ignores, with one-tap fixes and a URL tester naming the deciding line.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/robots-txt-guard/check-robots.ts","type":"registry:ui"},{"path":"registry/default/robots-txt-guard/robots-txt-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Site de-indexed by robots.txt","mistakes":["disallow all robots txt","site not on google","robots blocks css js","staging robots in production","sitemap url wrong"],"aliases":["robots.txt","seo","disallow","sitemap","googlebot","crawl","indexing","search console"],"flows":["devtools"],"fields":["code","url"],"summary":"Catches robots.txt rules that would quietly de-index a site, like Disallow: / in production or blocked CSS and JS.","catches":["Disallow: / and staging leftovers in a production file","Rules that block CSS and JS asset paths used for rendering","Relative or wrong-host Sitemap lines","Directive typos and missing colons","Fields Google doesn't support (crawl-delay, noindex)","Which line decides a crawl for a given URL and user agent"],"limits":["Models Google's robots.txt rules; other crawlers interpret files differently","robots.txt controls crawling, not indexing; blocked URLs can still be indexed from links","Asset checks use the paths you pass"],"a11y":["Visible <label> on the textarea; aria-invalid on errors and a role=status issue count","Each issue has a button to select its line, with an aria-label","After a fix, focus moves to the next fix or back to the textarea","URL tester inputs have labels; results use icon plus text"],"rules":{"verified":"2026-10-17","sources":[{"label":"Google Search Central – How Google interprets the robots.txt specification","url":"https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt"}],"note":"Confirmed: Google supports user-agent, allow, disallow and sitemap (crawl-delay isn't supported) and uses the most specific matching user-agent group. Check the source for changes."}}},{"name":"image-tag-input","type":"registry:ui","title":"Image Tag Input","description":"Image reference field showing the implied docker.io/library/ and :latest, flags :latest or no tag in production, moving tags, typo'd or frozen registries, private images that fall back to Docker Hub, malformed digests and arch-specific tags, with fixes such as pinning a digest.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/image-tag-input/image-tag-input.tsx","type":"registry:ui"},{"path":"registry/default/image-tag-input/parse-image-ref.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent a deploy pulling the wrong image","mistakes":["docker latest tag production","docker image tag wrong","pin docker digest","image arch mismatch","docker hub fallback private image"],"aliases":["docker image","image tag","container","digest","sha256","registry","ghcr","ecr","docker pull","kubernetes"],"flows":["devtools"],"fields":["code","text"],"summary":"Reads a container image reference back the way Docker resolves it, so :latest, moving tags or a typo'd registry don't deploy the wrong image.","server":"Pin digests and enforce allowed registries with admission policy in the cluster or CI.","catches":[":latest or a missing tag for production","Moving tags such as stable or main","Typo'd or frozen registry hosts","Private image names that would resolve to Docker Hub","Malformed digests and arch-specific tags that won't run on the target platform"],"limits":["Parses the reference; it does not query a registry unless you pass resolveDigest","Registry typo and moving-tag lists are heuristics","Doesn't scan the image for vulnerabilities or verify signatures"],"a11y":["Visible label; input has aria-invalid on errors and is described by the readback and issues","Implied parts carry a title explaining they are implied","Issues use role=status; focus returns to a nearby control after a fix"]}},{"name":"column-mapper","type":"registry:ui","title":"Column Mapper","description":"Checks a column mapping against sample values: emails in the phone column, first and last names swapped, DD/MM read as MM/DD (with a readback of what a date becomes), the same field mapped twice and required fields left unmapped, each with a one-tap fix.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/column-mapper/column-mapper.tsx","type":"registry:ui"},{"path":"registry/default/column-mapper/column-sniff.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Spreadsheet imported into wrong fields","mistakes":["csv import wrong columns","date dd mm swapped import","first last name swapped csv","column mapping excel import","required field not mapped"],"aliases":["csv import","excel upload","column mapping","bulk import","spreadsheet","data import","field mapping","xlsx"],"flows":["forms"],"fields":["file","text"],"summary":"Checks a spreadsheet column mapping against the actual values, so emails don't land in the phone field and DD/MM dates aren't read as MM/DD.","server":"Validate types and required fields again on the server during import.","catches":["Values that don't fit the mapped field (an email in Phone)","First and last name columns swapped, using a first-name list","Day/month order misread, with a readback of what a sample date becomes","The same field mapped twice, and required fields left unmapped"],"limits":["Works from the sample rows you pass; problems further down the file are not seen","Name-swap detection depends on the firstNames list and can miss uncommon names"],"a11y":["Each column uses a native <select>","After a fix, focus moves to the affected select","Issues use icons plus text, and each fix and keep action is a button"]}},{"name":"boolean-query-input","type":"registry:ui","title":"Boolean Query Input","description":"Shows how the engine groups AND and OR (implied brackets highlighted) and offers the grouping you meant; catches unclosed quotes and brackets, curly quotes, [ ] used as brackets, lowercase and/or and dangling operators, each with a one-tap fix.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/boolean-query-input/boolean-query-input.tsx","type":"registry:ui"},{"path":"registry/default/boolean-query-input/boolean-query.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Boolean search returns the wrong people","mistakes":["boolean search and or grouping","unclosed quote search","recruiter boolean string","brackets in boolean query","linkedin boolean search"],"aliases":["boolean search","search query","and or not","recruiter search","advanced search","naukri","linkedin","sourcing","filter"],"flows":["forms"],"fields":["text"],"summary":"Shows how a search engine will group AND and OR in a boolean query, so a search doesn't quietly return the wrong people.","catches":["Mixed AND/OR without brackets, read back with the engine's implied grouping","Unclosed quotes or brackets, and curly quotes","Square brackets used instead of parentheses","Lowercase and/or that engines search as words","Dangling operators at the start or end"],"limits":["Assumes AND binds first unless you set precedence; engines differ","Does not know engine-specific syntax beyond AND, OR, NOT, quotes and brackets"],"a11y":["Visible <label>; the input is described by the readback and status","Status and issues are aria-live=\"polite\"","Fix buttons have aria-labels with the exact query they apply","Dismiss button has an aria-label"]}},{"name":"cors-origin-input","type":"registry:ui","title":"CORS Origin Input","description":"Catches * with credentials, null, trailing slashes or paths, missing schemes, default ports and http or localhost in production, with one-tap fixes and a tester that names the near miss blocking an origin.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/cors-origin-input/cors-origin-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"CORS allow-list never matches","mistakes":["cors error trailing slash","access-control-allow-origin star credentials","cors origin not allowed","cors null origin","localhost cors production"],"aliases":["cors","allowed origins","access-control-allow-origin","api config","express cors","preflight","credentials"],"flows":["devtools"],"fields":["url"],"summary":"Checks a CORS allow-list for origins that never match or open the API to every site, and tests an origin against it.","server":"Enforce CORS in the server or gateway with exact origin matching, and authenticate API requests independently of CORS.","catches":["* combined with credentials, and the null origin","Trailing slashes or paths, which never match an Origin header","Missing scheme and explicit default ports","http:// or localhost origins in production","Near misses when testing an origin against the list"],"limits":["Checks the list text; your server's CORS middleware may normalise or match differently","CORS controls browsers only; it is not access control for the API"],"a11y":["Labelled add field; origins are a labelled list with remove buttons named per origin","aria-invalid and aria-describedby to an aria-live=\"polite\" issues region","Severity is stated in text (\", error\" / \", warning\"), not just colour","Enter adds an origin; focus moves sensibly after removals and fixes"]}},{"name":"webhook-url-input","type":"registry:ui","title":"Webhook URL Input","description":"Webhook URL field that judges the URL for its environment (ngrok, tunnel, localhost or private IP is an error in production, a hint in test), flags staging hosts, plain http and missing paths, and diffs against the last saved URL to catch www or trailing-slash-only changes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/webhook-url-input/webhook-url-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Webhooks pointed at localhost","mistakes":["ngrok webhook in production","webhook url redirect","webhook not receiving","localhost webhook","stripe webhook url wrong"],"aliases":["webhook url","endpoint url","callback","ngrok","stripe webhooks","razorpay webhook","dodo payments","integrations","settings"],"flows":["devtools","account-settings"],"fields":["url"],"summary":"Judges a webhook URL for the environment it's saved to and warns when an edit only changed www or a trailing slash, which senders treat as a failed redirect.","server":"Resolve and validate webhook targets server-side (block private ranges and metadata IPs) to prevent SSRF.","catches":["Tunnel, localhost, private-IP and cloud-metadata hosts (error in production, hint in test)","Public request inspectors where anyone can read the payloads","Staging hosts saved to production","Plain http and a missing path, with fixes","Edits that only change www or a trailing slash, with a one-tap revert"],"limits":["Hostname rules only; it does not resolve DNS or send a request","Tunnel and inspector lists are starter sets"],"a11y":["Visible <label>; aria-invalid on errors and notes via aria-describedby","Notes in a polite live region with icon and text","After a fix, focus returns to the input"]}},{"name":"list-paste","type":"registry:ui","title":"List Paste","description":"Paste box that splits on lines, commas, semicolons and tabs (keeping \"Shah, Priya\" <…> whole), counts valid, duplicate and invalid items with the line and reason for each, and removes the bad ones in one undoable tap. Validator is pluggable.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/list-paste/list-paste.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Messy pasted list imported","mistakes":["paste list of emails","bulk invite invalid emails","split comma separated list","duplicate emails in list","invite team members paste"],"aliases":["bulk invite","paste emails","invite members","list import","csv paste","comma separated","team invite","recipients"],"flows":["forms","account-settings"],"fields":["email","text"],"summary":"Splits a pasted list of emails or names, counts valid, duplicate and invalid items, and removes the bad ones before you import or invite.","catches":["Items split wrongly by commas inside quoted names","Duplicates, by a compare key you choose","Invalid items with their line and reason","Invisible characters and Name <email> wrappers stripped"],"limits":["Default email check is pragmatic (one @, dotted domain, no spaces), not full RFC 5322 or a deliverability check","Shows only the first problems (maxProblems)"],"a11y":["Visible label; textarea is described by a polite summary region","Remove buttons have aria-labels with the count they remove","Undo is a real button; icons are aria-hidden"]}},{"name":"env-paste","type":"registry:ui","title":"Env Paste","description":"Catches .env slips loaders accept silently (stray or curly quotes, padded values, a ' #' that cuts a value short, duplicate keys, bad key names, empty values, test keys in production) by showing a parsed table of what the app will read, with one-tap fixes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/env-paste/env-paste.tsx","type":"registry:ui"},{"path":"registry/default/env-paste/parse-env.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Env value silently cut short","mistakes":["env file quotes wrong","dotenv hash cuts value","duplicate env keys","test key in production","env var empty"],"aliases":[".env","dotenv","environment variables","env vars","vercel env","secrets","config","api keys","deploy settings"],"flows":["devtools"],"fields":["secret","code"],"summary":"Shows a pasted .env file as the loader will read it, so stray quotes, a ' #' that cuts a value short or duplicate keys don't slip through.","server":"Validate required environment variables when the app starts, and keep production secrets out of shared pastes.","catches":["Stray or curly quotes and padded values","An unquoted ' #' that turns the rest of the value into a comment","Duplicate keys (which one wins) and invalid key names","Empty values, and test keys in production"],"limits":["Follows common dotenv parsing; your loader may treat quotes and comments differently","Masking is visual; the pasted text still holds the secrets"],"a11y":["Visible <label>; aria-invalid when there are errors","Summary is aria-live=\"polite\"","Parsed variables are a labelled list; each row can select its line in the field"]}},{"name":"json-field","type":"registry:ui","title":"JSON Field","description":"JSON text field that validates as you type, shows a caret at the exact line and column, and repairs trailing commas, single or smart quotes, unquoted keys, comments, Python literals (True/None), missing commas and unclosed braces with a token-aware fix. Includes Format.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/json-field/json-check.ts","type":"registry:ui"},{"path":"registry/default/json-field/json-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Broken JSON saved to config","mistakes":["json syntax error","trailing comma json","invalid json paste","single quotes json","json validator input"],"aliases":["json","json editor","json validator","config","payload","metadata","settings","api body","jsonc"],"flows":["devtools","forms"],"fields":["code"],"summary":"Validates JSON as you type, points at the exact line and column, and repairs the usual hand-editing mistakes in one tap.","catches":["Trailing commas","Single or smart quotes, and unquoted keys","Comments and Python literals such as True and None","Missing commas and unclosed braces or brackets","Exact line and column, the same in every browser"],"limits":["Checks syntax only; no JSON Schema validation","Fixes are heuristic; review the result before saving"],"a11y":["Visible label; textarea has aria-invalid and is described by the status","Status is a polite live region and problems wait for a pause while typing","Position button moves the caret to the problem","Caret overlay is aria-hidden"]}},{"name":"regex-field","type":"registry:ui","title":"Regex Field","description":"Regex input with static checks (unescaped dot, missing ^…$, greedy .*, nested quantifiers like (a+)+), plain-English hints, one-tap fixes, and live test strings matched in a time-capped Web Worker so a runaway pattern can't freeze the page.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/regex-field/regex-analyze.ts","type":"registry:ui"},{"path":"registry/default/regex-field/regex-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Regex matches more than intended","mistakes":["regex unescaped dot","regex missing anchors","catastrophic backtracking","regex tester","greedy regex"],"aliases":["regex","regular expression","pattern","validation rule","regex tester","redos","match","filter rule"],"flows":["devtools"],"fields":["code"],"summary":"Flags regexes that look right but match too much or hang: unescaped dots, missing anchors, greedy wildcards and catastrophic backtracking.","server":"Apply your own timeout or a linear-time engine (e.g. RE2) wherever user-supplied patterns run on the server.","catches":["Unescaped . where a literal dot was likely meant","Missing ^…$ anchors that let partial matches pass","Greedy .* that swallows more than intended","Nested quantifiers prone to catastrophic backtracking","Test strings that pass or fail against your expectation, or time out"],"limits":["Checks JavaScript regex syntax; other engines (PCRE, RE2, Python) differ","Static checks are heuristics; a flagged pattern can be intentional and a clean one can still be wrong","The Worker time cap protects this page, not wherever you run the pattern later"],"a11y":["Visible <label> on the pattern input with aria-invalid and hints via aria-describedby","Hints render in a polite live region; syntax errors use role=alert","Each test string, expectation toggle and remove button has an aria-label","Pass/fail/timeout shown with distinct icons plus the toggle's accessible name; the highlight backdrop is aria-hidden"]}},{"name":"cidr-input","type":"registry:ui","title":"CIDR Input","description":"Reads an IPv4 or IPv6 CIDR back as its exact range and address count, catches host bits set (offering both the network and single-host readings), and warns on 0.0.0.0/0, a missing prefix, leading zeros and overlaps with existing ranges.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/cidr-input/cidr-input.tsx","type":"registry:ui"},{"path":"registry/default/cidr-input/cidr.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Firewall opened to 0.0.0.0/0","mistakes":["cidr host bits set","subnet range calculator","0.0.0.0/0 open","overlapping subnets","ip range wrong"],"aliases":["cidr","subnet","ip range","security group","allowlist","firewall","vpc","ipv4","ipv6"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Reads a CIDR block back as its exact range and size, and warns before a firewall rule opens to the whole internet or overlaps an existing range.","server":"Validate rules in your cloud or firewall API and review changes to public ranges there.","catches":["Host bits set in the address, with the network or /32 (/128) reading as fixes","0.0.0.0/0 or ::/0 (open to everyone)","Overlap with, or nesting inside, the existing ranges you pass, named","A missing prefix length and ambiguous leading zeros"],"limits":["Only compares against the ranges you pass in `existing`","It doesn't know whether a wide range is intended for a public service"],"a11y":["Visible <label>; aria-invalid and aria-describedby to an aria-live=\"polite\" hint","Serious warnings use role=\"alert\"","Fix buttons have aria-labels (\"Use …\") and return focus to the input","The bit diagram is aria-hidden; the range is stated in text"]}},{"name":"version-range-input","type":"registry:ui","title":"Version Range Input","description":"npm-style semver range input that reads ranges back in plain English, marks which published versions match and which installs, and warns about ^0.x lock-in, a hyphen without spaces, commas used for AND and ranges that match nothing.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/version-range-input/semver.ts","type":"registry:ui"},{"path":"registry/default/version-range-input/version-range-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Semver range installs the wrong version","mistakes":["caret range 0.x","semver range meaning","npm version range","tilde vs caret","range matches nothing"],"aliases":["semver","version range","package.json","caret","tilde","npm","dependencies","peer deps","renovate"],"flows":["devtools"],"fields":["code"],"summary":"Reads npm semver ranges back in plain English and shows which published versions match and which one installs.","catches":["Ranges that match no published version","^0.x and ^0.0.x, which lock to a minor or patch","A hyphen range without spaces","Commas used where npm expects spaces (AND)","Latest version falling outside the range","Which version npm would install"],"limits":["Implements node-semver default options; includePrerelease and loose modes are not modelled","Needs the published versions list you pass"],"a11y":["Visible <label>; aria-invalid with hint via aria-describedby","Hint in a polite live region; parse errors use role=alert","Versions list has an aria-label; each row states matches/does not match in text","Fix buttons have aria-labels"]}},{"name":"glob-input","type":"registry:ui","title":"Glob Input","description":"Glob field that matches the pattern live against the files you pass, flags no-match and match-everything patterns, the * vs ** mix-up that skips subfolders, ** glued to a name, ./ and backslashes, with one-tap fixes. Dependency-free matcher with {a,b} expansion.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/glob-input/glob-input.tsx","type":"registry:ui"},{"path":"registry/default/glob-input/glob.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Config glob silently misses files","mistakes":["glob pattern not matching","star vs double star","gitignore pattern","glob subfolders","tsconfig include glob"],"aliases":["glob","file pattern","gitignore","include exclude","tsconfig","eslint","ci paths","wildcard","minimatch"],"flows":["devtools"],"fields":["code"],"summary":"Runs a config glob live against your real file list, so a pattern that silently misses files, or matches everything, shows up while you type.","catches":["Pattern that matches no files","Pattern that matches every file","* used where ** was needed, skipping subfolders","** glued to a name (src/**.ts)","Leading ./ and Windows backslashes"],"limits":["Its own matcher; tools such as webpack, tsconfig, git or picomatch differ on edge cases (negation, dotfiles, extglobs)","Only as accurate as the file list you pass in","Case-insensitive by default; set caseSensitive for Linux/git behaviour"],"a11y":["Visible label tied to the input; input is described by the status","Status is a polite live region","Match list is a labelled list with icon labels (\"matches\"/\"no match\"), not colour alone","List animation is skipped under reduced motion"]}},{"name":"i18n-string-field","type":"registry:ui","title":"i18n String Field","description":"Translation field that compares the text with its source: dropped or renamed placeholders ({count}, %s, {{name}}, <b>…</b>), unclosed tags, ICU plural forms the target language needs per CLDR, rendered length against a UI budget, and untranslated pastes. One-tap fixes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/i18n-string-field/i18n-checks.ts","type":"registry:ui"},{"path":"registry/default/i18n-string-field/i18n-string-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Translation drops a placeholder","mistakes":["translation missing placeholder","icu plural wrong","untranslated string","i18n text too long","broken html tag translation"],"aliases":["i18n","l10n","translation","localization","placeholders","icu plural","locale","hindi","next-intl"],"flows":["devtools","forms"],"fields":["text"],"summary":"Checks each translation against its source as it is typed, so dropped placeholders, broken tags or missing plural forms don't ship.","catches":["Placeholders dropped or renamed ({count}, %s, %1$s, {{name}})","Markup tags missing or left unclosed","ICU plural branches the target language needs (via Intl.PluralRules)","Translation longer than the UI budget","Source text pasted back untranslated"],"limits":["Plural rules come from the browser's Intl.PluralRules data, which can lag CLDR","Rendered length is a character estimate, not measured pixel width","Doesn't judge translation quality or meaning"],"a11y":["Visible label tied to the field, described by the issues list","Issues list is a polite live region","Placeholder chips have aria-labels saying present or missing","Keep-same button has an aria-label and title"]}},{"name":"sql-guard","type":"registry:ui","title":"SQL Guard","description":"SQL box with a small tokenizer (comments and strings ignored) that flags UPDATE/DELETE without WHERE, a commented-out WHERE, always-true conditions, DROP and TRUNCATE, asks before writes on production, shows estimated rows and requires typing the table name.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/sql-guard/sql-guard.tsx","type":"registry:ui"},{"path":"registry/default/sql-guard/sql-lint.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a DELETE without WHERE before it runs","mistakes":["delete without where","update all rows mistake","drop table production","sql query safety","truncate by accident"],"aliases":["sql","query editor","delete","update","where clause","drop table","database console","admin","production db"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Warns before running SQL that touches every row or drops data, like UPDATE/DELETE without WHERE, and asks for the table name first.","server":"Run with least-privilege credentials, prefer transactions and read-only connections, and enforce production write approval on the server.","catches":["UPDATE or DELETE with no top-level WHERE","A WHERE that is only inside a -- or /* */ comment, with a one-tap uncomment","Simple always-true conditions after WHERE or OR (1=1, TRUE, id = id, 'a'='a')","DROP TABLE/DATABASE/SCHEMA and TRUNCATE","Any write when env is in productionEnvs","Estimated affected rows from your estimateRows"],"limits":["Pattern checks only: it does not parse full SQL or prevent SQL injection, and a query with a WHERE can still be destructive","Tokenizer understands --, # (MySQL) and /* */ comments, '…' and $$…$$ strings, and \"…\", `…` and […] identifiers; \"#>\" and \"#-\" are read as Postgres operators, but a bare Postgres \"#\" (bitwise XOR) is read as a comment","Always-true checks look at a single term right after WHERE or OR (\"WHERE 1=1\", \"OR 1=1\", \"OR id = id\"); a term followed by AND (\"WHERE 1=1 AND …\", the query-builder pattern) is not flagged, and tautologies inside parentheses or longer expressions are missed","Not dialect-aware beyond that; CTEs are judged by their writing verb"],"a11y":["Visible <label>; Cmd/Ctrl+Enter runs; hints in a polite live region via aria-describedby","Confirmation is a labelled group; focus moves into it, Escape closes","Type-the-table input has a <label>; Enter runs only when it matches","Environment shown as text with a colour dot"]}},{"name":"markdown-check","type":"registry:ui","title":"Markdown Check","description":"Markdown field that catches unclosed ** or backticks, [text] (url) with a space, empty or space-broken links, #Title with no space and raw HTML the sanitizer strips, previews each spot as it will render, and offers a one-tap fix.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/markdown-check/markdown-check.tsx","type":"registry:ui"},{"path":"registry/default/markdown-check/markdown-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Markdown renders differently than written","mistakes":["markdown link broken","unclosed bold markdown","markdown heading no space","markdown preview wrong","html stripped markdown"],"aliases":["markdown","md editor","readme","comment box","rich text","preview","github markdown","docs","mdx"],"flows":["messaging","devtools"],"fields":["text","code"],"summary":"Catches Markdown that will render differently from how it reads, like an unclosed ** or a link broken by a space, and previews each spot.","catches":["Unclosed emphasis or code backticks","[text] (url) with a space, and empty or space-broken links","#Title with no space after #","Raw HTML tags your sanitizer strips (allowedTags)"],"limits":["Follows CommonMark/GitHub-style rules; other renderers differ","Not a full Markdown parser, so nested edge cases can be missed","Default allowed tags mirror GitHub-style sanitizers; pass yours"],"a11y":["Visible label; textarea is described by a polite status region","Problems wait for a pause while typing and show at once on blur","Each issue has a select button and a fix button"]}},{"name":"yaml-field","type":"registry:ui","title":"YAML Field","description":"YAML editor that flags values that load without error but change meaning (NO/yes/on/off as booleans, 1.10 as 1.1, 0755 as octal, 22:22 as base-60), duplicate keys, tabs and misaligned indentation, with line, column, the parsed value and one-tap fixes.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/yaml-field/yaml-field.tsx","type":"registry:ui"},{"path":"registry/default/yaml-field/yaml-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"YAML NO read as false","mistakes":["yaml norway problem","yaml boolean yes no","yaml indentation error","yaml version number float","yaml tabs"],"aliases":["yaml","yml","config","docker compose","github actions","kubernetes","helm","ci config","front matter"],"flows":["devtools"],"fields":["code"],"summary":"Catches YAML that parses but means something else, like NO read as false or 1.10 read as 1.1, and shows what the parser will read.","catches":["Plain scalars YAML 1.1 loaders turn into booleans (NO, yes, on, off)","Version-like numbers that lose digits (1.10 → 1.1)","Leading-zero octals and base-60 times","Duplicate keys, with the first occurrence","Tabs and misaligned indentation"],"limits":["Targets YAML 1.1 behaviour (PyYAML, Psych, SnakeYAML, go-yaml v2); YAML 1.2 loaders differ","Not a full YAML parser; complex documents can produce misses"],"a11y":["Visible <label>; status via aria-describedby in a polite live region","Each issue has a button labelled with its line and column","After a fix, focus returns to the textarea"]}},{"name":"webhook-test","type":"registry:ui","title":"Webhook Test","description":"Webhook URL field that flags localhost and private IPs, plain http, a missing path and tunnel URLs, then judges a test delivery like the sender: redirects fail (one-tap switch to the final URL), slow replies near the timeout and 200 login pages are caught.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/webhook-test/webhook-rules.ts","type":"registry:ui"},{"path":"registry/default/webhook-test/webhook-test.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Webhook saved to an unreachable URL","mistakes":["test webhook delivery","webhook redirect fails","webhook timeout","webhook returns login page","localhost webhook"],"aliases":["webhook","test webhook","send test event","endpoint","integrations","stripe","dodo payments","razorpay","callback url"],"flows":["devtools","account-settings"],"fields":["url"],"summary":"Checks a webhook URL the way the sender will before it's saved: reachability, https, redirects, slow replies and login pages.","server":"Validate webhook URLs server-side (resolve DNS, block private ranges) to prevent SSRF, and sign deliveries.","catches":["localhost, private and link-local IPs the sender can't reach","Plain http","Missing path and temporary tunnel URLs","Redirects, with a switch to the final URL","Replies close to the sender's timeout (default 10 s)","200 responses that are really login pages"],"limits":["Test delivery runs through your onSendTest; results depend on where it's sent from","Hostname checks don't resolve DNS, so names pointing at private IPs aren't caught"],"a11y":["Visible <label>; status via aria-describedby in a polite live region","Danger notes use role=alert","Fix and send buttons have text labels"],"rules":{"verified":"2026-10-17","sources":[{"label":"GitHub Docs – Best practices for using webhooks","url":"https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks"}],"note":"Confirmed GitHub asks receivers to respond within 10 seconds. Other senders use different timeouts; set timeout."}}},{"name":"utm-check","type":"registry:ui","title":"UTM Check","description":"UTM link checker that catches mixed case and spaces, source and medium swapped, a medium GA4's default channel rules file as organic or Unassigned, misspelled keys, a second \"?\", tags after \"#\" and tags on internal links, and reads back the GA4 channel.","categories":["Parsers and analyzers"],"files":[{"path":"registry/default/utm-check/utm-check.tsx","type":"registry:ui"},{"path":"registry/default/utm-check/utm.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"UTM links split analytics","mistakes":["utm case mismatch","utm source medium swapped","ga4 unassigned traffic","utm builder","utm after hash"],"aliases":["utm","utm builder","campaign url","ga4","google analytics","utm_source","marketing link","tracking","diwali sale"],"flows":["devtools","forms"],"fields":["url"],"summary":"Catches campaign links that split analytics: inconsistent casing, swapped source and medium, misspelled keys and tags that land as Unassigned.","catches":["Case and spacing that split reports (Facebook vs facebook)","Values your reports already spell differently","Source and medium swapped","Mediums that GA4 default channel rules send to organic or Unassigned","Misspelled UTM keys, a second ?, or tags after #","UTM tags on links to your own site"],"limits":["Channel prediction follows GA4's default channel group; custom channel groups differ","Source lists are short built-ins; pass your own for wider coverage"],"a11y":["URL and builder fields have <label>s; issues linked via aria-describedby","Issues and the predicted channel in a polite live region","Fix and fix-all are labelled buttons with Undo"],"rules":{"verified":"2026-10-17","sources":[{"label":"Google Analytics Help – [GA4] Default channel group","url":"https://support.google.com/analytics/answer/9756891"}],"note":"Confirmed the page defines GA4 default channels (e.g. Paid Social) and that Unassigned is used when no channel rule matches. The individual rule conditions in utm.ts were not re-checked one by one."}}},{"name":"reorder-readback","type":"registry:ui","title":"Reorder Readback","description":"Sortable list that announces each move with its meaning (\"Moved Razorpay from 3rd to 1st. It's now tried first.\") and Undo. Drags only from the grip, refuses to displace pinned rows, marks rows moved since save, and offers click, arrow and keyboard moves.","categories":["Forms and saving"],"files":[{"path":"registry/default/reorder-readback/reorder-readback.tsx","type":"registry:ui"},{"path":"registry/default/reorder-readback/reorder.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Slipped drag reordered payment fallbacks","mistakes":["drag reordered list by mistake","priority order changed accidentally","touch scroll reordered items","sortable list undo"],"aliases":["drag to reorder","sortable","dnd-kit","priority list","reorder","payment gateway order","razorpay","drag handle"],"flows":["forms","devtools"],"fields":[],"summary":"Reads back every reorder with its consequence and an Undo, so a slipped drag doesn't silently change priorities, fallbacks or steps.","catches":["Every move, read back with what the new position means","Moves that would push a pinned row aside, refused with a reason","Rows moved since the last saved baseline","Touch scrolling that would start a drag (drag only from the grip)"],"limits":["Consequence text comes from your describePosition; without it only positions are read back","Does not save; persist the order yourself (normalizeOrder in reorder.ts can run on the server)"],"a11y":["Space picks up, arrow keys move, Space drops and Escape cancels; instructions linked via aria-describedby","Move-up/down buttons on every row give a non-drag alternative (WCAG 2.2 SC 2.5.7)","One role=status region with debounced announcements","Grip has an aria-label and aria-pressed; pinned rows use aria-disabled with a lock icon","Animations respect reduced motion; focus returns to the grip after moves and undo"]}},{"name":"nominee-check","type":"registry:ui","title":"Nominee Check","description":"Nomination form check for bank deposits, lockers and demat/MF holdings: shares not adding to 100%, nominees past the limit (4 bank, 3 demat), splits on a locker, missing relationship or minor's date of birth, minors with no adult, duplicates and no nominee, with a plain readback.","categories":["Forms and saving"],"files":[{"path":"registry/default/nominee-check/nomination.ts","type":"registry:ui"},{"path":"registry/default/nominee-check/nominee-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch nominee shares that don't add to 100%","mistakes":["nominee percentage wrong","minor nominee no guardian","bank nomination invalid","demat nominee rules"],"aliases":["nominee","nomination","demat","sebi","bank account","locker","kyc","guardian","zerodha","rbi"],"flows":["india","forms"],"fields":["name","number"],"summary":"Checks a bank, locker or demat nomination against India's 2025 bank rules and SEBI's 2026 limits, and reads back who gets what.","catches":["Simultaneous shares that don't add to 100%","Nominees beyond the limit: 4 for bank deposits and lockers, 3 for demat and MF from 1 Sep 2026","Percentage split on a locker, which allows successive nomination only","Demat nominee without a relationship, or a minor without a date of birth","Minor nominee with no adult to receive, the same person twice, and no nominee"],"limits":["Indian rules only, baked in as of the sources below; they can change","Doesn't submit or verify the nomination with your bank or depository","Not legal advice"],"a11y":["The nominee list is a labelled group described by the issues list","Each row's inputs have labels","Focus moves to the relevant row or fix after a change"],"rules":{"verified":"2026-10-17","sources":[{"label":"Banking Companies (Nomination) Rules, 2025 (in force 1 Nov 2025)","url":"https://thc.nic.in/Central%20Governmental%20Rules/Banking%20Companies%20(Nomination)%20Rules,%202025.pdf"},{"label":"SEBI circular SEBI/HO/OIAE/OIAE_IAD-3/P/CIR/2026/12676 (29 May 2026, effective 1 Sep 2026)","url":"https://www.sebi.gov.in/sebi_data/attachdocs/jun-2026/1780397706130.pdf"}],"note":"Informational; check the official source. Confirmed: deposits up to four nominees successive or simultaneous, lockers up to four successively, from 1 Nov 2025; demat/MF up to 3 nominees from 1 Sep 2026. Minor-nominee and odd-lot details were not re-checked line by line."}}},{"name":"passport-validity-check","type":"registry:ui","title":"Passport Validity Check","description":"Passport check against the destination's rule (6 months, Schengen's 3 months after leaving and 10 years since issue, blank pages) that names the exact date it must last until and offers the latest return that would pass. Default rules carry sources and can be replaced.","categories":["Forms and saving"],"files":[{"path":"registry/default/passport-validity-check/passport-rules.ts","type":"registry:ui"},{"path":"registry/default/passport-validity-check/passport-validity-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Passport expires before trip allows","mistakes":["passport 6 month rule","schengen passport validity","denied boarding passport expiry","passport expiring soon travel"],"aliases":["passport","passport expiry","visa","schengen","flight booking","travel docs","6 month rule","traveller details"],"flows":["checkout","forms"],"fields":["id-number","datetime"],"summary":"Checks a passport against the destination's validity rule and names the date it must last until, so a trip isn't booked that the passport can't make.","catches":["Validity shorter than the destination needs, from arrival or departure","Schengen: issued more than 10 years before arrival","Not enough blank or facing pages","Latest return date that would still pass"],"limits":["Built-in rules cover a few destinations and can change; airlines may apply stricter rules","Doesn't check visas or transit rules","Informational; check the official source"],"a11y":["Section labelled by its heading","Expiry, issue date and pages inputs have visible labels","Icons are aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"ICA Singapore: Entering Singapore (at least 6 months' validity)","url":"https://www.ica.gov.sg/enter-transit-depart/entering-singapore"},{"label":"Schengen Borders Code, Regulation (EU) 2016/399","url":"https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32016R0399"}],"note":"Informational; check the official source. Only the Singapore rule was confirmed this round. EUR-Lex blocked automated fetches, so the Schengen 3-month/10-year rule and the South Africa rule (dha.gov.za) were not re-verified."}}},{"name":"seat-gap-check","type":"registry:ui","title":"Seat Gap Check","description":"Seat-map check for group bookings: a party split across the row, a single empty seat left behind (which many venues refuse) and a child seated away from every adult, with problem seats marked and a one-tap move to the nearest block that fits.","categories":["Forms and saving"],"files":[{"path":"registry/default/seat-gap-check/seat-gap-check.tsx","type":"registry:ui"},{"path":"registry/default/seat-gap-check/seats.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Group split across row at booking","mistakes":["family seats split","single seat gap left","child seated alone","movie seats not together"],"aliases":["seat selection","seat map","movie tickets","bookmyshow","cinema seats","flight seats","seats together","bus seats"],"flows":["checkout","india"],"fields":[],"summary":"Catches group seat picks that split the party, leave a single empty seat or seat a child away from every adult.","catches":["Party split across a row or aisle","A single orphan seat left between taken seats (orphanRule)","A child with no adult beside them","One-tap move to the nearest contiguous block, children beside adults"],"limits":["Venue rules differ; set orphanRule to match yours","Works on the seat map you pass; prices and accessibility seats are not considered"],"a11y":["Seat map is a role=grid with rowheaders; one tab stop with arrow-key, Home and End navigation","Each seat button has an aria-label with its status and aria-pressed when chosen","Issue icons are aria-hidden with text alongside"]}},{"name":"formula-overwrite-guard","type":"registry:ui","title":"Formula Overwrite Guard","description":"Previews in place which cells a paste would change and how, leaves out a heading row the copy dragged along, and offers to paste values only into empty and non-formula cells; safe pastes go straight in with Undo.","categories":["Forms and saving"],"files":[{"path":"registry/default/formula-overwrite-guard/formula-overwrite-guard.tsx","type":"registry:ui"},{"path":"registry/default/formula-overwrite-guard/paste-plan.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Paste wiped spreadsheet formulas","mistakes":["paste overwrote formulas","paste values only","pasted over header row","spreadsheet paste spilled"],"aliases":["spreadsheet","data grid","paste","excel","google sheets","formulas","paste special","ag grid"],"flows":["forms"],"fields":["number"],"summary":"Previews which cells a spreadsheet paste would change, so formulas, locked cells and headings aren't wiped silently.","catches":["Pastes that overwrite formulas","Locked cells and the heading row","Data spilling past the table edge","A heading row copied along with the data"],"limits":["Works on the grid model you pass; it isn't connected to a spreadsheet engine","\"Safe\" paste skips cells rather than merging values"],"a11y":["The preview is a role=\"region\" with an aria-label summarising rows, columns and cell","Locked and formula cells use icons plus text","Actions are buttons with visible text"]}},{"name":"passkey-name-prompt","type":"registry:ui","title":"Passkey Name Prompt","description":"Post-creation passkey naming prompt that prefills a distinct name from the AAGUID (iCloud Keychain, 1Password) or the device (\"Mac · Chrome\", phone, security key), numbers it when taken, blocks empty and duplicate names, nudges away from generic ones, and says whether it syncs.","categories":["Forms and saving"],"files":[{"path":"registry/default/passkey-name-prompt/passkey-name-prompt.tsx","type":"registry:ui"},{"path":"registry/default/passkey-name-prompt/passkey-name.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Identical passkeys nobody can tell apart","mistakes":["all passkeys named passkey","cant tell which passkey to delete","name passkey","passkey list confusing"],"aliases":["passkey","webauthn","security keys","aaguid","icloud keychain","1password","security settings","biometrics"],"flows":["account-settings","signup-login"],"fields":["text"],"summary":"Prefills a distinct name right after a passkey is created, so the security page isn't a list of identical \"Passkey\" rows nobody can tell apart.","catches":["Identical generic names (\"Passkey\")","Empty or duplicate names, with a one-tap fix","Provider from the AAGUID, or device from the user agent","Whether the passkey syncs (manager) or stays on this device"],"limits":["Display only, never a trust decision; AAGUIDs can be zeroed or spoofed","Built-in AAGUID table is small; pass providers for others","User-agent device names are best-effort"],"a11y":["Visible label; input has aria-invalid when blocked and is described by a polite status","Blocking errors use role=alert","Icons are aria-hidden"]}},{"name":"irreversible-diff-badge","type":"registry:ui","title":"Irreversible Diff Badge","description":"Chip for permanent fields that turns amber once edited, plus a pre-save readback listing only the permanent fields that changed (from → to) with consequences and one-tap Revert, behind a confirmation that resets whenever that list changes.","categories":["Forms and saving"],"files":[{"path":"registry/default/irreversible-diff-badge/irreversible-changes.ts","type":"registry:ui"},{"path":"registry/default/irreversible-diff-badge/irreversible-diff-badge.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Permanent field changed without warning","mistakes":["changed username cant undo","permanent field edited","slug change broke links","cant change back"],"aliases":["username","slug","handle","permanent","readonly after save","profile settings","subdomain","diff"],"flows":["account-settings","forms"],"fields":["text"],"summary":"Marks fields that can't be changed back and, before save, lists only the permanent fields that actually changed, from → to.","server":"Enforce which fields are immutable on the server and audit-log changes to them.","catches":["Permanent or rarely changeable fields edited without notice","Readback of only the hard-to-undo fields that changed, with consequences","Confirmation given for one set of changes reused for another: it resets","One-tap Revert per field"],"limits":["You decide which fields are permanent; it can't know","Comparison uses your equals or a strict/JSON comparison"],"a11y":["Readback is a labelled group described by the change list","Changes are announced in a polite live region; from/to are read as text, with del/ins visuals aria-hidden","Revert buttons have aria-labels naming the field and old value; focus moves after a revert","Save errors use role=alert"]}},{"name":"stale-attachment-check","type":"registry:ui","title":"Stale Attachment Check","description":"Attachment check against nearby files: a newer save of the same document, names saying draft, old, copy or (1), Office lock files and files dated before the subject's date, with a one-tap swap to the newer file and Undo.","categories":["Forms and saving"],"files":[{"path":"registry/default/stale-attachment-check/stale-attachment-check.tsx","type":"registry:ui"},{"path":"registry/default/stale-attachment-check/stale.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Old draft attached instead of final","mistakes":["attached old version","sent draft not final","wrong file attached","report_v2 instead of v3"],"aliases":["attachment","file version","final_final","email attach","upload","compose","proposal","invoice pdf"],"flows":["messaging"],"fields":["file"],"summary":"Flags an attachment that looks like an older copy, e.g. report_v2.pdf attached while report_v3_final.pdf was edited today.","catches":["A newer file of the same document nearby","Names containing draft, old, copy or (1)","Office lock files (~$…)","Files last modified before a date named in the subject","One-tap swap to the newer file, with Undo"],"limits":["Only sees the nearby files you pass; it can't browse folders","Name matching ignores filler words, so different documents with similar names can be confused","Modified dates can be wrong after copying or syncing"],"a11y":["Summary in a polite live region; status shown with icon and text","Focus moves to the next fix, Undo or Send as issues are resolved","Fix and Undo are labelled buttons"]}},{"name":"dependency-license-check","type":"registry:ui","title":"Dependency License Check","description":"Reads each SPDX expression against your policy (GPL or AGPL in closed source, no or unknown license, source-available terms in a hosted product, deny lists) and catches relicenses between versions with a one-tap \"Stay on\" the last version under the old license.","categories":["Forms and saving"],"files":[{"path":"registry/default/dependency-license-check/dependency-license-check.tsx","type":"registry:ui"},{"path":"registry/default/dependency-license-check/license.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"GPL dependency shipped in closed source","mistakes":["gpl package in commercial app","license changed in new version","npm license check","agpl dependency"],"aliases":["license","spdx","npm install","dependencies","open source compliance","gpl","mit","package.json","sbom"],"flows":["devtools"],"fields":["code"],"summary":"Reads each new dependency's SPDX license against your policy, so a GPL, unlicensed or relicensed package doesn't slip into your project.","server":"Enforce license policy in CI (lockfile scan) so dependencies added outside this UI are caught too.","catches":["Copyleft (GPL, AGPL) licenses in closed-source projects","Missing or unrecognised licenses","Source-available terms (e.g. BSL, SSPL) in a hosted product","Packages on your deny list","A license change between versions, with an option to stay on the older version"],"limits":["Uses the license metadata you pass; packages sometimes declare a license that differs from their files","Doesn't scan transitive dependencies unless you pass them","Not legal advice: your policy decides what is allowed"],"a11y":["License categories are written in text, not just chip colour","Focus moves to the first fix, or the add button when clear","Package names show the version in a title"]}},{"name":"heading-outline","type":"registry:ui","title":"Heading Outline","description":"Live heading outline for a Markdown draft or DOM region. Flags skipped levels (shown as dashed ghost rows), extra or missing H1s, empty headings, bold text posing as a heading and overlong headings, with a one-tap level fix and rows that jump to the heading.","categories":["Forms and saving"],"files":[{"path":"registry/default/heading-outline/heading-outline.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Skipped heading levels confuse screen readers","mistakes":["skipped h2 to h4","multiple h1 on page","bold text as heading","heading structure accessibility"],"aliases":["headings","h1","a11y","accessibility","wcag","markdown editor","document outline","screen reader","cms"],"flows":["forms","devtools"],"fields":["text"],"summary":"Shows the live heading outline of a Markdown draft or page region and flags skipped levels and fake headings that confuse screen reader users.","catches":["Skipped heading levels (H2 to H4)","More than one H1, or none","Empty headings","Bold lines that look like headings but aren't","Headings long enough to be paragraphs"],"limits":["DOM mode reads headings and role=heading; it skips hidden content but cannot judge whether heading text is meaningful","Markdown mode parses ATX, setext and single-line HTML headings, not every Markdown dialect","One outline check, not a full WCAG audit"],"a11y":["Rendered as a labelled nav","Summary line is a polite live region","Jump focuses the heading (adding tabindex=-1 when needed); smooth scroll is skipped under reduced motion","Focus returns to a nearby row after a fix"]}},{"name":"link-text-check","type":"registry:ui","title":"Link Text Check","description":"Link text check for Markdown or a live DOM region that flags \"click here\" and \"read more\", URLs as link text, empty links and the same text pointing to different places, with a rewrite that lifts the sentence's own words into the link and a view of the links list as read out.","categories":["Forms and saving"],"files":[{"path":"registry/default/link-text-check/link-rules.ts","type":"registry:ui"},{"path":"registry/default/link-text-check/link-text-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Click here links fail screen readers","mistakes":["click here link text","read more links accessibility","url as link text","empty link"],"aliases":["link text","a11y","accessibility","wcag","anchor text","screen reader","cms","rich text editor"],"flows":["forms","devtools"],"fields":["url","text"],"summary":"Flags \"click here\", bare URLs and empty links that make no sense to screen reader users browsing a list of links, and suggests better text.","catches":["Generic text such as \"click here\" and \"read more\"","Raw URLs used as link text","Empty links, including linked images with no alt","The same text pointing to different destinations","Rewrite suggestion from the surrounding sentence"],"limits":["English generic phrases by default; pass genericText to translate","Suggested rewrites need a human check","Doesn't check link targets or contrast"],"a11y":["Section is labelled; summary is a polite live region","Rows focus the link in live mode; smooth scroll is skipped under reduced motion","Focus returns to a nearby fix after applying one"]}},{"name":"reading-level","type":"registry:ui","title":"Reading Level","description":"Live Flesch-Kincaid grade against a target. Tints the hardest sentences in place (only when long and dense), with the reason and a jump to each, and offers one-tap plain-word swaps (\"utilize\" → \"use\").","categories":["Forms and saving"],"files":[{"path":"registry/default/reading-level/readability.ts","type":"registry:ui"},{"path":"registry/default/reading-level/reading-level.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Copy harder to read than its audience needs","mistakes":["text too complex","readability score","plain language check","flesch kincaid"],"aliases":["readability","plain language","flesch-kincaid","writing assistant","content editor","cms","copywriting","grammarly"],"flows":["forms"],"fields":["text"],"summary":"Flags copy that reads above its audience's grade level and points at the sentences and wordy phrases that cause it.","catches":["Grade above the target you set (default 8)","Sentences that are both long and dense, tinted in place","Wordy words and phrases with a plainer swap","Non-English text, where it shows a notice instead of a grade"],"limits":["Syllables are estimated from vowel groups (roughly ±10%), so grades are approximate","Flesch-Kincaid is an English-only formula and ignores meaning, layout and audience","The swap list is a starter set; it will miss domain jargon"],"a11y":["Visible <label> tied to the textarea, with the grade wired via aria-describedby","On/above-target status announced through a polite sr-only live region","Each swap button has an aria-label naming the word and its replacement","After a swap, focus moves to the next swap button (or back to the field)","The tinted backdrop is aria-hidden; sentence difficulty also shown as text"]}},{"name":"backup-codes","type":"registry:ui","title":"Backup Codes","description":"Continue unlocks only after the codes are downloaded, printed or copied (or all three with require=\"all\"); then one code must be typed back, with paste refused and look-alike characters forgiven, to show the saved copy is readable.","categories":["Forms and saving"],"files":[{"path":"registry/default/backup-codes/backup-codes.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Locked out with unsaved backup codes","mistakes":["user never saved recovery codes","lost 2fa backup codes","backup codes not downloaded","2fa lockout"],"aliases":["backup codes","recovery codes","2fa setup","mfa","totp","download codes","authenticator setup"],"flows":["signup-login","account-settings"],"fields":["code","secret"],"summary":"Makes users save their recovery codes and type one back before continuing, so they aren't locked out later by codes they never kept.","server":"Store only hashes of the codes, make each one single-use, and rate-limit recovery attempts on the server.","catches":["Moving on without saving the codes by download, print or copy","A saved copy that can't be read back, by asking for one code","Paste into the check field (refused unless allowPaste)","Leaving the page before finishing (beforeunload warning, on by default)"],"limits":["It knows a download, print or copy was started, not that the file was kept","Typing one code back proves one code was readable, not that all were saved"],"a11y":["Codes are an ordered list labelled by the title; save buttons use aria-pressed","The still-locked Continue uses aria-disabled with aria-describedby to what is left to do","The check field has a <label>, aria-invalid on a wrong length and an aria-live hint","Progress messages are aria-live=\"polite\""]}},{"name":"recovery-check","type":"registry:ui","title":"Recovery Check","description":"Account-recovery checkup. Flags recovery email or phone not confirmed in months, no backup method, a password-only account, one phone acting as both second factor and recovery, and low backup codes, each with a fix callback.","categories":["Forms and saving"],"files":[{"path":"registry/default/recovery-check/recovery-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Lost phone meant lost account","mistakes":["lost phone locked out","no backup 2fa method","recovery email outdated","account recovery weak"],"aliases":["account recovery","security checkup","recovery email","recovery phone","2fa","mobile number","security settings","+91"],"flows":["account-settings","signup-login"],"fields":["email","phone"],"summary":"Shows what would lock a user out if they lost their phone: stale recovery contacts, no backup method, or one phone holding both keys.","server":"checkRecovery is pure and can run on the server; the fixes themselves (adding factors, confirming contacts) must be verified server-side.","catches":["Recovery email or phone not confirmed within staleAfterDays (default 180)","No backup second factor","Password-only account","The same phone used for SMS codes and recovery","Backup codes running low or all used"],"limits":["Only as accurate as the RecoveryState you pass in","Does not verify that an email or phone still works; it reads the last-confirmed dates","Fixes run your onFix callback; nothing changes by itself"],"a11y":["Section labelled by its title; open-issue summary in a polite live region","A failed fix shows role=alert text","After a fix, focus moves to the issue's \"Fixed\" marker","Severity is shown with an icon and text, not colour alone; icons are aria-hidden"]}},{"name":"unsaved-changes-bar","type":"registry:ui","title":"Unsaved Changes Bar","description":"Unsaved-changes bar that names every dirty field (with per-field revert), guards the tab close, saves on Ctrl/⌘+S and asks twice before Discard; editing a field back to its saved value makes it clean again.","categories":["Forms and saving"],"files":[{"path":"registry/default/unsaved-changes-bar/unsaved-changes-bar.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Left page with unsaved edits","mistakes":["lost changes on navigate","unsaved changes warning","beforeunload prompt","forgot to save settings"],"aliases":["unsaved changes","dirty form","beforeunload","save bar","ctrl+s","settings form","react hook form","discard changes"],"flows":["forms","account-settings"],"fields":[],"summary":"Names every unsaved field, warns before the tab closes, saves on Ctrl/⌘+S and asks twice before discarding.","catches":["Each changed field, by name","Fields edited back to their saved value (clean again, deep compare)","Closing the tab with unsaved edits (beforeunload)","Discard, which needs a second confirm","Save failures, shown as an error"],"limits":["In-app navigation must be guarded by your router; only tab close is covered","Browsers show their own generic leave-page text"],"a11y":["Region with an aria-label; changed fields list has an aria-label","Ctrl/⌘+S saves (aria-keyshortcuts); revert buttons have aria-labels","Discard confirm is a labelled group focused on Keep; Escape cancels","Errors use role=alert; status in a polite live region"]}},{"name":"save-status","type":"registry:ui","title":"Save Status","description":"Autosave status with an aging \"Saved · 2 min ago\", an honest offline state and Retry on error, plus a debounced useAutosave hook that runs one save at a time and always saves the newest value last.","categories":["Forms and saving"],"files":[{"path":"registry/default/save-status/save-status.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Autosave failed silently while offline","mistakes":["autosave not working","lost work offline","saved status wrong","save out of order"],"aliases":["autosave","saved indicator","offline","saving...","google docs","debounce","sync status","retry"],"flows":["forms"],"fields":["text"],"summary":"Makes autosave honest: an aging \"Saved · 2 min ago\", a clear offline state and Retry on error, so failures aren't silent.","server":"Reject stale writes with a version or ETag check so an older save can't overwrite a newer one.","catches":["Saves that failed, shown with Retry","Being offline, instead of a misleading \"Saved\"","How long ago the last save landed","Out-of-order saves (one at a time, newest value goes next)","Leaving with unsynced changes (optional beforeunload warning)"],"limits":["The offline draft is kept in localStorage on this device only, if you enable it","Concurrent edits from other tabs or users are not merged"],"a11y":["Status text in a polite live region; errors announced with role=alert","Exact save time available in a title tooltip","Retry is a real button; icons are aria-hidden"]}},{"name":"error-summary","type":"registry:ui","title":"Error Summary","description":"A summary that takes focus on submit, links each error to its field, and ticks items off live as they are fixed, plus a useFormErrors() hook.","categories":["Forms and saving"],"files":[{"path":"registry/default/error-summary/error-summary.tsx","type":"registry:ui"}],"dependencies":["@number-flow/react","lucide-react","motion"],"meta":{"stops":"Users miss errors on long forms","mistakes":["form errors not visible","cant find which field invalid","validation errors scroll","error summary accessibility"],"aliases":["form validation","error summary","gov.uk","react hook form","zod","a11y","inline errors","useFormErrors"],"flows":["forms","signup-login"],"fields":[],"summary":"A GOV.UK-style error summary that takes focus on submit, links each error to its field and ticks errors off as they are fixed.","catches":["Errors missed on long forms, listed in one place on submit","Finding the field, by linking each error to it","Progress, by ticking off fixed errors live"],"limits":["Lists the errors you pass; validation is up to you","Linking to a field needs the field's id to match the error item"],"a11y":["Takes focus on submit (tabIndex=-1) and is labelled by its title","Each link moves focus to its field","Fixed items say \"Fixed:\" in text; progress changes are aria-live=\"polite\"","Motion respects prefers-reduced-motion"]}},{"name":"form-progress","type":"registry:ui","title":"Form Progress","description":"Watches a form and counts the required fields still empty or invalid, with a Next missing button that moves focus to the next one.","categories":["Forms and saving"],"files":[{"path":"registry/default/form-progress/form-progress.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Form won't submit, field unknown","mistakes":["why wont form submit","required field missing","find empty required field","submit disabled no reason"],"aliases":["required fields","form progress","progress bar","validation","next missing","kyc form","application form","react hook form"],"flows":["forms"],"fields":[],"summary":"Counts required fields still empty or invalid and jumps to the next one, so people aren't left wondering why a form won't submit.","catches":["Required fields left empty","Fields failing native validation (pattern, minlength and similar)","Fields off-screen, by jumping to them"],"limits":["Relies on native required and constraint validation; custom validation isn't seen","Skips disabled, hidden and inert fields"],"a11y":["Field names come from their <label> (or aria-label/name)","Next missing moves focus to the field","Updates when required, disabled, hidden or inert attributes change"]}},{"name":"preflight-checklist","type":"registry:ui","title":"Preflight Checklist","description":"Checklist that locks an action until auto and manual checks pass, names the blockers, resets manual ticks and re-runs checks when the revision changes, and re-verifies right before the action runs.","categories":["Forms and saving"],"files":[{"path":"registry/default/preflight-checklist/preflight-checklist.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Shipped with checks skipped","mistakes":["deployed without checks","release checklist","publish before review","checks reset on new version"],"aliases":["checklist","pre-launch","release checklist","deploy","publish","gate","ci checks","go live"],"flows":["devtools","forms"],"fields":[],"summary":"Locks a deploy or publish button until automatic and manual checks pass, and re-verifies right before it runs.","server":"Gate the real deploy in CI or the API with the same required checks.","catches":["Action started with failing or pending checks","Manual ticks left over from an earlier revision","A check that started failing since it was ticked: re-verified on run","Auto-check that throws: counted as a failure"],"limits":["The lock is in this UI; the same action through an API or CLI isn't gated","Checks are only as good as the functions you pass"],"a11y":["Each check row is focusable and its state is read as text","Status is a polite live region linked to the action button","Locked action uses aria-disabled and stays focusable","Re-run button has a title and visually hidden text"]}},{"name":"attachment-nudge","type":"registry:ui","title":"Attachment Nudge","description":"Spots attachment phrases in English, Hindi and Hinglish (\"PFA\", \"attach kar diya\"), ignoring quoted replies, negations, future mentions and shared cloud links, and asks inline to Attach or Send anyway.","categories":["Forms and saving"],"files":[{"path":"registry/default/attachment-nudge/attachment-nudge.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"\"See attached\" sent without attachment","mistakes":["forgot attachment","pfa without file","attached but nothing attached","email sent without attachment"],"aliases":["attachment reminder","forgot attachment","pfa","please find attached","hinglish","compose email","gmail","chat"],"flows":["messaging","india"],"fields":["file","text"],"summary":"Notices \"see attached\" in a message with nothing attached and asks inline whether to attach a file or send anyway.","catches":["Attachment phrases (\"see attached\", \"PFA\", \"attach kar diya\") with no file attached","Phrases you add through extraPhrases","Skips quoted reply lines, negated or future mentions and messages with a Drive, Dropbox or OneDrive link"],"limits":["Phrase matching only; other wordings and other languages are missed","It cannot check that the attached file is the right one"],"a11y":["Textarea is labelled (aria-label) and Ctrl/Cmd+Enter sends, exposed via aria-keyshortcuts","The prompt is a role=\"group\" labelled by its text; focus moves to Attach when it appears and Escape returns to the textarea","Remove buttons on attachments have aria-labels naming the file","A polite live region announces changes"]}},{"name":"file-guard","type":"registry:ui","title":"File Guard","description":"Checks size, type, count, empty files and duplicates, and reads each file's first bytes to catch a renamed extension, with a one-tap rename fix.","categories":["Forms and saving"],"files":[{"path":"registry/default/file-guard/file-check.ts","type":"registry:ui"},{"path":"registry/default/file-guard/file-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong file type uploaded","mistakes":["file too large upload","renamed exe as pdf","empty file uploaded","duplicate file upload"],"aliases":["file upload","dropzone","mime type","file size limit","magic bytes","react-dropzone","pdf upload","aadhaar upload","document upload"],"flows":["forms","india"],"fields":["file"],"summary":"Checks files before upload, including their first bytes, so renamed, empty, duplicate or oversized files are caught before they start uploading.","server":"Re-validate size, type and content on the server and scan uploads; never trust the client's checks.","catches":["Files over maxSize, or more than maxFiles","Types outside accept","A file whose content doesn't match its extension (renamed)","Empty files and duplicates"],"limits":["Magic-byte detection covers common formats only","Runs in the browser and is easy to bypass; it is not a malware scan"],"a11y":["The pick button is described by the hint; the native input is hidden from the tab order","Selected files are a labelled list; remove buttons name the file","Status is aria-live=\"polite\""]}},{"name":"draft-restore","type":"registry:ui","title":"Draft Restore","description":"Autosaves to localStorage and, on reopen, offers the draft with a field-by-field preview that keeps any value the server changed since, unless you tick it. Drafts expire after 7 days by default.","categories":["Forms and saving"],"files":[{"path":"registry/default/draft-restore/draft-restore.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Lost form after accidental refresh","mistakes":["lost form data on refresh","restore draft","localstorage form save","old draft overwrote new data"],"aliases":["draft","autosave","localstorage","restore","form persistence","resume application","refresh lost data"],"flows":["forms"],"fields":[],"summary":"Autosaves a form locally and offers the draft back on reopen, keeping any value the server changed since, so work isn't lost or stale data restored.","catches":["Form work lost to a refresh, crash or closed tab","Stale draft values that would overwrite newer server values, kept server-side unless chosen","Old drafts, via an expiry (ttl)"],"limits":["Drafts live in this browser's localStorage: not on other devices, and cleared with site data","Don't store sensitive fields; use `exclude` for passwords, card numbers and similar","Anyone with access to the device can read localStorage"],"a11y":["A role=\"region\" labelled \"Unsaved draft\"","Message is aria-live=\"polite\"","Preview toggle uses aria-expanded and aria-controls"]}},{"name":"tab-collision","type":"registry:ui","title":"Tab Collision","description":"Single-editor lock across tabs: one tab edits (BroadcastChannel, with a localStorage storage-event fallback), the others become read-only until someone picks \"Use this tab\", with a confirm when the other tab has unsaved changes.","categories":["Forms and saving"],"files":[{"path":"registry/default/tab-collision/tab-collision.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Second tab save erased edits","mistakes":["edited in two tabs","last save overwrote changes","multiple tabs conflict","broadcastchannel"],"aliases":["multiple tabs","broadcastchannel","concurrent edit","conflict","read only","edit lock","cross-tab"],"flows":["forms"],"fields":[],"summary":"Lets only one browser tab edit a record at a time, so a save in one tab doesn't silently erase edits in another.","server":"Use optimistic concurrency (version or ETag) on save so concurrent edits from anywhere are rejected, not overwritten.","catches":["The same record open for editing in another tab of this browser","Taking over while the other tab has unsaved changes (confirm first)","Status changes reported through onStatusChange"],"limits":["Same browser profile only; other devices, browsers or users are not seen","Read-only mode is up to you to apply using the status","Does not merge edits"],"a11y":["Notice is a region with an aria-label; message in a polite live region","Confirm step closes with Escape","Buttons have text labels; icons are aria-hidden"]}},{"name":"review-step","type":"registry:ui","title":"Review Step","description":"Review page for wizards: every answer by step with named Edit links, empty optional fields and values changed since last time flagged, missing required answers blocking submit, and one deliberate submit that ignores carried-over clicks and disables while pending.","categories":["Forms and saving"],"files":[{"path":"registry/default/review-step/review-step.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong answer submitted in a wizard","mistakes":["submitted wrong details","no review before submit","multi step form review","double submit on final step"],"aliases":["review page","summary step","confirm details","wizard","stepper","multi-step form","checkout review","application review"],"flows":["forms","checkout"],"fields":[],"summary":"Lists every answer of a multi-step form by step with Edit links before submit, flagging empty, missing and changed answers.","server":"Validate every answer again and make the final submit idempotent on the server.","catches":["Missing required answers, blocking submit","Empty optional fields, shown as \"Not provided\"","Values changed since the previous submission","A click carried over from the previous step's button (guard window)","Repeat clicks while the submit is pending"],"limits":["Duplicate protection is client-side only","Display formatting is yours (pass format for money or masking)"],"a11y":["Each step is a section labelled by its title","Edit/Add buttons have aria-labels naming the step or field","Submit is described by the missing-answers note; errors use role=alert","Changed values have a sr-only \"Previously:\" prefix"]}},{"name":"dependent-reset","type":"registry:ui","title":"Dependent Reset","description":"A hook clears only what a parent change made invalid (pruning just the add-ons a new plan lacks), and a notice names each lost value, highlights those fields and offers Undo.","categories":["Forms and saving"],"files":[{"path":"registry/default/dependent-reset/dependent-reset.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Dependent fields cleared without notice","mistakes":["changing country cleared state","plan change removed add-ons","dependent dropdown reset","cascading select lost value"],"aliases":["dependent fields","cascading select","country state city","conditional fields","add-ons","plan picker","react hook form"],"flows":["forms","checkout"],"fields":["address"],"summary":"When a parent field changes, clears only the dependent values it made invalid and names each one with Undo, so fields don't vanish silently.","catches":["Dependent values that became invalid after a parent change","Partial loss (some options pruned) versus a field cleared entirely","Silent data loss, by naming each value and offering Undo"],"limits":["You define which values are valid for each parent value","Undo restores the previous values in the form; it doesn't undo anything already saved"],"a11y":["The notice is role=\"status\" with aria-live=\"polite\"","Each lost field is described in text (e.g. \"cleared\" or \"removed\")","Dismiss has an aria-label"]}},{"name":"version-scrubber","type":"registry:ui","title":"Version Scrubber","description":"Version timeline with the current version pinned, a word diff of exactly what a restore would change, and a second confirm step that counts words lost and gained and names changed numbers.","categories":["Forms and saving"],"files":[{"path":"registry/default/version-scrubber/version-scrubber.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/text-diff.json"],"meta":{"stops":"Restored wrong version of document","mistakes":["restored wrong version","version history lost words","rollback document","compare versions"],"aliases":["version history","restore version","diff","timeline","google docs history","revisions","time travel"],"flows":["forms"],"fields":["text"],"summary":"Lets people scrub through saved versions and see exactly what a restore would change before confirming it.","catches":["What a restore would change, as a word diff","Changed numbers, called out","Words lost and gained, counted in the confirmation","Selecting the current or an identical version (nothing to restore)","Restore failures, with the current version kept"],"limits":["Word-level diff, not semantic","Restore runs through your onRestore"],"a11y":["Timeline slider is labelled with aria-valuetext naming the version","Diff parts have sr-only \"comes back\" / \"removed\" prefixes","Confirm is a labelled group; Cancel gets focus; Escape closes","Errors use role=alert; summary in a polite live region"]}},{"name":"autofill-check","type":"registry:ui","title":"Autofill Check","description":"Detects autofill (the :autofill animationstart hook plus a no-typing batch heuristic), outlines and lists filled fields for review, flags an email in Name or a value that differs from the account with a one-tap fix, and holds the first submit.","categories":["Forms and saving"],"files":[{"path":"registry/default/autofill-check/autofill-check.tsx","type":"registry:ui"},{"path":"registry/default/autofill-check/autofill-rules.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Autofill put email in name field","mistakes":["autofill wrong field","password manager filled wrong","chrome autofill address wrong","autofill submitted wrong data"],"aliases":["autofill","autocomplete","password manager","chrome autofill","address form","checkout form","1password"],"flows":["forms","checkout"],"fields":["email","name","address"],"summary":"Highlights fields that the browser or password manager autofilled and flags values that look wrong, like an email in the Name field.","catches":["Fields filled without typing, detected by the :autofill hook or a timing heuristic","Values that do not fit the field (an email in Name, a number in Email)","Values that differ from what the account already holds (an old address), with a one-tap replacement","The same value filled into two different fields"],"limits":["Detection is heuristic; some password managers and browsers fill in ways it cannot see, and fast pasting can look like autofill","Field types are guessed from autocomplete, type and name attributes","The first submit is held only once; the second goes through"],"a11y":["Review banner is aria-live=\"polite\" and becomes role=\"alert\" when a submit is held","A held submit moves focus to the first suspect field; list entries focus their field","The highlight animation respects prefers-reduced-motion","Field names in the list come from their <label> or aria-label"]}},{"name":"field-history","type":"registry:ui","title":"Field History","description":"A trigger beside the label flags a recent change by someone else and opens \"Was ₹500 until Priya changed it yesterday\" with a one-tap revert of that field alone, says what the revert replaces (including your unsaved edit) and lets you undo it.","categories":["Forms and saving"],"files":[{"path":"registry/default/field-history/field-history.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Overwrote teammate's change unknowingly","mistakes":["someone changed the price","who changed this field","revert field change","overwrote colleague edit"],"aliases":["field history","audit log","change history","revert","who changed","activity","collaboration","price change"],"flows":["forms"],"fields":["amount","text"],"summary":"Flags a field someone else just changed and shows its previous value with a one-tap revert, so a teammate's change isn't overwritten unknowingly.","catches":["Recent changes by someone else (within recentFor)","The previous value and who changed it","Reverts that would also replace your unsaved edit","Regret after a revert, with Undo"],"limits":["History comes from your props; it doesn't track changes itself","Reverting runs your onRevert"],"a11y":["Pass htmlFor so the label is a real <label> for the field","The trigger has aria-expanded, aria-controls and an aria-label that says when it changed","Animations respect prefers-reduced-motion"]}},{"name":"terms-changes","type":"registry:ui","title":"Terms Changes","description":"Terms re-acceptance with a summary and exact word diff since the accepted version, changed numbers called out, Accept locked until every change has stayed on screen long enough to read (an early tap jumps to the next unread one), and a consent record with both versions.","categories":["Forms and saving"],"files":[{"path":"registry/default/terms-changes/terms-changes.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/text-diff.json"],"meta":{"stops":"Terms accepted without seeing changes","mistakes":["updated terms no diff","accept terms blindly","privacy policy changed","consent record terms"],"aliases":["terms of service","privacy policy","tos update","accept terms","diff","consent","legal","dpdp"],"flows":["signup-login","account-settings"],"fields":["text"],"summary":"Shows what changed in updated terms since the version a user accepted, and keeps Accept locked until each change has been on screen.","server":"Record consent server-side with the version ids and timestamp, and check the accepted version on later requests.","catches":["Every change since the accepted version, as a word diff","Changed numbers (fees, days, limits), called out","Changes not yet on screen for the dwell time","An early Accept tap, which jumps to the next unread change","Consent record with both versions, timestamp and changes seen"],"limits":["\"Seen\" means on screen for dwell ms, not read or understood","Not legal advice on what counts as valid consent in your jurisdiction","Store the consent record server-side yourself"],"a11y":["Changes region is focusable (tabIndex=0) with an aria-label","Removed/added text has sr-only prefixes; read changes marked with an aria-labelled check","Progress in a polite live region; Accept uses aria-disabled and is described by what's left","Scrolling jumps respect reduced motion"]}},{"name":"hidden-field-guard","type":"registry:ui","title":"Hidden Field Guard","description":"Banner for a hidden conditional section that names the fields still holding values and says whether they will be submitted or dropped, with Keep, Clear (through real input events so React state follows) and Undo.","categories":["Forms and saving"],"files":[{"path":"registry/default/hidden-field-guard/hidden-field-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Hidden field still submitted old value","mistakes":["hidden field submitted","conditional field kept value","toggled section still sent","ghost values in form"],"aliases":["conditional fields","hidden fields","show hide","react hook form","shouldUnregister","form state","toggle section"],"flows":["forms"],"fields":[],"summary":"When a conditional form section is hidden, it names the fields still filled in there, so old values don't ride along unseen.","catches":["Values left in a section the user has hidden","Whether those values will still be submitted","Clearing that bypasses React state: it fires real input events instead"],"limits":["Only sees native inputs inside the section's DOM; custom widgets that keep values elsewhere are missed","Submitted vs dropped depends on the hiddenValues/submitted setting you pass, not on inspecting your submit code"],"a11y":["Banner is a labelled region inside a polite live region","Field names come from the field's label, aria-label, placeholder or name","Keep, Clear and Undo are buttons; icons are aria-hidden"]}},{"name":"upload-wait","type":"registry:ui","title":"Upload Wait","description":"Submit button for forms with uploads: pressing submit mid-upload validates and queues the send, the button fills with combined progress (\"Waiting for 2 uploads…\"), it submits by itself when they finish, a failed upload pauses it until retried, and leaving asks first.","categories":["Forms and saving"],"files":[{"path":"registry/default/upload-wait/upload-wait.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Form submitted before upload finished","mistakes":["submitted before upload done","attachment missing after submit","upload in progress submit","left page mid upload"],"aliases":["upload progress","file upload","submit button","waiting for upload","s3 upload","document upload","kyc docs"],"flows":["forms"],"fields":["file"],"summary":"Lets people submit a form while attachments are still uploading: it queues the send, shows progress and submits when uploads finish.","server":"Validate on the server that every referenced upload exists and finished before accepting the form.","catches":["Submitting before attachments have uploaded (queued instead)","Failed uploads, which pause the send until retried","Leaving the page mid-upload","Cancelling the queued send"],"limits":["Upload state comes from your uploads prop","The server must still reject submissions that reference missing files"],"a11y":["Uploads list has an aria-label; retry buttons name the file","Status in a polite live region","Submit uses aria-busy while waiting or sending and aria-disabled once sent","Focus moves to Retry when an upload fails"]}},{"name":"consent-group","type":"registry:ui","title":"Consent Group","description":"Boxes are never pre-ticked, required terms sit apart from optional purposes, Accept all spells out what it will tick first, one sentence reads back the choice, and each change produces a record with time, method, text version and a fingerprint of the wording.","categories":["Forms and saving"],"files":[{"path":"registry/default/consent-group/consent-group.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pre-ticked consent bundled with terms","mistakes":["pre ticked checkbox consent","bundled marketing consent","gdpr consent checkbox","accept all unclear"],"aliases":["consent","checkbox","gdpr","dpdp","marketing emails","newsletter opt-in","cookie consent","terms checkbox","signup"],"flows":["signup-login","forms"],"fields":[],"summary":"Consent checkboxes that are never pre-ticked and keep required terms apart from optional purposes, and record what was agreed and when.","server":"Store consent records server-side with the exact text version shown, and let users withdraw as easily as they gave consent.","catches":["Pre-ticked consent boxes","Optional purposes bundled with required terms","\"Accept all\" ticking more than the person read, by listing it first","Changes to consent wording, via a version and a fingerprint in each record"],"limits":["The fingerprint is a short FNV-1a hash for spotting wording changes, not a tamper-proof signature","Records are passed to you; storing them and honouring withdrawal is up to your backend","Not legal advice: whether consent is valid depends on your jurisdiction and context"],"a11y":["Required and optional sections are role=\"group\" with headings via aria-labelledby","Each checkbox has a <label> and aria-describedby to its description","Accept-all confirmation is a labelled group; focus moves to Confirm and Escape cancels","The required marker is aria-hidden; requirement is stated in the heading"],"rules":{"verified":null,"sources":[{"label":"GDPR Recital 32 (EUR-Lex, Regulation (EU) 2016/679)","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}],"note":"Not verified against the primary source on 2026-10-17: EUR-Lex could not be fetched. Recital 32 text (\"Silence, pre-ticked boxes or inactivity should not therefore constitute consent\") was read on a secondary reproduction only. Informational, not legal advice."}}},{"name":"dob-input","type":"registry:ui","title":"DOB Input","description":"Reads the date and age back as you type and catches this year typed by habit (age 0), future dates, implausible ages, a day typed into the month box, pasted dates that read two ways and under-age sign-ups, with a fix and the date someone becomes eligible.","categories":["Forms and saving"],"files":[{"path":"registry/default/dob-input/dob-input.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Birth year typed as this year","mistakes":["wrong date of birth","dob shows age 0","dd/mm vs mm/dd","underage signup"],"aliases":["date of birth","dob","birthday","age verification","dd/mm/yyyy","kyc","pan","18+","date input"],"flows":["forms","signup-login","india"],"fields":["datetime"],"summary":"Reads a birth date and age back as it is typed and catches this year typed by habit, future dates and impossible ages, so birth dates aren't saved wrong.","server":"Re-check age eligibility on the server; a client-side date check can be bypassed.","catches":["This year typed as the birth year (age 0)","Future dates and implausible ages (minAge, maxAge)","A day typed into the month box","Pasted dates that could be read two ways","Under-age sign-ups, with the date the person becomes eligible"],"limits":["Plausibility checks only; it can't verify a real birth date","Field order follows the `order` prop or locale; check it matches your users"],"a11y":["Day, month and year each have a <label>, and are described by a shared status","aria-invalid on errors; Backspace in an empty part moves to the previous part","Focus moves between parts as each one is completed"]}},{"name":"recurrence-scope","type":"registry:ui","title":"Recurrence Scope","description":"This one / This and following / All picker for recurring events that reads back the dates each choice changes, with counts and past dates called out, and warns by date and field before overwriting occurrences that were edited on their own.","categories":["Forms and saving"],"files":[{"path":"registry/default/recurrence-scope/recurrence-scope.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Edited whole series instead of one","mistakes":["edited all events by mistake","this and following events","recurring meeting edit","overwrote edited occurrence"],"aliases":["recurring event","repeat","rrule","calendar","this event only","all events","google calendar","series"],"flows":["scheduling"],"fields":["datetime"],"summary":"Reduces edits that hit more of a recurring series than intended by listing exactly which dates each scope choice changes.","catches":["Exactly which occurrences each scope touches, with a count","Past occurrences included in the change","Occurrences with their own edits that would be overwritten, by date and field","A repeat-rule change, which regenerates (and overwrites) the whole affected range","Option to keep individually edited occurrences"],"limits":["Works on the occurrences you pass in; it does not expand RRULEs itself","Does not save anything; onConfirm receives the choice"],"a11y":["Scope options are a radiogroup labelled by the title, built from labelled radio inputs","Changed dates list has an aria-label; changed/kept/unchanged state shown as text, not colour alone","Count and overwrite warning render in polite live regions","Save disables while pending, with an aria-hidden spinner"]}},{"name":"tree-select-readback","type":"registry:ui","title":"Tree Select Readback","description":"Nested checkbox tree that reads the choice back in plain words (\"All of Marketing except Paid ads; 3 of 12 in Sales\"), tells a whole-group tick apart from ticking every item, and warns that a whole-group tick includes items added later, with a one-tap pin to today's items.","categories":["Forms and saving"],"files":[{"path":"registry/default/tree-select-readback/tree-select-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Whole group ticked, future items included","mistakes":["tree checkbox selected too much","select parent selects all","nested checkbox confusion","indeterminate checkbox"],"aliases":["tree select","nested checkbox","indeterminate","permissions tree","category filter","multi select","folder picker"],"flows":["forms"],"fields":[],"summary":"Reads a nested checkbox selection back in plain words and tells a whole-group tick apart from ticking every item today.","catches":["Whole-group selections that will include future items","Groups partly selected, read back as \"3 of 12\"","Exceptions inside a selected group","A pin-to-today option that converts a group rule to today's items"],"limits":["Future-items semantics apply only when the group is open (open !== false)","Readback wording is English by default; override the text props"],"a11y":["Expand buttons use aria-expanded/aria-controls and an aria-label","Children are a role=group labelled by the parent","Checkboxes use <label>s","Readback in a polite live region"]}},{"name":"repeat-rows","type":"registry:ui","title":"Repeat Rows","description":"Add-another rows (line items, guests) with min and max limits, Undo after a remove, duplicate rows flagged with a one-tap remove, blank rows called out before submit, Enter that adds a row instead of submitting, and focus that lands on the right field.","categories":["Forms and saving"],"files":[{"path":"registry/default/repeat-rows/repeat-rows.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Duplicate line items in add-another form","mistakes":["duplicate rows in form","removed row by mistake","blank line items submitted","enter key submitted form"],"aliases":["add another","field array","line items","useFieldArray","guests","invoice items","dynamic rows","react hook form"],"flows":["forms","checkout"],"fields":["text","number"],"summary":"Reduces lost, doubled and blank entries in add-another forms with undoable removes, duplicate flags and min/max limits.","catches":["Duplicate rows, compared case- and space-insensitively on uniqueBy keys","Blank rows, with a one-tap clean-up","Accidental removes, with Undo in place","Enter in the last row adding a row instead of submitting","Min and max row counts"],"limits":["Duplicate check compares only the keys you list in uniqueBy","Undo is time-limited (undoMs) and in-memory","Validation of field contents is up to you"],"a11y":["Each row is a group labelled \"Item n of m\"; inputs carry per-row aria-labels","Enter moves to the next row or adds one; focus moves to the new or next logical field after add/remove","Remove buttons have aria-labels and disable at the minimum","Duplicate and blank notices render in polite live regions"]}},{"name":"step-invalidation","type":"registry:ui","title":"Step Invalidation","description":"Wizard helper that says exactly what a change to an earlier answer clears (\"Changing plan to Basic clears Region on step 3…\") before applying it, marks affected steps, reviews only them and holds Finish until they're checked; changing back restores what was removed.","categories":["Forms and saving"],"files":[{"path":"registry/default/step-invalidation/step-invalidation.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/dependent-reset.json"],"meta":{"stops":"Earlier wizard change broke later steps","mistakes":["changed step 1 broke step 3","wizard answers cleared","multi step form invalidated","plan change removed options"],"aliases":["wizard","stepper","multi-step form","onboarding","plan selection","dependent steps","configurator"],"flows":["forms","signup-login"],"fields":[],"summary":"Says which later wizard answers an earlier change will clear before it applies, then walks back through only those steps.","catches":["Later answers a change would clear or trim, grouped by step","Steps needing review, marked in the stepper","Finish attempted before affected steps are checked","Changing the answer back, which restores what was removed"],"limits":["Dependencies are what you declare in the wizard config","Restore works within the session; it isn't persisted"],"a11y":["Stepper buttons use aria-current=\"step\" and aria-labels that include the stale hint","Impact message is role=status (polite)","Keep, change and review are labelled buttons; icons are aria-hidden"]}},{"name":"import-preview","type":"registry:ui","title":"Import Preview","description":"Import preview that finds the real heading row, maps columns by heading and by values with a confidence, holds the import while a required field is unmapped, and lists every skipped line with its line number and reason before a plain \"Import 412 · skip 7\".","categories":["Forms and saving"],"files":[{"path":"registry/default/import-preview/import-match.ts","type":"registry:ui"},{"path":"registry/default/import-preview/import-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bad CSV import created junk records","mistakes":["csv import wrong columns","header row imported as data","duplicate rows imported","excel import errors"],"aliases":["csv import","excel upload","bulk upload","column mapping","import contacts","xlsx","papaparse","data import"],"flows":["forms","devtools"],"fields":["file"],"summary":"Previews a CSV or spreadsheet import, finds the real heading row and maps columns, so a bad file doesn't create junk records.","server":"Validate rows and enforce uniqueness constraints on the server during the import.","catches":["Title lines above the real headings, and headings about to be imported as a record","Columns matched by heading or by their values, with a confidence","Required fields left unmapped: import is held","Duplicates in the file and against existing records","Repeated headings and cells that fail a field's check"],"limits":["Needs rows already parsed; it doesn't read CSV or XLSX itself","Value-based matching is a guess; check low-confidence columns","Duplicate check uses one key field, trimmed and case-insensitive"],"a11y":["Each column mapping select has a label in a labelled list","Status and counts are in polite live regions; unmapped required fields use role=alert","Skipped-line groups expand with aria-expanded"]}},{"name":"crop-guard","type":"registry:ui","title":"Crop Guard","description":"Checks pluggable face and text boxes against the crop, the profile circle and Story safe zones, offers a one-tap fit that moves the crop as little as possible, and previews it at the small size people actually see.","categories":["Forms and saving"],"files":[{"path":"registry/default/crop-guard/crop-check.ts","type":"registry:ui"},{"path":"registry/default/crop-guard/crop-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Profile crop cut off face","mistakes":["crop cut head off","text hidden under story ui","avatar crop bad","image crop safe zone"],"aliases":["image crop","avatar upload","profile photo","instagram story","safe zone","cropper","thumbnail","dp"],"flows":["account-settings","forms"],"fields":["file"],"summary":"Checks a crop against faces, headlines and platform overlays, so a profile photo or Story doesn't cut off a face or hide text under the UI.","catches":["Faces or text boxes cut by the crop edge","Content hidden by a circle mask or a platform's Story UI","Subjects too small to recognise at thumbnail size","A nearest fit that keeps everything in, moving the crop as little as possible"],"limits":["Needs face and text boxes from you or a detect function; it has no built-in detector","Safe zones are presets and may not match a platform's latest layout","Sometimes no crop can fit everything; it says so"],"a11y":["The crop area is keyboard-operable: arrow keys move it, + and - resize","Shape presets are a role=\"radiogroup\" with arrow-key navigation","The image takes alt text; problems are described in text"]}},{"name":"stay-length","type":"registry:ui","title":"Stay Length","description":"Date range readback (\"Mon 2 Nov – Sun 8 Nov · 6 nights, 7 days\") that flags an end before the start, nights confused with days, a check-out that may really be the last night, and weekends or holidays that change how much leave it costs, each with a one-tap fix.","categories":["Forms and saving"],"files":[{"path":"registry/default/stay-length/stay-length.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Booked one night too few","mistakes":["nights vs days confusion","checkout date off by one","leave days miscounted","end date before start"],"aliases":["date range","check-in check-out","hotel booking","leave request","nights","holidays","date picker","oyo","airbnb"],"flows":["scheduling","checkout","india"],"fields":["datetime"],"summary":"Reads a date range back as nights and days so bookings and leave requests aren't off by one.","catches":["End date before start date","Nights versus days confusion against the expected count","A check-out date entered as the last night","Weekend days and holidays inside the range (for leave)"],"limits":["Works on calendar dates only, never times","Holidays are yours to pass; weekend days default to Saturday–Sunday unless you set weekend"],"a11y":["Start and end inputs have <label>s; aria-invalid on errors","Readback in a polite live region via aria-describedby; errors use role=alert","Fix and keep are labelled buttons"]}},{"name":"event-overlap","type":"registry:ui","title":"Event Overlap","description":"Lists clashes (overlaps, back-to-back with no break, too little travel time between places, busy all-day events, other-zone events in their own time) on a small timeline, and moves the event to the nearest slot that keeps your buffer and travel time, with Undo.","categories":["Forms and saving"],"files":[{"path":"registry/default/event-overlap/event-overlap.tsx","type":"registry:ui"},{"path":"registry/default/event-overlap/overlap.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Meeting booked on top of another","mistakes":["double booked meeting","calendar conflict","no buffer between meetings","timezone meeting clash"],"aliases":["calendar","double booking","conflict","meeting scheduler","calendly","buffer time","travel time","time zone"],"flows":["scheduling"],"fields":["datetime"],"summary":"Lists what a new or moved calendar event runs into and moves it to the nearest free slot, so meetings aren't booked on top of each other.","catches":["Overlapping events and busy all-day events","Back-to-back events with no break","Too little travel time between places","Events in other time zones, shown in their own local time","The nearest free slot within work hours, with Undo"],"limits":["Only knows the events you pass; other calendars are unseen","Travel times come from your data"],"a11y":["The timeline is aria-hidden; clashes are listed in text","Results are aria-live=\"polite\"","Move and Undo are buttons with visible text"]}},{"name":"unanswered-check","type":"registry:ui","title":"Unanswered Check","description":"Pre-submit check for tests and surveys: required and optional blanks, partly answered and flagged questions as numbered jump links, time left and per-question pace, a second click for blanks, a hard stop for required survey questions and a submit that waits for saves.","categories":["Forms and saving"],"files":[{"path":"registry/default/unanswered-check/unanswered-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Test submitted with questions skipped","mistakes":["submitted quiz with blanks","missed questions in exam","survey skipped required","answers not saved on submit"],"aliases":["quiz","exam","test","survey","questionnaire","mcq","assessment","google forms","review answers"],"flows":["forms"],"fields":[],"summary":"Lists blank, partial and flagged questions before a test or survey is submitted, with jump links and time left.","server":"Enforce required answers and the deadline on the server; accept submissions only once all answers are saved.","catches":["Required and optional questions left blank","Partly answered questions (\"1 of 2 chosen\")","Questions flagged for review","Answers not yet saved to the server","Time left and how much each open question gets"],"limits":["Only knows the question states you pass","Time limits and auto-submit rely on your onTimeUp and server timing"],"a11y":["Section labelled by its heading; countdown is role=timer with an aria-label","Jump links have aria-labels including notes","Status in a polite live region; errors use role=alert"]}},{"name":"cutoff-picker","type":"registry:ui","title":"Cutoff Picker","description":"Counts down to the cutoff in store time with the shopper's time beside it, names skipped holidays and closed days, says when the store's date differs, and never moves a chosen date silently: a passed cutoff is announced with the new earliest date one tap away.","categories":["Forms and saving"],"files":[{"path":"registry/default/cutoff-picker/cutoff-picker.tsx","type":"registry:ui"}],"dependencies":["@date-fns/tz","@number-flow/react","date-fns","lucide-react","motion"],"meta":{"stops":"Order missed today's delivery cutoff","mistakes":["missed same day delivery cutoff","delivery date changed silently","store timezone different","order after cutoff"],"aliases":["delivery slot","same day delivery","cutoff time","order by","delivery date","blinkit","time slot","store hours","holidays"],"flows":["checkout","scheduling","india"],"fields":["datetime"],"summary":"Counts down to a store's order cutoff in store time with the shopper's time beside it, so an order doesn't land on a different day than expected.","server":"Re-check the cutoff on the server at order time and reject or reschedule dates that are no longer available.","catches":["Store time zone differing from the shopper's (including a different calendar date)","Holidays and closed days the delivery date skips","A cutoff that passes while the shopper is still choosing","Time needed to finish checkout before the cutoff (checkoutMinutes)"],"limits":["The cutoff, holidays and closed days come from your props","Uses the visitor's clock unless you pass `now`; a wrong device clock skews the countdown"],"a11y":["Dates are keyboard-navigable with arrow keys","The countdown is a role=\"img\" with an aria-label; the animated digits are aria-hidden","Changes are announced in a role=\"status\" region","Motion respects prefers-reduced-motion"]}},{"name":"trip-date-chain","type":"registry:ui","title":"Trip Date Chain","description":"Night-by-night strip of stays under flights that catches a small-hours landing with the room booked from that date, nights paid before a +1 arrival, gap nights, double-booked nights and a room kept past a small-hours departure, with one-tap fixes and Undo.","categories":["Context and awareness"],"files":[{"path":"registry/default/trip-date-chain/chain.ts","type":"registry:ui"},{"path":"registry/default/trip-date-chain/trip-date-chain.tsx","type":"registry:ui"}],"dependencies":["lucide-react","motion","date-fns","@date-fns/tz"],"meta":{"stops":"Hotel booked for the wrong night","mistakes":["landed after midnight no hotel room","hotel booked a day late after overnight flight","gap night between hotels","double booked hotel nights","red-eye flight wrong date"],"aliases":["itinerary","hotel booking","check-in date","check-out date","red-eye","overnight flight","+1 day arrival","travel planner","airbnb","multi-city trip"],"flows":["scheduling","checkout"],"fields":["datetime"],"summary":"Lines up hotel nights under the flights and catches a stay booked for the wrong night, like a room from the date of a 00:50 landing.","catches":["Room booked from the date of a small-hours arrival (needs the night before)","Nights paid before a +1-day arrival","Gap nights with no stay","Overlapping stays (double-booked nights)","Room kept past a small-hours departure","Tight connections below minConnectionMinutes"],"limits":["Times are local wall-clock times as printed; add IANA zones for real durations","Reads only the legs and stays you pass","earlyHours defines \"small hours\" (configurable)"],"a11y":["Visual strip is aria-hidden; every finding is also text","Fix and Undo are labelled buttons; outcome via role=status","Focus moves to the next fix after one is applied","Animations respect reduced motion"]}},{"name":"meeting-time-spread","type":"registry:ui","title":"Meeting Time Spread","description":"Meeting time slider with each attendee's day underneath (night, before or after work, weekend, day ahead or behind, DST weeks via the tz database) that suggests the kindest slot: nobody at night, then fewest out of hours, then rotating away from recent sufferers.","categories":["Context and awareness"],"files":[{"path":"registry/default/meeting-time-spread/meeting-time-spread.tsx","type":"registry:ui"},{"path":"registry/default/meeting-time-spread/spread.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion","@date-fns/tz"],"meta":{"stops":"Same colleague always gets 3 AM calls","mistakes":["meeting at night for remote colleague","time zone meeting planner","unfair recurring meeting time","dst changed meeting time","find time across time zones"],"aliases":["world clock","time zone","scheduler","meeting planner","calendar","remote team","standup","recurring meeting","working hours","slider"],"flows":["scheduling"],"fields":["datetime"],"summary":"Shows every attendee's local day under a meeting-time slider and suggests the kindest slot, so the same person doesn't always take the 3 AM call.","catches":["Slots in someone's night or outside their working hours","Weekend or a different calendar day for some attendees","DST weeks when the time gap shifts","The same person repeatedly taking bad slots (from history)"],"limits":["Working hours default to 9–17.5 local and weekends to Sat/Sun; pass real ones","History-based fairness only knows the past meetings you pass","Plans one day at a time"],"a11y":["Slider has a visible label and aria-valuetext describing who is at night","Status uses role=status; icons are aria-hidden","Focus returns to the slider after applying a suggestion","Animations are reduced under reduced motion"]}},{"name":"bnpl-schedule-check","type":"registry:ui","title":"BNPL Schedule Check","description":"Draws each payment on a calendar beside other plans and paydays, flags a card that expires before a payment, payments just before payday and plans stacking in one week, offers one-tap fixes, shows the late fee in money and asks for a second press while the card problem stands.","categories":["Context and awareness"],"files":[{"path":"registry/default/bnpl-schedule-check/bnpl-schedule-check.tsx","type":"registry:ui"},{"path":"registry/default/bnpl-schedule-check/bnpl.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion","date-fns"],"meta":{"stops":"Instalment fails on an expired card","mistakes":["bnpl payment due before payday","card expires before last instalment","too many pay later plans at once","missed klarna payment late fee","pay in 4 schedule clash"],"aliases":["buy now pay later","bnpl","pay in 4","klarna","afterpay","affirm","instalments","installment plan","payment schedule","checkout financing"],"flows":["checkout","scheduling"],"fields":["amount","datetime"],"summary":"Lays out buy-now-pay-later payments on a calendar with paydays and other plans, so an instalment doesn't fail on an expired card or before payday.","server":"The provider's schedule, fees and card on file are the source of truth; re-check them on the server before confirming.","catches":["A card that expires before a scheduled payment","Payments due shortly before payday (gap set by payGapDays)","Several plans charging in the same week","The late fee and its cap, shown in money"],"limits":["Paydays, other plans, the card expiry and the late-fee rule all come from your props","Moving a payment only changes the proposed date; the provider must allow it"],"a11y":["The card problem is announced with role=\"alert\"","Payment source is a role=\"radiogroup\" with arrow-key navigation","Confirm sets aria-busy while pending; status is announced in a role=\"status\" region","Issues use text and icons; motion respects prefers-reduced-motion"]}},{"name":"transit-visa-check","type":"registry:ui","title":"Transit Visa Check","description":"Layover check against starter airport transit visa lists (UK, Germany, France, Netherlands; US has no airside transit) and border-entry traps (self-transfer, onward Schengen flight, UK to Ireland, leaving the airport), with exempting documents and official links.","categories":["Context and awareness"],"files":[{"path":"registry/default/transit-visa-check/transit-rules.ts","type":"registry:ui"},{"path":"registry/default/transit-visa-check/transit-visa-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Stranded at a layover without a visa","mistakes":["layover needs transit visa","denied boarding on connection","self transfer visa problem","indian passport transit visa","schengen transit visa check"],"aliases":["airport transit visa","layover","self-transfer","flight booking","passport","schengen","timatic","indian passport","connecting flight"],"flows":["checkout","india"],"fields":["text"],"summary":"Flags layovers that may need a transit visa for the traveller's passport, and the itinerary traps that force them through the border.","catches":["Nationalities on a country's airport transit visa list","Documents that commonly exempt (e.g. valid US/CA/UK visas, EEA residence)","Self-transfers and separate tickets that mean collecting bags and entering","An onward flight to another Schengen country, which means entering here","UK to Ireland and leaving the airport during the layover","Airports with no airside transit (US)"],"limits":["Starter rules for a few countries only; it is not a visa determination","Visa lists and exemptions change; each rule carries its own checked date","Airlines decide boarding: confirm with Timatic, the airline or the official source"],"a11y":["Section labelled by its heading; every input and document checkbox has a <label>","Verdict in a polite live region with icon and text","Official and Timatic links have visible text"],"rules":{"verified":null,"sources":[{"label":"UK – Immigration (Passenger Transit Visa) Order 2014","url":"https://www.legislation.gov.uk/uksi/2014/2702/article/4"},{"label":"Germany – Airport transit visa","url":"https://www.germany.info/us-en/service/visa/airport-transit-visa-924624"},{"label":"France-Visas – Airport transit visa","url":"https://france-visas.gouv.fr/en/web/france-visas/visa-de-transit-aeroportuaire"},{"label":"US – Transit visas","url":"https://travel.state.gov/content/travel/en/us-visas/other-visa-categories/transit.html"}],"note":"Nationality lists carry a checked date of 2026-10-14 from a previous round but were not re-verified this round (official pages blocked automated fetches). GOV.UK confirms the DATV/Visitor in Transit split and the visitor-visa exemption. Informational; always confirm with Timatic or the official source."}}},{"name":"llm-cost-estimate","type":"registry:ui","title":"LLM Cost Estimate","description":"Pre-run cost panel that estimates input/output tokens and cost from your dated price table, shows the worst case without max_tokens, catches context-window overflows, runs far bigger than the last and premium models on bulk jobs, and shows Batch API savings.","categories":["Context and awareness"],"files":[{"path":"registry/default/llm-cost-estimate/cost.ts","type":"registry:ui"},{"path":"registry/default/llm-cost-estimate/llm-cost-estimate.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Surprise bill from an LLM batch","mistakes":["openai bill too high","forgot max_tokens","token cost estimate before run","prompt exceeds context window","expensive model for bulk job"],"aliases":["token counter","openai cost","llm pricing","max_tokens","context window","batch api","prompt playground","anthropic api","ai cost calculator"],"flows":["devtools"],"fields":["number","amount"],"summary":"Estimates an LLM batch's tokens and cost from your own dated price table before it runs, so a missing max_tokens or wrong model doesn't cause a surprise bill.","server":"Set spend limits and max_tokens on the server or API key; the estimate doesn't cap anything.","catches":["No max_tokens: worst-case cost shown","Request bigger than the model's context window","Run many times larger than the last one","Premium model picked for a bulk job, with a switch","Batch API discount on bulk jobs","Price table older than staleAfterDays"],"limits":["Token counts are estimates unless you pass inputTokens; real tokenizers differ by model","Prices come only from your table; it doesn't fetch provider pricing","Doesn't enforce a budget; your API key limits or server must"],"a11y":["Model select and max-tokens input have visible labels","Inputs get aria-invalid when a finding applies","Focus moves to the first finding's fix after a change"]}},{"name":"kube-context-guard","type":"registry:ui","title":"Kube Context Guard","description":"kubectl/helm field that shows the context, cluster and namespace a command will really use, flags production, the default namespace, a manifest namespace that disagrees with -n and a switched context, and asks for the context name before production changes.","categories":["Context and awareness"],"files":[{"path":"registry/default/kube-context-guard/kube-context-guard.tsx","type":"registry:ui"},{"path":"registry/default/kube-context-guard/kube.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps prevent kubectl apply on the wrong cluster","mistakes":["deployed to prod by mistake","wrong kube context","kubectl wrong namespace","helm install wrong cluster","deleted pods in production"],"aliases":["kubectl","helm","kube context","namespace","eks","gke","kubernetes","prod cluster","dry run","terminal"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Shows the cluster, context and namespace a kubectl or helm command will really hit, before it runs, so production isn't changed by accident.","server":"Restrict production with RBAC and admission control; never rely on a client-side prompt.","catches":["Production contexts, read word by word so pre-prod and nonprod aren't","Deploys into the default namespace","Manifest namespace that disagrees with -n (helm splits it silently)","Context switched since the last command","Fixes such as adding a server-side dry run"],"limits":["Works from the contexts you pass; it doesn't read your kubeconfig","Command parsing covers common kubectl/helm forms, not every flag or plugin","Typing the context name is friction, not authorization"],"a11y":["Command and confirmation inputs have visible labels","Command is described by the target readback and the issues","Status uses role=status; warnings are text with aria-hidden icons","Focus returns to a nearby fix or the input after a fix"]}},{"name":"security-event-log","type":"registry:ui","title":"Security Event Log","description":"Security activity log that highlights risky changes from other devices, groups a burst (new device, 2-step off, new recovery email) as a likely takeover, and offers \"This wasn't me\" with each remedy as an untickable checkbox. Times follow the account's zone.","categories":["Context and awareness"],"files":[{"path":"registry/default/security-event-log/events.ts","type":"registry:ui"},{"path":"registry/default/security-event-log/security-event-log.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Account takeover buried in the activity log","mistakes":["account hacked unnoticed","2fa turned off by attacker","this wasn't me button","suspicious login activity","recovery email changed"],"aliases":["security activity","login history","recent devices","account takeover","2fa","recovery email","audit log","this wasn't me","sessions"],"flows":["account-settings","signup-login"],"fields":["datetime"],"summary":"Highlights risky account changes from other devices and groups a takeover-shaped burst, with a \"This wasn't me\" path the user can review.","server":"Run the remedies server-side (revoke sessions, restore factors) and require re-authentication or a recovery check before doing so.","catches":["Risky changes from devices other than the current one","A burst of risky changes within windowMinutes, named as a likely takeover","Remedies listed before acting (sign out device, re-enable 2-step, restore recovery)","Required remedies that can't be unticked","Day grouping in the account's time zone"],"limits":["Risk and grouping are heuristics over the events you pass","Location comes from IP and is approximate","Remedies run through your onConfirm; nothing is secured until the server does it"],"a11y":["Remedy list is a group labelled by its title; Escape cancels","Remedy checkboxes have <label>s; \"choose one\" note is polite, errors are role=alert","Focus moves to the secure button on open and back to the row's report button on cancel","Times use <time> with a machine-readable dateTime"]}},{"name":"dst-shift-notice","type":"registry:ui","title":"DST Shift Notice","description":"Lists each attendee's affected dates with the new local time when daylight saving changes on different dates (US and EU weeks apart, India never), flags moves outside working hours, and offers to fix the series to the organiser's zone or UTC with a count of who moves.","categories":["Context and awareness"],"files":[{"path":"registry/default/dst-shift-notice/dst-shift-notice.tsx","type":"registry:ui"},{"path":"registry/default/dst-shift-notice/dst.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Meeting moves an hour after daylight saving","mistakes":["meeting time changed after dst","daylight saving broke recurring meeting","us eu clock change gap","standup wrong time in india","recurring event shifted hour"],"aliases":["daylight saving","dst","recurring meeting","time zone","standup","calendar","utc","ist","clock change"],"flows":["scheduling"],"fields":["datetime"],"summary":"Lists who sees a recurring meeting move by an hour when daylight saving changes on different dates, so part of the team doesn't miss it.","catches":["Recurring meetings that shift by an hour for some attendees","Weeks where US and EU clock changes differ","New local times that fall outside working hours","How many people move under each anchor zone"],"limits":["Uses the browser's time zone data (Intl), which can lag behind recent government changes","Attendee zones and working hours come from your props"],"a11y":["Section labelled by its heading; zone choices are labelled radio options","Apply result is aria-live=\"polite\"; failures use role=\"alert\"","Icons are aria-hidden; times are written in text"]}},{"name":"environment-badge","type":"registry:ui","title":"Environment Badge","description":"Marks the page, tab title (kept through navigation) and favicon with PRODUCTION, STAGING or DEV (production hatched in red), reads names like \"eu-prod-2\" word by word, never calls an unknown environment safe, and includes a hook that asks for confirmation only in production.","categories":["Context and awareness"],"files":[{"path":"registry/default/environment-badge/environment-badge.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Edited production thinking it was staging","mistakes":["changed prod instead of staging","which environment am i in","prod vs staging confusion","admin panel wrong env","deleted live data by mistake"],"aliases":["environment banner","prod badge","staging","dev","favicon","tab title","admin panel","env indicator","production warning"],"flows":["devtools","danger-zone"],"fields":[],"summary":"Marks the page, tab title and favicon with the environment, so nobody edits production thinking it is staging.","server":"Separate credentials and permissions per environment on the server; a badge is a reminder, not access control.","catches":["Working in production by mistake, via page, tab title and favicon marks","Environment names like \"eu-prod-2\", recognised word by word","Unknown environments, which are never shown as safe","Production-only confirmation through the hook"],"limits":["Only as accurate as the env value you pass","Marks the browser UI; it doesn't stop API calls made against production"],"a11y":["The environment is written out in text; short codes are aria-hidden","The production confirmation is a native <dialog> labelled by its title, with the safe button focused","Production uses a hatch pattern as well as colour"]}},{"name":"acting-as-banner","type":"registry:ui","title":"Acting As Banner","description":"Shows a sticky \"You're acting as…\" bar with who is really signed in, a countdown to when impersonation ends and a switch-back action, plus an org chip that makes each destructive button say where it acts.","categories":["Context and awareness"],"files":[{"path":"registry/default/acting-as-banner/acting-as-banner.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Edited a customer's data as myself","mistakes":["impersonation mode mistake","support agent edited wrong account","forgot i was logged in as user","admin acting as customer","wrong org selected"],"aliases":["impersonate","login as user","acting as","support mode","org switcher","admin panel","sudo mode","workspace switcher"],"flows":["account-settings","danger-zone"],"fields":["name"],"summary":"A sticky bar that says whose account you are acting as, and labels destructive buttons with the org they act on, so support staff don't edit the wrong data.","server":"Scope impersonation sessions on the server, log every action with both the actor and the subject, and expire sessions server-side.","catches":["Forgetting you are impersonating a customer","Destructive buttons that don't say which org they act on (via the org chip)","Impersonation sessions that have expired","A failed switch back, shown as an error"],"limits":["Shows the session you describe in props; it doesn't create or end impersonation","The org chip only appears where you add it"],"a11y":["A role=\"region\" with an aria-label naming who you act as and the org","Switch-back errors use role=\"alert\"","The org colour is decorative; the org is always named in text"]}},{"name":"offline-banner","type":"registry:ui","title":"Offline Banner","description":"Connection banner that counts queued changes while offline, stays up after reconnecting until the queue has actually synced, offers Retry if the sync fails, and can warn before leaving with unsynced changes.","categories":["Context and awareness"],"files":[{"path":"registry/default/offline-banner/offline-banner.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Edits made offline never reached the server","mistakes":["changes lost when offline","no internet but app said saved","sync failed silently","offline queue not synced","network down data lost"],"aliases":["offline","no internet","network status","sync queue","pwa","reconnecting","retry","connection lost"],"flows":["forms"],"fields":[],"summary":"Counts changes queued while offline and stays up after reconnecting until they have actually synced, so nobody assumes their edits reached the server.","server":"Make queued writes idempotent so replays after reconnecting don't apply twice.","catches":["Edits made offline that haven't synced","Reconnecting before the queue is flushed: banner stays until it is","Sync failure, with Retry","Leaving the page with unsynced changes (guardUnload)"],"limits":["Only shows the queue state you pass; it doesn't queue or sync anything itself","navigator.onLine can report online on a network with no internet"],"a11y":["Status text uses role=status","Hidden banner is inert so it can't take focus","Retry is a button; the syncing spinner icon is aria-hidden"]}},{"name":"stale-data-notice","type":"registry:ui","title":"Stale Data Notice","description":"Banner that appears when a tab comes back after a while (default 5 min away) or the data passes its max age (default 10 min), says how old it is and why that matters, and offers a one-tap refresh.","categories":["Context and awareness"],"files":[{"path":"registry/default/stale-data-notice/stale-data-notice.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Acting on out-of-date numbers","mistakes":["dashboard showing old data","stale tab data","price changed since page load","data not refreshed","old numbers after coming back"],"aliases":["stale data","refresh","last updated","dashboard","cache","max age","visibilitychange","swr","react query"],"flows":["forms"],"fields":["datetime"],"summary":"Tells people when the numbers on screen may be out of date, after time away or past a max age, with a one-tap refresh.","catches":["Data older than maxAge","Returning to the tab after being away longer than awayAfter","Refresh failures, with an error"],"limits":["Age comes from the updatedAt you pass","Does not refresh by itself; onRefresh does"],"a11y":["Notice is role=status; errors use role=alert","Refresh button disables while refreshing; dismiss has an aria-label","Icons are aria-hidden"]}},{"name":"conflict-banner","type":"registry:ui","title":"Conflict Banner","description":"Names who changed the record and when, shows a word diff, and before \"Keep mine\" confirms how many of their words it would replace, until you've compared.","categories":["Context and awareness"],"files":[{"path":"registry/default/conflict-banner/conflict-banner.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/text-diff.json"],"meta":{"stops":"Overwrote a teammate's edit","mistakes":["someone else edited this","last write wins overwrote changes","edit conflict","my changes overwrote theirs","concurrent edit lost"],"aliases":["edit conflict","merge conflict","optimistic locking","keep mine","version mismatch","collaboration","cms","diff"],"flows":["forms"],"fields":["text"],"summary":"Names who changed a record while you were editing and shows the difference, so nobody silently overwrites a teammate's edit.","server":"Detect conflicts on the server with a version or ETag check, and reject stale writes unless overwrite is explicit.","catches":["Saving over a newer version from someone else","Unseen differences, via a word diff","How much of the other person's work \"Keep mine\" would discard","Versions that turn out to be identical"],"limits":["Needs both versions and the editor from your backend; it doesn't detect conflicts itself","The diff is word-level on plain text"],"a11y":["role=\"region\" with an aria-label naming the editor","Message is aria-live=\"polite\"; the compare toggle uses aria-expanded","The overwrite confirmation is a labelled group; focus moves to Cancel and Escape closes it"]}},{"name":"session-timeout","type":"registry:ui","title":"Session Timeout","description":"Idle-timeout warning dialog with a countdown and a draining ring that turns red in the last 10 seconds, a Stay signed in action that extends the session through your callback, cross-tab activity sync and calm screen-reader announcements.","categories":["Context and awareness"],"files":[{"path":"registry/default/session-timeout/session-timeout.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@number-flow/react","motion"],"meta":{"stops":"Logged out and lost unsaved work","mistakes":["session expired form lost","auto logout lost data","idle timeout warning","banking session expired","logged out while filling form"],"aliases":["session timeout","idle timeout","auto logout","stay signed in","countdown","net banking","jwt expiry","keep me logged in"],"flows":["signup-login","forms"],"fields":[],"summary":"Warns before an idle session expires and lets the user stay signed in, so a timeout doesn't silently lose unsaved work.","server":"Expire sessions on the server and extend them only through an authenticated keep-alive.","catches":["Idle time approaching the timeout (warns warnBefore ms ahead)","Activity in another tab, which resets the timer (BroadcastChannel)","Sign-out in another tab, followed here","Keep-alive failures, shown while the warning stays open"],"limits":["The client timer is advisory; the server session can still expire on its own clock","Cross-tab sync needs BroadcastChannel","Does not save work; pair with autosave"],"a11y":["Native <dialog> with role=alertdialog, labelled and described","Stay signed in receives focus (autoFocus)","Countdown is spoken only from announceAt seconds, assertively; errors use role=alert","Ring and digit animation respect reduced motion"]}},{"name":"do-not-close","type":"registry:ui","title":"Do Not Close","description":"Adds a close warning and a marked tab title while work runs, shows a rolling-rate time-left estimate, says \"Safe to close\" when done and offers Retry when it fails.","categories":["Context and awareness"],"files":[{"path":"registry/default/do-not-close/do-not-close.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Closed the tab mid-payment","mistakes":["closed tab during payment","upload cancelled by closing tab","do not refresh this page","payment processing do not press back","beforeunload warning"],"aliases":["do not close","beforeunload","payment processing","upload progress","migration","do not refresh","do not press back","time left"],"flows":["checkout","forms"],"fields":["file"],"summary":"Warns before closing the tab mid-payment, upload or migration and says clearly when it is safe to close, so work isn't cut off halfway.","server":"Make long operations resumable or idempotent on the server, so a closed tab or retry doesn't leave work half done or duplicated.","catches":["Closing or reloading the tab while work is running (beforeunload warning)","Unclear progress, with a rolling-rate time-left estimate and a stalled notice","Uncertainty at the end, with an explicit \"Safe to close\"","Failures, with a Retry"],"limits":["Browsers decide whether to show the close warning and use their own text; mobile browsers often skip it","Can't prevent a crash, power loss or the OS killing the tab"],"a11y":["role=\"progressbar\" with aria-valuenow and an aria-valuetext including time left","Status in a role=\"status\" region; failures use role=\"alert\"","Retry is a labelled button"]}},{"name":"timezone-mismatch","type":"registry:ui","title":"Timezone Mismatch","description":"Banner shown when the device zone differs from the account zone: both clocks side by side, a one-time choice to use the device zone, a confirmation that keeps focus, remembered per pair of zones, comparing real clock changes so briefly drifting zones still count.","categories":["Context and awareness"],"files":[{"path":"registry/default/timezone-mismatch/timezone-mismatch.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@date-fns/tz","@number-flow/react","motion"],"meta":{"stops":"Misread every time by hours while travelling","mistakes":["wrong time zone while travelling","device timezone differs from account","times off by hours","ist vs local time confusion","booking time wrong zone"],"aliases":["time zone","timezone","device time","account time zone","travel","ist","tz database","clock","settings"],"flows":["scheduling","account-settings"],"fields":["datetime"],"summary":"When the device's time zone differs from the account's, shows both clocks and asks once which to use, so times aren't misread while travelling.","catches":["Device and account zones that differ now or at any clock change in the coming year","Zones that drift apart only for a few days (e.g. Jerusalem and Athens)","Unknown zones the runtime can't resolve","The user's choice, remembered per pair of zones"],"limits":["Choice is stored in localStorage on this device","Device zone comes from the browser and can be wrong behind some VPNs or settings"],"a11y":["Banner is a region with an aria-label; result is role=status and receives focus","Message in a polite live region","Animations respect reduced motion"]}},{"name":"update-available","type":"registry:ui","title":"Update Available","description":"New-deploy banner that polls a version endpoint (or takes a prop), explains what a refresh does, never refreshes while there are unsaved changes (offering \"Refresh after I save\"), and lets \"Later\" skip only that build.","categories":["Context and awareness"],"files":[{"path":"registry/default/update-available/update-available.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Deploy refresh wiped unsaved work","mistakes":["new version available refresh","app refreshed and lost form","stale client after deploy","chunk load error after release","refresh banner"],"aliases":["update available","new version","refresh","deploy","service worker","chunkloaderror","version polling","release banner"],"flows":["devtools","forms"],"fields":[],"summary":"Tells people a new version is out without losing work: it never refreshes over unsaved changes and offers \"Refresh after I save\".","catches":["A newer build than the one running","Unsaved changes, which hold the refresh until saved","\"Later\" applied to one build only"],"limits":["Polling interval and endpoint are yours; nothing is pushed","Dirty state comes from your dirty prop"],"a11y":["Banner is a region with an aria-label; message in a polite live region","Actions are labelled buttons; icons are aria-hidden"]}},{"name":"revision-banner","type":"registry:ui","title":"Revision Banner","description":"Banner for viewing an old revision: the content sits in a read-only striped frame that explains blocked edits, and \"Restore this version\" confirms which latest version it replaces and how many words change.","categories":["Context and awareness"],"files":[{"path":"registry/default/revision-banner/revision-banner.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/text-diff.json"],"meta":{"stops":"Edited an old version by mistake","mistakes":["edited old revision","restored wrong version","version history confusion","viewing old version of doc","restore overwrote latest"],"aliases":["version history","revision","restore version","document history","read only","docs","wiki","cms"],"flows":["forms"],"fields":["text"],"summary":"Makes an old revision unmistakable and read-only, and confirms what \"Restore this version\" replaces before it does.","catches":["Viewing an old revision, said up front with how far behind it is","Edit attempts on the old version, explained instead of silently ignored","Restore impact: which version it replaces and words changed","Restore errors, with the latest kept"],"limits":["Read-only framing is visual and ARIA; it does not disable editors you render inside","Word-change count comes from what you pass"],"a11y":["Banner is a region with an aria-label naming the version","Content frame is role=group with aria-readonly and an aria-label","Restore confirmation focuses Cancel first; Escape closes it","Blocked-edit note in a polite live region; errors use role=alert"]}},{"name":"filter-notice","type":"registry:ui","title":"Filter Notice","description":"Says how many items active filters hide, shows each filter as a removable chip with how many items it hides on its own, and warns when selected items are hidden.","categories":["Context and awareness"],"files":[{"path":"registry/default/filter-notice/filter-notice.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Thought data was missing behind a filter","mistakes":["filter hiding results","bulk delete on filtered list","selected rows hidden by filter","why are items missing","forgot filter applied"],"aliases":["filters","filter chips","data table","bulk action","search results","clear filters","hidden rows","admin table"],"flows":["forms"],"fields":[],"summary":"Says how many items active filters hide and which filter hides what, so people don't think data is missing or bulk-act on a partial set.","catches":["Items hidden by active filters, counted","Which filter hides the most, per chip","Selected items that are currently hidden"],"limits":["Counts come from your props; it doesn't compute them","Per-filter counts are what each filter hides on its own, so they can add up to more than the total hidden"],"a11y":["A role=\"region\" labelled \"Active filters\"","Counts and warnings are aria-live=\"polite\"","Filter chips are a labelled list; remove buttons name the filter"]}},{"name":"disabled-reason","type":"registry:ui","title":"Disabled Reason","description":"Keeps the control focusable with aria-disabled and explains on hover, focus or tap what blocks it, in a portalled panel no toolbar can clip, with a jump to each fix, a shake when a blocked press lands and cleared blockers sliding away.","categories":["Context and awareness"],"files":[{"path":"registry/default/disabled-reason/disabled-reason.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@floating-ui/react","motion"],"meta":{"stops":"Disabled button with no reason given","mistakes":["why is submit disabled","disabled button no explanation","can't click continue","greyed out button","aria-disabled tooltip"],"aliases":["disabled button","aria-disabled","tooltip","submit button","form validation","continue button","requirements","checkout button"],"flows":["forms","checkout"],"fields":[],"summary":"Keeps a blocked control focusable and explains exactly what blocks it, with a jump to each fix, so disabled buttons aren't dead ends.","catches":["Disabled buttons that give no reason","Every blocker at once, with a link to fix each","Presses on a blocked control, acknowledged with a shake"],"limits":["Only lists the reasons you pass; it doesn't validate the form","Don't also set `disabled` on the child, or it can't receive focus"],"a11y":["Sets aria-disabled on the child instead of disabled, so it stays focusable","Adds aria-describedby to the reasons while blocked","Opens on focus as well as hover and tap","The shake and list animations respect prefers-reduced-motion"]}},{"name":"quota-forecast","type":"registry:ui","title":"Quota Forecast","description":"Usage meter that projects the current rate to the reset (\"At this pace you'll run out Thu 22 Oct\"), turns amber or red on that forecast rather than on raw usage, marks the even-pace point, and says what happens at the limit.","categories":["Context and awareness"],"files":[{"path":"registry/default/quota-forecast/quota-forecast.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Hit the usage limit by surprise","mistakes":["ran out of api quota","usage limit reached suddenly","plan limit exceeded","credits finished early","rate of usage forecast"],"aliases":["usage","quota","plan limit","credits","api limit","billing","storage","meter","forecast"],"flows":["account-settings"],"fields":["number"],"summary":"Projects current usage to the reset date and warns when you'll run out first, so a usage limit isn't hit by surprise.","catches":["Running out before the reset at the current pace, with the date","Usage ahead of an even pace","Too early in the period to trust a rate: says so","What happens at the limit (atLimit)"],"limits":["Assumes the average rate so far continues; spikes and seasonality aren't modelled","Numbers are what you pass; it doesn't meter usage"],"a11y":["role=meter with min, max, now and aria-valuetext including the forecast","Forecast text is a polite live region","The even-pace marker is aria-hidden; the message says it in words"]}},{"name":"resume-marker","type":"registry:ui","title":"Resume Marker","description":"Saves the item a reader rested on (after a short delay) to localStorage, marks it \"You were here\" on return, and offers a jump there and back instead of scrolling the page by itself.","categories":["Context and awareness"],"files":[{"path":"registry/default/resume-marker/resume-marker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Lost my place in a long feed","mistakes":["lost scroll position","where was i reading","feed jumped to top","continue reading","unread marker"],"aliases":["you were here","continue reading","scroll position","unread","feed","docs","bookmark","last read"],"flows":["messaging"],"fields":[],"summary":"Remembers where a reader stopped in a long feed or doc and offers a jump back, without moving the page on its own.","catches":["The item the reader rested on, saved only after saveDelay so a quick flick isn't stored","Their place on return, shown as a marker","A jump to it and a jump back, never an automatic scroll","Stale or malformed saved positions, ignored after maxAge"],"limits":["Stored in localStorage per device and browser; it does not sync","Needs stable item keys; reordered or removed items lose the marker"],"a11y":["Marker is a role=separator with an aria-label","Jump and back move focus to the target item (tabIndex=-1) without an extra scroll jump","Smooth scrolling is skipped under reduced motion","Jump pill sits in a polite live region; dismiss has an aria-label"]}},{"name":"new-content-pill","type":"registry:ui","title":"New Content Pill","description":"Live feed wrapper that holds new arrivals behind a \"5 new posts\" pill that inserts and scrolls up on tap, inserting directly only when the reader is at the top and not mid-press. Works in its own scroll box or with scrollTarget=\"window\".","categories":["Context and awareness"],"files":[{"path":"registry/default/new-content-pill/new-content-pill.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Feed jumped while I was tapping","mistakes":["feed shifted under finger","tapped wrong post","new posts pushed content","layout shift on new items","scroll jump in feed"],"aliases":["new posts","feed","timeline","infinite scroll","live updates","chat","layout shift","pill","twitter feed"],"flows":["messaging"],"fields":[],"summary":"Holds new feed items behind a \"5 new posts\" pill, so the feed doesn't jump under the reader's finger while they read or tap.","catches":["Posts shifting under a tap when new items arrive","Reader losing their place mid-feed","Inserts directly when the reader is already at the top"],"limits":["Assumes a newest-first list where new items arrive at the start","Needs a stable key per item (getKey)","Doesn't virtualise long lists"],"a11y":["Pill is a button; the count is announced via role=status","After tapping, focus moves to the first feed item without scrolling past it","Smooth scroll and the pill animation are skipped under reduced motion"]}},{"name":"presence-lock","type":"registry:ui","title":"Presence Lock","description":"Presence UI that shows who is editing what (\"Priya is editing Price\"), softly locks that field in their colour with an \"Edit anyway\" that warns once, never locks someone already typing, and releases locks held by idle people. Transport-agnostic.","categories":["Context and awareness"],"files":[{"path":"registry/default/presence-lock/presence-lock.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Two people editing the same field","mistakes":["two users editing same field","collaborative edit overwrite","who is editing","field lock","multiplayer conflict"],"aliases":["presence","collaboration","field lock","liveblocks","multiplayer","realtime","avatars","who's editing","google docs style"],"flows":["forms"],"fields":["text"],"summary":"Shows who is editing which field and softly locks it in their colour, so two people don't silently overwrite each other's edit.","server":"Use versioning or merge rules on save so a late write can't silently overwrite another.","catches":["Two people editing the same field","Edit anyway: warns once before overriding","Locks held by people who went idle are released","Someone already typing is never locked out"],"limits":["Soft lock only; your backend must handle conflicting saves","Presence comes from your transport; latency can show stale state","Idle detection depends on your heartbeats"],"a11y":["Avatars are a labelled group; images have empty alt with names in titles","Joins and leaves are announced in a polite live region","A locked field is inert until Edit anyway"]}},{"name":"unsent-marker","type":"registry:ui","title":"Unsent Marker","description":"Left-behind draft shown as a dashed \"Not sent · Priya hasn't received it\" bubble in the thread where the reply would be, with Send, Edit and a two-tap Discard, plus a \"Not sent:\" preview for conversation lists.","categories":["Context and awareness"],"files":[{"path":"registry/default/unsent-marker/unsent-marker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Thought I replied but never sent it","mistakes":["forgot to hit send","draft never sent","message left in box","unsent reply","thought i replied"],"aliases":["draft","unsent","chat","reply","message composer","whatsapp","inbox","conversation list"],"flows":["messaging"],"fields":["text"],"summary":"Brings a typed-but-never-sent message back as a clear \"Not sent\" bubble, so people don't think they replied.","catches":["Text left in the composer and never sent","Who hasn't received it","Accidental discard (two taps)","Send failures, with an error"],"limits":["Drafts are kept where your useUnsentDraft storage keeps them (per device)","Doesn't know about sends from other devices unless you clear the draft"],"a11y":["Bubble is a labelled group including the text","Send failures use role=alert","Buttons have text labels; icons are aria-hidden"]}},{"name":"presenter-mode","type":"registry:ui","title":"Presenter Mode","description":"Presenting mode where values wrapped in <Sensitive> are removed from the page (not blurred) behind fixed-length masks, a visible bar shows the mode is on, a deliberate hold peeks one value, and starting a screen share in this tab suggests turning it on.","categories":["Context and awareness"],"files":[{"path":"registry/default/presenter-mode/presenter-mode.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Leaked a key on a screen share","mistakes":["api key visible on zoom","screen share leaked email","customer data on screen share","hide sensitive data while presenting","demo leaked secrets"],"aliases":["screen share","presenter mode","streamer mode","zoom","google meet","demo mode","mask","privacy","getDisplayMedia"],"flows":["devtools","account-settings"],"fields":["secret","email","amount"],"summary":"Removes marked emails, amounts and keys from the page while presenting, so they don't leak on a screen share.","catches":["Sensitive values visible during a screen share","Blur that can be reversed or read: values are removed, not blurred","Value length given away: masks are fixed-length","Screen share started in this tab: suggests presenting mode"],"limits":["Only hides values you wrap in <Sensitive>","Can detect only screen shares started from this tab with getDisplayMedia, not ones started in another app","Peeking shows the value to the audience too"],"a11y":["Masked values are focusable buttons with an aria-label naming the kind of value","Peek works by holding Space or Enter","Presenting bar uses role=status; icons are aria-hidden"]}},{"name":"locale-notice","type":"registry:ui","title":"Locale Notice","description":"Shows a value as you write it and as the reader's locale writes it, says what they would take it to mean (\"In Germany, 1,000 reads as 1\"), offers a form neither side can flip, and catches numbers typed with the other convention's decimal mark (\"1,5\" becoming 15).","categories":["Context and awareness"],"files":[{"path":"registry/default/locale-notice/locale-format.ts","type":"registry:ui"},{"path":"registry/default/locale-notice/locale-notice.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"1,000 read as one in Germany","mistakes":["comma decimal confusion","number format different locale","1,5 became 15","date format misread abroad","intl number format"],"aliases":["locale","number format","decimal separator","intl","i18n","date format","lakh format","international users"],"flows":["forms"],"fields":["number","datetime"],"summary":"Shows a number or date as the reader's locale would read it, so 1,000 isn't taken as 1 in Germany or 04/03 as the wrong day.","catches":["Numbers whose separators flip meaning between locales","Dates like 04/03 read day-first vs month-first","Decimal comma or point typed for the wrong locale","Offers an unambiguous form (SI grouping, worded month)"],"limits":["Uses the browser's Intl data, which varies by browser version","Only compares two locales at a time","Can't know the reader's locale unless you pass it"],"a11y":["Notice and hints are polite live regions","Input has a visible label and is described by the hint","Keep button has an aria-label and title; icons are aria-hidden"]}},{"name":"progress-heartbeat","type":"registry:ui","title":"Progress Heartbeat","description":"Progress bar that separates working from stalled (no heartbeat for N seconds: \"No progress for 40s\" with Retry), alive-but-not-advancing from silence, shows a time-left range that widens instead of a jumpy countdown, and holds at 99% until the job reports done.","categories":["Context and awareness"],"files":[{"path":"registry/default/progress-heartbeat/progress-heartbeat.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Killed a job that was still running","mistakes":["progress bar stuck at 99","is it frozen or working","job stalled no progress","upload hanging","cancelled working job"],"aliases":["progress bar","loading","job status","upload","export","stalled","heartbeat","eta","spinner"],"flows":["devtools","forms"],"fields":["file"],"summary":"Tells a working job from a stalled one using its heartbeat, so nobody waits on a dead job or kills a live one.","catches":["No heartbeat for stallAfter seconds","Alive but not advancing","Jumpy countdowns: a widening time-left range instead","100% shown before the job finishes: held at 99%"],"limits":["Needs heartbeat and progress timestamps from your job","Time-left range is an estimate from recent samples","Retry and cancel are your callbacks"],"a11y":["role=progressbar with min, max, now and aria-valuetext including the status","Status is text with icons aria-hidden","Bar motion is reduced under reduced motion"]}},{"name":"account-switch-guard","type":"registry:ui","title":"Account Switch Guard","description":"When several accounts are signed in, names who the action runs as and who it is not (\"Posting as Acme Corp, not Tushar\"), and catches a switch made in another tab so the button names the page's account instead of silently acting as the new one.","categories":["Context and awareness"],"files":[{"path":"registry/default/account-switch-guard/account-switch-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/acting-as-banner.json"],"meta":{"stops":"Posted from the wrong account","mistakes":["posted from company account by mistake","wrong google account","multiple accounts logged in","switched account in other tab","acting as wrong user"],"aliases":["account switcher","multiple accounts","posting as","brand account","google account","workspace","profile switch","multi-login"],"flows":["account-settings","signup-login"],"fields":["name","email"],"summary":"Names which account an action runs as, and notices when another tab switched accounts, so nobody posts from the wrong one.","server":"The server must act as the account the session actually authenticates, and reject requests whose stated account differs from it.","catches":["An account switch made in another tab of the same browser (BroadcastChannel, with a storage-event fallback)","The page's account differing from the most recently active account","Ambiguous action buttons, by naming the acting account on the button"],"limits":["Only sees switches reported through its own channel; switches made elsewhere (another browser, device or app) are invisible","It does not change which session the request is sent with; your onAction decides that"],"a11y":["Mismatch notice sits in an aria-live=\"polite\" region","The action button is described by the identity line via aria-describedby","The highlight animation is skipped when prefers-reduced-motion is set","Avatar colour is decorative (aria-hidden); the account is always named in text"]}},{"name":"maintenance-window","type":"registry:ui","title":"Maintenance Window","description":"Banner for planned downtime in the reader's time zone with a countdown that sharpens as it nears, a start button that warns when a long job would run into the window, and, during the window, a notice to keep unsaved edits plus a reload warning.","categories":["Context and awareness"],"files":[{"path":"registry/default/maintenance-window/maintenance-window.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Work lost to planned downtime","mistakes":["site down for maintenance lost work","maintenance started mid task","scheduled downtime banner","irctc maintenance","planned outage warning"],"aliases":["maintenance","downtime","scheduled maintenance","banner","countdown","read only","status page","outage"],"flows":["scheduling"],"fields":["datetime"],"summary":"A maintenance banner in the reader's own time zone, with a countdown and a warning before starting a job that would be cut off.","server":"Reject writes during maintenance on the server with a clear error so clients can retry later.","catches":["Downtime shown in the wrong time zone: converted to the reader's","Starting a long job that maintenance would cut off","Reloading with unsaved changes during the window (beforeunload warning)","Window crossing midnight"],"limits":["Disables only the fields it wraps (a disabled fieldset) in this tab; it doesn't make your app or API read-only, so block writes on the server during the window","Times come from your start and end props","The reload warning depends on the browser showing beforeunload prompts"],"a11y":["Banner is role=status","Cut-off hint is a polite live region linked to the start button","Dismiss is a button; icons are aria-hidden"]}},{"name":"permission-pre-ask","type":"registry:ui","title":"Permission Pre-Ask","description":"In-page pre-ask for browser permissions that explains why before calling the real API, tells a closed prompt from a block, shows this browser's exact re-enable steps, and flips to on by itself once the user allows it.","categories":["Context and awareness"],"files":[{"path":"registry/default/permission-pre-ask/permission-pre-ask.tsx","type":"registry:ui"},{"path":"registry/default/permission-pre-ask/permissions.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wasted the browser's one permission prompt","mistakes":["user blocked notifications","camera permission denied","location permission blocked","how to re-enable permission","notification prompt ignored"],"aliases":["permissions","notifications","camera","microphone","location","geolocation","push","priming","allow access"],"flows":["account-settings","signup-login"],"fields":[],"summary":"Explains why a permission is needed before the browser's one-shot prompt, and shows this browser's re-enable steps if it is blocked.","catches":["Spending the browser prompt before the user knows why","Closed prompt vs permanent block","Browser-specific steps to turn a blocked permission back on (iOS uses Settings)","Permission granted later from settings: detected"],"limits":["The Permissions API doesn't report every permission in every browser","Re-enable steps are written for major browsers and can drift as they change","Can't re-show the browser prompt once blocked"],"a11y":["Container uses role=status; the blocked panel is a labelled region","Icons are aria-hidden","Allow and Not now are buttons"]}},{"name":"clock-skew","type":"registry:ui","title":"Clock Skew","description":"Compares the device clock with server time (Date header or prop), says by how much and what breaks (authenticator codes, \"expires in\" timers, secure sites), corrects the countdowns inside it, and clears the warning once the clock is fixed.","categories":["Context and awareness"],"files":[{"path":"registry/default/clock-skew/clock-skew.tsx","type":"registry:ui"},{"path":"registry/default/clock-skew/skew.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Wrong phone clock breaks OTP codes","mistakes":["authenticator code not working","device time wrong","otp invalid wrong clock","timer shows wrong time","certificate error wrong date"],"aliases":["clock skew","device time","totp","authenticator","otp","server time","date header","countdown"],"flows":["signup-login","devtools"],"fields":["datetime","code"],"summary":"Detects when a device clock is off from server time, says what breaks and corrects countdowns, so OTP codes and timers don't fail mysteriously.","catches":["Device clocks ahead of or behind server time beyond a threshold","Authenticator codes that will be rejected because of the offset","\"Expires in\" timers that would show the wrong time","Clock fixes, by clearing itself when the offset goes away"],"limits":["Accuracy is limited by the Date header's one-second resolution and network round-trip","Needs a server time source (prop or a measurable URL)"],"a11y":["A role=\"region\" with an aria-label from the title","The message is aria-live=\"polite\"","\"How to fix\" toggle uses aria-expanded and aria-controls","Animation is skipped with prefers-reduced-motion"]}},{"name":"private-mode-notice","type":"registry:ui","title":"Private Mode Notice","description":"Notice for private windows, blocked site data or a full quota, based on a real write test before any guess, that offers to export unsaved work and can warn before the tab closes.","categories":["Context and awareness"],"files":[{"path":"registry/default/private-mode-notice/private-mode-notice.tsx","type":"registry:ui"},{"path":"registry/default/private-mode-notice/storage-probe.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Drafts vanish in incognito","mistakes":["incognito lost my data","remember me not working","localstorage full","private window drafts gone","cookies blocked"],"aliases":["incognito","private browsing","localstorage","remember me","storage quota","drafts","cookies blocked","safari private"],"flows":["signup-login","forms"],"fields":[],"summary":"Warns before someone relies on drafts or \"Remember me\" in a browser that won't keep them, after testing that storage really works.","catches":["Private window that erases saved data on close","Site data blocked, so nothing can be saved","Storage nearly full, so new saves may fail","Unsaved work at risk: export button"],"limits":["Private-mode detection is sometimes inferred (\"likely\") when a write succeeds","Browsers change storage behaviour often","Export uses the data you pass in exportData"],"a11y":["Notice is a labelled region inside a polite live region","Export and dismiss are buttons; icons are aria-hidden"]}},{"name":"experimental-badge","type":"registry:ui","title":"Experimental Badge","description":"The badge opens a plain list of concrete risks (work may be lost, things may change or disappear), a feedback link and a one-tap switch back to the stable version that says what happens to their work.","categories":["Context and awareness"],"files":[{"path":"registry/default/experimental-badge/experimental-badge.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Trusted a beta feature and lost work","mistakes":["beta feature lost data","experimental feature risks","switch back to stable","labs feature broke","preview feature warning"],"aliases":["beta","experimental","labs","preview","feature flag","early access","badge","opt out"],"flows":["account-settings"],"fields":[],"summary":"A badge that opens the concrete risks of a beta feature and a one-tap way back to the stable version, so people don't trust it blindly.","catches":["Unclear risk of using an unfinished feature","No way back, by offering a switch to stable","What happens to work when switching back"],"limits":["Lists the risks you write; it can't know them","Switching back runs your onSwitchBack"],"a11y":["The badge is a button with aria-expanded and a descriptive aria-label","The panel is role=\"dialog\" with an aria-label","Switch-back errors use role=\"alert\""]}},{"name":"retry-after","type":"registry:ui","title":"Retry After","description":"Action button that, on 429/503, reads Retry-After (seconds or HTTP-date) and RateLimit headers, explains which limit was hit and when it resets, counts down, ignores impatient clicks and retries automatically with jitter while the user's input stays put.","categories":["Context and awareness"],"files":[{"path":"registry/default/retry-after/rate-limit.ts","type":"registry:ui"},{"path":"registry/default/retry-after/retry-after.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Retry spam extends the lockout","mistakes":["too many requests error","429 rate limit","locked out for spamming","otp resend limit","retry after header"],"aliases":["429","rate limit","retry-after","too many requests","throttling","resend otp","backoff","503"],"flows":["signup-login","devtools"],"fields":[],"summary":"Turns a 429/503 into a clear wait: which limit was hit, when it resets, a countdown, and one automatic retry instead of impatient re-clicks.","server":"Enforce the rate limit server-side and send Retry-After; the client wait only reduces wasted requests.","catches":["Retry-After as delay-seconds or an HTTP-date","IETF RateLimit / RateLimit-Policy fields and common X-RateLimit headers","Repeated clicks during the wait, swallowed with a gentle note","Automatic retry with jitter, capped by maxAutoRetries","Fallback backoff when the server gives no hint"],"limits":["Client-side wait only; another tab or a direct request is not held back","RateLimit fields follow an IETF draft and vendor variants; unusual headers fall back to backoff","The countdown uses performance.now(), so it survives clock changes but not a page reload"],"a11y":["While waiting the button stays focusable with aria-disabled (not disabled) and is described by the reason","aria-busy while the request is pending","Wait reason in a polite live region; the spam note is assertive; errors use role=alert","Label transition animation is skipped under reduced motion"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 9110 §10.2.3 Retry-After","url":"https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after"}],"note":"Retry-After (HTTP-date or delay-seconds) confirmed in RFC 9110. RateLimit/RateLimit-Policy parsing follows an IETF draft and was not re-verified this round."}}},{"name":"low-battery-guard","type":"registry:ui","title":"Low Battery Guard","description":"Pre-task banner that compares battery runway (to a safety reserve, from the Battery Status API or a measured drain rate) with the task's length, shows where the battery would cut it off, and offers \"Start when plugged in\". Stays silent when the task will finish anyway.","categories":["Context and awareness"],"files":[{"path":"registry/default/low-battery-guard/low-battery-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Upload died on a flat battery","mistakes":["battery died during upload","exam stopped low battery","video call dropped battery","export failed phone died","battery status api"],"aliases":["low battery","battery api","upload","online exam","export","plug in","charger","mobile"],"flows":["forms"],"fields":["file"],"summary":"Before a long upload, export or call, compares battery runway with the task's length and offers to start once plugged in.","catches":["Task longer than the battery's remaining runway","Battery already low before starting","Start when plugged in: runs the task once a charger connects","Silent when the task fits"],"limits":["The Battery Status API is missing in Safari and Firefox; then it uses your fallback","Runway is an estimate from current drain","Doesn't pause or resume the task itself"],"a11y":["Banner is role=status","Runway bar is role=img with an aria-label giving runway and duration","Pulse animation is motion-safe only; icons are aria-hidden"]}},{"name":"data-cost-notice","type":"registry:ui","title":"Data Cost Notice","description":"On Data Saver, mobile data or a slow connection (Network Information API, or a Save-Data hint you pass) shows the size and an honest rough time, offers a lighter version with what it saves, or waits for Wi-Fi and then starts by itself.","categories":["Context and awareness"],"files":[{"path":"registry/default/data-cost-notice/data-cost-notice.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Big download ate the mobile data pack","mistakes":["download used all mobile data","autoplay ate data","slow network huge file","wait for wifi","data saver mode"],"aliases":["mobile data","data saver","wifi only","network information api","save-data","download size","jio","slow 3g"],"flows":["india","forms"],"fields":["file"],"summary":"On mobile data, Data Saver or a slow line, shows a big download's size and rough time and offers a lighter version or waiting for Wi-Fi.","catches":["Large downloads, uploads or autoplay on mobile data","Data Saver / Save-Data preference","Slow connections, with a rough time estimate","A lighter version and how much it saves"],"limits":["The Network Information API is not available in every browser (notably Safari and Firefox); without it, pass connection details yourself","Chrome caps reported bandwidth at 10 Mbps, so times are rough and fast lines aren't called slow","\"Wait for Wi-Fi\" relies on the browser reporting the connection type"],"a11y":["Notice content is a role=\"status\" region","Version choices are a labelled role=\"group\" of buttons","The waiting pulse only animates when motion is allowed"]}},{"name":"sunset-notice","type":"registry:ui","title":"Sunset Notice","description":"Reads Sunset (RFC 8594) and Deprecation (RFC 9745) headers and their Link relations, lists what stops working, shows when in the reader's zone (flagging a different day than the UTC date), turns amber then red, and snoozes only until the next 30/7/1-day mark.","categories":["Context and awareness"],"files":[{"path":"registry/default/sunset-notice/sunset-headers.ts","type":"registry:ui"},{"path":"registry/default/sunset-notice/sunset-notice.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"API shutdown arrives as an outage","mistakes":["deprecated api stopped working","missed api sunset date","deprecation header","endpoint removed surprise","api version end of life"],"aliases":["sunset header","deprecation","api versioning","end of life","breaking change","migration","rfc 8594","developer console"],"flows":["devtools"],"fields":["datetime","url"],"summary":"Turns Sunset and Deprecation response headers into a visible countdown, so an API shutdown isn't a surprise outage.","catches":["Sunset and Deprecation dates from response headers","Sunset earlier than deprecation, or already past","A local date that differs from the header's UTC date","Approaching deadlines (soonDays, finalDays)","Migration and policy links from Link rel=deprecation/sunset"],"limits":["Sunset timestamps are hints; the API can stop earlier or later","Snooze state lives in localStorage on this device","Accepts legacy \"Deprecation: true\" with no date"],"a11y":["Banner is a region with an aria-label; status is role=status, or role=alert once past","Snooze and migrate are labelled actions; icons are aria-hidden","Countdown bar is aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"RFC 8594 – The Sunset HTTP Header Field","url":"https://www.rfc-editor.org/rfc/rfc8594.html"},{"label":"RFC 9745 – The Deprecation HTTP Response Header Field","url":"https://www.rfc-editor.org/rfc/rfc9745.html"}],"note":"Confirmed: Sunset = HTTP-date with rel=\"sunset\" links; Deprecation is a Structured Field date (e.g. @1688169599)."}}},{"name":"print-guard","type":"registry:ui","title":"Print Guard","description":"Pre-print check that finds light-on-dark text the browser drops, collapsed sections, tables wider than the page, scroll boxes that print only what's visible and links without addresses, with print-only fixes; since Ctrl+P can't be stopped, autoFix applies them too.","categories":["Context and awareness"],"files":[{"path":"registry/default/print-guard/print-guard.tsx","type":"registry:ui"},{"path":"registry/default/print-guard/print-scan.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Printout doesn't match the screen","mistakes":["print preview broken","table cut off when printing","dark mode prints blank","print only visible part","links lost in print"],"aliases":["print","ctrl+p","print css","invoice print","pdf","receipt","print preview","@media print"],"flows":["forms"],"fields":[],"summary":"Before printing, finds what the paper will lose (dropped backgrounds, collapsed sections, cut-off tables) and fixes it for print only.","catches":["Light text on dark backgrounds the browser won't print","Collapsed sections that would print closed","Tables wider than the printable width","Scroll boxes that print only the visible part","Links printed without their address"],"limits":["Default page width is A4 with 1 cm margins; set pageWidth for Letter or other sizes","Can't change the browser's own print settings (backgrounds, scale)","Checks the live DOM at print time; content loaded later is missed"],"a11y":["Toggle uses aria-expanded; the panel is a labelled region, inert when closed","Status uses role=status; focus moves into the panel and back to Print","Highlight motion is skipped under reduced motion"]}},{"name":"bcc-guard","type":"registry:ui","title":"Bcc Guard","description":"Previews the To line as one recipient sees it, counts the names, private addresses and companies exposed, flags lists where membership is itself sensitive (\"HIV\", \"arrears\"), moves outside recipients to Bcc in one tap with Undo, and asks for a second press to send.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/bcc-guard/bcc-guard.tsx","type":"registry:ui"},{"path":"registry/default/bcc-guard/exposure.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion","@number-flow/react"],"meta":{"stops":"Recipients see each other's addresses","mistakes":["forgot to use bcc","group email exposed addresses","newsletter sent with everyone in to","cc instead of bcc","email list data breach"],"aliases":["bcc","cc","to field","recipients","newsletter","mailing list","compose","mail merge","gdpr","email client"],"flows":["messaging"],"fields":["email"],"summary":"Shows the To line as each recipient will see it, so a group email doesn't expose everyone's names and addresses to each other.","catches":["Recipients who can see each other's addresses on To or Cc","Addresses or display names that give away a person's name, and outside companies","Subjects or bodies with terms that make being on the list sensitive","Sending while addresses still show, with options to move them to Bcc or send one email each"],"limits":["Sensitive-term matching uses a word list (replaceable); other sensitive contexts are missed","Internal vs outside is decided only by the internalDomains you pass","It cannot stop replies-all or forwards after sending"],"a11y":["The preview is labelled by its heading (aria-labelledby)","Focus moves to the next sensible action (Move to Bcc, then Send)","The second-press warning uses role=\"alert\" and the Send button is described by it","Status is announced in a role=\"status\" region; motion respects prefers-reduced-motion"]}},{"name":"hidden-content-check","type":"registry:ui","title":"Hidden Content Check","description":"Pre-share check for Office files that reads the file locally with no dependencies and lists hidden and very hidden sheets, hidden rows and columns, tracked deletions, hidden text, comments, speaker notes, hidden slides, embedded workbooks, internal-drive links and author details.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/hidden-content-check/hidden-content-check.tsx","type":"registry:ui"},{"path":"registry/default/hidden-content-check/inspect.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"meta":{"stops":"Hidden tab leaked staff names","mistakes":["hidden sheet in shared spreadsheet","tracked changes left in document","speaker notes shared in deck","foi reply leaked personal data","excel very hidden sheet"],"aliases":["xlsx","docx","pptx","excel","word","powerpoint","document inspector","metadata","file upload","share file"],"flows":["messaging","forms"],"fields":["file"],"summary":"Opens an .xlsx, .docx or .pptx in the browser before it is shared and lists what is hidden inside: sheets, rows, comments, notes, tracked deletions, author details.","server":"If files are shared through your servers, scan and strip metadata there too; the client check can be skipped.","catches":["Hidden and very hidden sheets, with their real first rows shown","Hidden rows and columns, hidden text and tracked deletions","Comments, speaker notes and hidden slides","Embedded workbooks (for example a chart's source data in a deck)","Links to internal drives and author metadata","Share pressed with items still open: asks for a second press"],"limits":["Office Open XML only (.xlsx, .docx, .pptx); not .xls, .doc, PDF or images","Removal is done by your onRemove; the component re-checks whatever file you hand back","Does not detect sensitive data in visible cells, or white-on-white text"],"a11y":["Results are labelled by the summary; each finding is a focusable labelled region","Expanders use aria-expanded and aria-controls; remove buttons show aria-busy while working","Errors use role=alert; focus moves to the next open finding or the share button","Animations are skipped under reduced motion"]}},{"name":"prompt-data-guard","type":"registry:ui","title":"Prompt Data Guard","description":"AI chat composer guard that says before send what goes where (\"248 customer rows and a CONFIDENTIAL doc will go to OpenAI\"), counting table rows, name columns, emails, phones, cards, Aadhaar, PAN and API keys in text and attachments, and can send with stand-ins.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/prompt-data-guard/prompt-data-guard.tsx","type":"registry:ui"},{"path":"registry/default/prompt-data-guard/stand-ins.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion"],"registryDependencies":["https://bollard-ui.vercel.app/r/pii-post-guard.json","https://bollard-ui.vercel.app/r/secret-paste-guard.json"],"meta":{"stops":"Helps keep a customer list out of an AI chat","mistakes":["pasted customer data into chatgpt","confidential document sent to ai","pii in llm prompt","employees leaking data to ai","anonymize prompt before sending"],"aliases":["ai chat","chat composer","prompt box","data loss prevention","dlp","pseudonymize","redact","openai","csv export","gdpr"],"flows":["messaging","devtools"],"fields":["text","file","email"],"summary":"Before a message goes to an AI chat, says exactly what personal data, secrets or confidential documents are in it and where they'll go.","server":"Apply DLP and redaction on the server or gateway before data reaches the model provider.","catches":["Pasted tables with personal data, counted by row","Names in name columns and their other mentions","Emails, phones, card numbers, Aadhaar and PAN","API keys and other secrets (via detectSecrets)","CONFIDENTIAL markings in attachments"],"limits":["Names in free text need your detectNames (an NER model or CRM list)","Destination and retention text come from your destination prop","Detection is pattern-based; it can miss data or flag harmless text"],"a11y":["Visible label; textarea is described by the readback when risky","Remove-attachment buttons have aria-labels naming the file","Focus returns to the field after changes; animations are skipped under reduced motion"]}},{"name":"bill-split-check","type":"registry:ui","title":"Bill Split Check","description":"Checks an item-by-item split against the receipt and catches unclaimed items, tax counted twice, a tip on top of a service charge, a total that doesn't match and what an equal split costs the lightest eater, each with a fix and Undo. Shares are rounded so they always add up.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/bill-split-check/bill-split-check.tsx","type":"registry:ui"},{"path":"registry/default/bill-split-check/split.ts","type":"registry:ui"}],"dependencies":["lucide-react","motion","@number-flow/react"],"meta":{"stops":"Tip added on top of service charge","mistakes":["split bill doesn't add up","tax added twice when splitting","tip and service charge both paid","item nobody claimed on the bill","equal split unfair to light eater"],"aliases":["split the bill","splitwise","restaurant bill","receipt","gst","service charge","tip","settle up","group expense","upi"],"flows":["checkout","india"],"fields":["amount"],"summary":"Checks a bill split against the printed receipt so nobody is quietly overcharged by double-counted tax, a tip on a service charge or a missed item.","catches":["Items nobody claimed","Tax counted twice, or a tip added on top of a service charge","A split total that does not match the printed total (allowing for a round-off line)","How much an equal split costs the person who ordered least","Rounding drift, by largest-remainder rounding so shares add up exactly"],"limits":["Works from the receipt lines you pass in; it does not read receipts","The tax-twice and wrong-total checks need the printed total"],"a11y":["Per-person item toggles are buttons with aria-pressed and an aria-label naming person and item","Split mode is a role=\"radiogroup\" with arrow-key navigation","The tip field has a <label> and aria-invalid when it duplicates a service charge","Status is announced in a role=\"status\" region; motion respects prefers-reduced-motion"]}},{"name":"callback-number-check","type":"registry:ui","title":"Callback Number Check","description":"Reads a callback number's country and line type with libphonenumber and flags +1 Caribbean codes posing as US area codes, one-ring calls from abroad, premium-rate and shared-cost lines, foreign toll-free, unassigned ranges and 'bank' numbers missing from its list.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/callback-number-check/callback-number-check.tsx","type":"registry:ui"},{"path":"registry/default/callback-number-check/callback.ts","type":"registry:ui"}],"dependencies":["lucide-react","libphonenumber-js","motion"],"meta":{"stops":"Called back a premium-rate scam number","mistakes":["missed call from abroad scam","one ring scam","876 area code","premium rate number","fake bank number"],"aliases":["phone number","callback","libphonenumber","+91","missed call","scam call","toll free","call button"],"flows":["india","messaging"],"fields":["phone"],"summary":"Says what a number really is before you call it back, so a Caribbean, premium-rate or one-ring scam number isn't mistaken for a local one.","catches":["+1 numbers in Caribbean countries that look like US area codes (e.g. 876 Jamaica)","One-ring missed calls from abroad (wangiri pattern)","Premium-rate and shared-cost lines","Toll-free numbers that are only free in their own country, and unassigned ranges","A number claiming to be an organisation that isn't in the officialNumbers you pass"],"limits":["Line types come from libphonenumber metadata, which can lag behind real number allocations","It cannot tell who owns a number or detect a spoofed caller ID","Risky calls still go through after a second press"],"a11y":["Findings are a labelled list with icon plus text","The Call button is described by the status once armed for a second press","Status is announced in a role=\"status\" region (visually hidden until armed)","Motion respects prefers-reduced-motion"]}},{"name":"quote-lock","type":"registry:ui","title":"Quote Lock","description":"Held-quote panel that drains as the hold ends, fetches a fresh quote (skew-safe with validFor), rolls old to new with the difference called out, turns a worse-than-tolerance price into an explicit Accept, re-quotes a late Pay, and ignores stale responses and double presses.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/quote-lock/quote-lock.tsx","type":"registry:ui"},{"path":"registry/default/quote-lock/quote.ts","type":"registry:ui"}],"dependencies":["lucide-react","@number-flow/react","motion"],"meta":{"stops":"Helps prevent paying a re-quoted price nobody saw","mistakes":["price changed at payment","forex rate changed","quote expired","fare changed at checkout","slippage"],"aliases":["quote","fx rate","price lock","fare hold","swap","crypto","checkout","remittance","pay button"],"flows":["checkout"],"fields":["amount"],"summary":"Shows a held FX, swap or fare quote counting down and makes any worse re-quote an explicit Accept step, so nobody pays a price they never saw.","server":"Honour only server-side quote IDs within their validity and reject a confirm against an expired or replaced quote; make confirm idempotent.","catches":["Paying after the hold expired","New price worse than tolerance: Pay becomes Accept first","Pay pressed in the last seconds: re-quoted","Out-of-order quote responses and double presses","Client clock skew, when validFor is used"],"limits":["The server's quote and its expiry are the real truth","Tolerance and direction come from your props","Animated amounts need @number-flow/react and motion"],"a11y":["The amount is always in the Pay label as text","Errors use role=alert; Pay uses aria-disabled outside the hold","Animations are skipped under reduced motion"]}},{"name":"calendar-external-guest-guard","type":"registry:ui","title":"Calendar External Guest Guard","description":"Names guests outside your domains (and a colleague's personal Gmail used for their work address), then lists what they get under Google Calendar defaults: internal agenda lines, file names, open-link files, the guest list, edit rights. Fixes, Undo, second press to send.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/calendar-external-guest-guard/calendar-external-guest-guard.tsx","type":"registry:ui"},{"path":"registry/default/calendar-external-guest-guard/exposure.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Internal agenda shared with outside guests","mistakes":["invite sent to external guest","internal doc in calendar invite","personal gmail on work meeting","guests can see guest list","anyone with link attachment"],"aliases":["google calendar","invite","external guest","meeting","agenda","attachments","gmail","guest permissions","domain"],"flows":["scheduling","messaging"],"fields":["email"],"summary":"Shows what outside guests on an internal invite will actually see, so an agenda, file or guest list isn't shared beyond your domain by mistake.","catches":["Guests outside your domains, and personal addresses standing in for a colleague's work address","Agenda lines that contain sensitive terms","Attachments whose names show, or that anyone with the link can open","Guest list, invite-others and modify-event rights given to outside guests","Recurring events, where outside guests get every future occurrence"],"limits":["Models Google Calendar's sharing defaults; other calendar products behave differently","File access comes from what you pass (Drive link settings), not a live Drive check","Personal-address matching uses the directory you provide"],"a11y":["Guest remove buttons have aria-labels with the address","Send sets aria-busy while sending and is described by the second-press hint","Escape disarms the second press and returns focus to Send","Failures use role=\"alert\"; status updates role=\"status\""]}},{"name":"chat-audience-check","type":"registry:ui","title":"Chat Audience Check","description":"Flags members whose name differs from how you saved them, unidentifiable names, people who joined after your last message and outsiders. Quiet for ordinary messages; for sensitive text it lists them with Remove, View profile and \"I know them\" and needs a second press.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/chat-audience-check/audience.ts","type":"registry:ui"},{"path":"registry/default/chat-audience-check/chat-audience-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Secret message reached a stranger in group","mistakes":["wrong person in group chat","journalist added to signal chat","unknown member in group","sent secret to whole group","external member in channel"],"aliases":["group chat","members","signal","whatsapp group","slack channel","audience","external member","send confirmation"],"flows":["messaging"],"fields":["name","phone"],"summary":"Before a sensitive message goes to a group chat, flags members who don't seem to belong, so it doesn't reach the wrong person.","server":"Membership and access to the conversation must be enforced by the chat service; this is a pre-send review.","catches":["Members whose display name disagrees with what you saved them as","Names nobody can identify (initials, blanks)","People who joined after your last message","People outside your organisation, or marked external by the platform","Messages with sensitive terms, which raise the check"],"limits":["Only as good as the member, contact and organisation data you pass","Sensitive-term matching is a word list (replaceable)","Sending is still possible after a second press"],"a11y":["Visible <label>; the textarea is described by the flagged-member list","Escape cancels the second-press state; Enter sends only when not composing (IME-safe)","Focus moves to the first action in the flagged list","Remove buttons set aria-busy; errors use role=\"alert\" and status a role=\"status\" region"]}},{"name":"out-of-office-check","type":"registry:ui","title":"Out of Office Check","description":"Auto-reply check for Gmail or Outlook that reads back when replies go out and who gets which text, compares dates with the return date in the message, and flags replying on your return day, stopping early, no end date, last time's date, and outside replies that overshare.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/out-of-office-check/ooo.ts","type":"registry:ui"},{"path":"registry/default/out-of-office-check/out-of-office-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Auto-reply runs on the day I'm back","mistakes":["out of office wrong dates","vacation responder never ends","auto reply leaks phone number","ooo message old date","gmail vacation end date"],"aliases":["out of office","ooo","auto reply","vacation responder","gmail","outlook","leave","away message"],"flows":["messaging","scheduling"],"fields":["datetime","text"],"summary":"Reads an auto-reply back with exactly when it runs and who gets what, so it doesn't reply all day on the day you're back or tell strangers too much.","catches":["Replies still going out all day on the day you're back (Gmail ends at 11:59 PM)","Auto-reply stopping before the return date in the message","No end date, or a date left from last time","Outside replies giving a phone number, a colleague's direct address, your location or a health reason","Answering every outside sender"],"limits":["Return dates are read from the message text and may be missed in unusual wording","Platform behaviour (Gmail, Outlook) is modelled as documented by the authors and can change","Doesn't save to your mail provider; onSave does"],"a11y":["Date and time inputs have labels and get aria-invalid when a finding applies","Audience options are labelled","Focus moves to the first fix or Save after a change"]}},{"name":"retract-window","type":"registry:ui","title":"Retract Window","description":"Under a sent message: how long Edit and Unsend stay open on that platform (iMessage, WhatsApp, Slack presets or your own), a draining bar, who already read it and whose app keeps the original, Delete for me kept apart, and a correction once windows close.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/retract-window/policies.ts","type":"registry:ui"},{"path":"registry/default/retract-window/retract-window.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Trusted an unsend that didn't work","mistakes":["unsend didn't work","delete for everyone failed","edit window expired","they already read it","delete for me vs everyone"],"aliases":["unsend","delete for everyone","edit message","whatsapp","imessage","slack","recall","chat"],"flows":["messaging"],"fields":["text"],"summary":"Shows how long Edit and Unsend really stay open for a sent message, and who will keep the original anyway.","catches":["Time left to edit and to unsend, per the platform's rules","Edits used against a limit (iMessage: 5)","Recipients who already read it or whose app keeps the original","\"Delete for me\" labelled as leaving everyone else's copy","Who still has it after an unsend, and a correction once windows close"],"limits":["Presets reflect the platforms' help pages as of the verified date and change without notice","Slack windows depend on each workspace's admin settings; pass the real limits","Does not edit or unsend; your callbacks do, and a failed unsend shows an error"],"a11y":["Edit/Unsend buttons are described by their window text (aria-describedby)","Only a window closing is announced (role=status), never the countdown","Failures use role=alert; after unsend, focus moves to the result","The draining bar is aria-hidden; time left is also text"],"rules":{"verified":"2026-10-17","sources":[{"label":"Apple – Unsend and edit messages on iPhone","url":"https://support.apple.com/guide/iphone/unsend-and-edit-messages-iphe67195653/ios"},{"label":"Apple – How to edit messages (105083)","url":"https://support.apple.com/en-us/105083"},{"label":"WhatsApp – How to delete messages","url":"https://faq.whatsapp.com/1370476507114859"},{"label":"Slack – Manage permissions for message editing and deletion","url":"https://slack.com/help/articles/115004868646"}],"note":"Confirmed: iMessage unsend 2 min, edit up to 5 times within 15 min; WhatsApp edit 15 min, Delete for everyone within two days; Slack members can edit/delete any time by default, admins can limit. Check the platforms' current help pages."}}},{"name":"card-bill-pay","type":"registry:ui","title":"Card Bill Pay","description":"Before Pay, explains what the amount does: anything short of the total loses the interest-free period on the whole bill and on new spends, the minimum is turned into years and interest, and below the minimum or past the 3-day grace it shows the late fee and credit-bureau report.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/card-bill-pay/card-bill-pay.tsx","type":"registry:ui"},{"path":"registry/default/card-bill-pay/card-bill.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Minimum due paid, interest charged anyway","mistakes":["paid minimum due interest charged","credit card bill partial payment","lost interest free period","late fee credit card","cibil hit late payment"],"aliases":["credit card bill","minimum due","total due","statement","interest free","late fee","cibil","cred","rbi"],"flows":["checkout","india"],"fields":["amount"],"summary":"Reads back what a credit card payment amount really costs before Pay, so paying the minimum or a little short doesn't quietly trigger interest.","server":"Take the amount due, minimum and due date from the issuer's records, and make the payment idempotent.","catches":["Paying less than the total due, which loses the interest-free period","Paying only the minimum, shown as time and interest to clear the balance","Paying below the minimum or more than three days late (late fee and bureau reporting)","One-tap switch to paying the total"],"limits":["Uses the statement figures and fee schedule you pass; issuers' terms differ","Interest projections are estimates and assume no new spending","Rules follow RBI's credit card directions for India only"],"a11y":["Amount choices are labelled options; the custom amount has a <label>","The amount field is described by the warning lines","Pay uses aria-disabled while pending or at zero; focus moves to the first fix","Status is announced in a role=\"status\" region"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI Master Direction: Credit Card and Debit Card – Issuance and Conduct Directions, 2022 (updated 7 Mar 2024)","url":"https://www.rbi.org.in/Scripts/BS_ViewMasDirections.aspx?id=12300"}],"note":"Informational; check the official source and your card's terms. Checked: interest-free period needs the entire outstanding paid by the due date and is suspended while the previous bill is outstanding; late charges and bureau reporting only after more than three days past due."}}},{"name":"otp-share-guard","type":"registry:ui","title":"OTP Share Guard","description":"OTP display that says what the code approves (\"approves a ₹24,999 payment to Croma\") and, during a call or screen share or when it isn't what the user started, shows a never-share warning with hold-to-reveal, delayed Copy, a code hidden while sharing and \"Not me\".","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/otp-share-guard/otp-share-guard.tsx","type":"registry:ui"},{"path":"registry/default/otp-share-guard/otp.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps stop an OTP being read out to a scammer","mistakes":["shared otp with scammer","bank caller asked otp","otp during screen share","never share otp warning","didn't request this otp"],"aliases":["otp","one time password","scam","fraud","screen share","vishing","i didn't request this","sms code","upi"],"flows":["signup-login","india","checkout"],"fields":["code"],"summary":"Says in one sentence what a one-time code really approves and warns hard during a call or screen share, so it's less likely to be read out to a scammer.","server":"Bind each code to one action and amount, expire it quickly, and block approval when the user reports \"not me\".","catches":["Code approving something other than what the user started","Active phone call or screen share while a code is shown","Code drawn on a shared screen: hidden while sharing","Quick copy under pressure: Copy is delayed","\"I didn't request this\" reporting in one tap"],"limits":["Call and screen-share state must come from your app (onCall, screenSharing); the web can't detect them reliably","A determined user can still read the code aloud","Wording assumes Indian banking defaults (INR); pass your own messages"],"a11y":["Hold-to-reveal works with Space or Enter held as well as pointer","The hidden code is aria-hidden until revealed","Warnings are text with icons aria-hidden"]}},{"name":"bank-transfer-timing","type":"registry:ui","title":"Bank Transfer Timing","description":"Reads back when money arrives on the chosen rail (IMPS/UPI instantly, the next half-hourly NEFT batch plus up to 2 hours, RTGS within 30 minutes) and catches RTGS under ₹2 lakh, IMPS over ₹5 lakh, UPI over its category cap, bank limits and new-payee caps.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/bank-transfer-timing/bank-transfer-timing.tsx","type":"registry:ui"},{"path":"registry/default/bank-transfer-timing/transfer.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/amount-in-words.json"],"meta":{"stops":"Transfer refused or credited a day late","mistakes":["neft not credited","rtgs minimum 2 lakh","imps limit exceeded","new payee cooling period","upi limit exceeded"],"aliases":["neft","rtgs","imps","upi","bank transfer","fund transfer","payee","cooling period","net banking","lakh"],"flows":["india","checkout"],"fields":["amount","datetime"],"summary":"Says when an Indian bank transfer will actually arrive on IMPS, NEFT, RTGS or UPI, and catches amounts the chosen rail will refuse.","server":"The bank or payment provider enforces the real limits; validate the amount, rail and payee on the server and use idempotency for the transfer.","catches":["RTGS below the ₹2 lakh minimum and IMPS above ₹5 lakh per transaction","UPI amounts above the per-transaction or 24-hour cap for the category","Your bank's own daily limit, and a new payee's cooling period or first-day cap","Late arrival: the next NEFT batch, the last batch landing at 00:00, branch hours and holidays","Offers the rail that would work, and spells the amount in lakh and crore"],"limits":["Rail limits are built in as of the verification date; banks may set lower caps, and NPCI/RBI can change them","Bank limits, new-payee rules and holidays come only from the props you pass","Arrival times are the regulator's expected timelines, not a guarantee for a specific transfer"],"a11y":["Rails are a role=\"radiogroup\" of role=\"radio\" buttons with roving tabIndex and arrow-key navigation","The continue button uses aria-disabled rather than disabled while blocked","Status is announced in a role=\"status\" region","Issues use text and icons, not colour alone"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI FAQ: NEFT (updated 2 Sep 2024)","url":"https://www.rbi.org.in/Scripts/FAQView.aspx?Id=60"},{"label":"RBI FAQ: RTGS (updated 14 Oct 2026)","url":"https://www.rbi.org.in/Scripts/FAQView.aspx?Id=65"},{"label":"NPCI IMPS FAQ","url":"https://www.npci.org.in/faqs/imps"},{"label":"NPCI UPI OC-185B (28 Aug 2025)","url":"https://www.npci.org.in/uploads/UPI_OC_No185_B_FY_2025_26_Addendum_to_OC_185_A_Implementation_of_higher_per_transaction_limit_for_specific_categories_in_UPI_ba517a0902.pdf"}],"note":"Informational; check the official source. NEFT (half-hourly batches, credit within 2 hours), RTGS (₹2 lakh minimum, credit within 30 minutes) and IMPS (₹5 lakh) were checked against RBI/NPCI pages. The UPI category caps were not re-checked: the NPCI circular is a scanned PDF with no readable text."}}},{"name":"gift-card-scam-guard","type":"registry:ui","title":"Gift Card Scam Guard","description":"Gift card checkout check. Spots scam patterns (large total, several cards, notes about a boss, tax, police, refunds or tech support, sending codes, a first order), says no real company takes gift-card payment, and asks one question; \"someone asked me to\" ends at a stop screen.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/gift-card-scam-guard/gift-card-scam-guard.tsx","type":"registry:ui"},{"path":"registry/default/gift-card-scam-guard/scam.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps stop a buyer paying a scammer in gift cards","mistakes":["boss asked for gift cards","gift card scam","tax paid with gift cards","send gift card codes","sir ne bola gift card"],"aliases":["gift card","scam","fraud","checkout","amazon pay gift card","google play card","hinglish","tech support scam"],"flows":["checkout","india"],"fields":["amount"],"summary":"Interrupts a gift card checkout that looks like a scam payment and asks one question before the order goes through.","server":"Apply your own fraud and velocity rules on gift card orders server-side; the client prompt is advisory.","catches":["Large total or several cards in one order, scaled to the order currency","Notes that mention a boss, tax, police, refunds, tech support or sharing codes","Common Hinglish and Hindi scam phrases such as \"sir ne bola\"","First-ever gift card purchase","Someone else asking the buyer to pay: routes to a stop screen instead of checkout"],"limits":["Keyword and threshold heuristics; a scam with an innocent note and small total is not flagged","Genuine gifts that match the patterns are asked the question too","The buyer can still choose to continue; it does not block payment on its own"],"a11y":["Question section is labelled by its heading; the stop screen is role=alert","A polite live region announces changes; focus moves to the relevant control after each step","Continue button is described by its waiting reason when disabled","Decorative icons are aria-hidden"]}},{"name":"commit-secret-guard","type":"registry:ui","title":"Commit Secret Guard","description":"Scans only added diff lines for provider keys, private keys, passwords in URLs, hard-coded secrets and high-entropy strings, flags .env and key files, skips test fixtures by pattern (except live key formats), and offers per-finding fixes, warning to rotate if already pushed.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/commit-secret-guard/commit-secret-guard.tsx","type":"registry:ui"},{"path":"registry/default/commit-secret-guard/scan-diff.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"API key committed to git","mistakes":["committed .env file","aws key pushed to github","secret in git diff","hardcoded password commit","rotate leaked key"],"aliases":["git","commit","secret scanning","pre-commit",".env","gitleaks","diff","api key","github"],"flows":["devtools"],"fields":["secret","code"],"summary":"Scans the lines a diff adds for keys, passwords and high-entropy strings, so credentials are noticed before they go into a commit.","server":"Run secret scanning in CI or as a pre-receive hook, and rotate any credential that was pushed.","catches":["Provider key formats (AWS, GitHub, Stripe, OpenAI, Slack and others) and private keys","Passwords embedded in URLs and hard-coded secrets","High-entropy strings that look like tokens",".env and key files added to the commit","Changes already pushed, with a rotate-now link for the provider"],"limits":["Pattern and entropy based: it can miss custom secret formats and flag random-looking test data","Only scans the diff you pass; history and other branches are not checked","Marking a false positive is your call; it does not verify the key is inactive"],"a11y":["Findings are a labelled list that can receive focus","Focus moves to the next unresolved finding or the commit button","Each finding states its file, line and kind in text","Rotate links open in a new tab with rel=\"noreferrer noopener\""]}},{"name":"publish-preview","type":"registry:ui","title":"Publish Preview","description":"npm publish preview from `npm pack --dry-run --json` that flags .env files and keys, source maps, tests, repo config, large files, entry points missing from the build, an already-published version, private: true and no files allowlist, with fixes.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/publish-preview/pack-review.ts","type":"registry:ui"},{"path":"registry/default/publish-preview/publish-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"npm publish shipped .env","mistakes":["npm published secrets","package missing dist","wrong files in npm package","version already published","npm pack dry run"],"aliases":["npm publish","npm pack","package.json","files field",".npmignore","tarball","library","semver"],"flows":["devtools"],"fields":["file","secret"],"summary":"Previews what npm publish will ship from npm pack --dry-run and flags .env files, source maps, tests and missing build output before it goes out.","server":"Run npm pack checks in CI with a files allowlist and provenance before publishing.","catches":[".env files and keys in the tarball","Source maps, tests and repo config shipped by accident","Entry points missing from the build, with the reason (.gitignore fallback, files list, no build)","Version already published, with a next version","private: true, or no files allowlist"],"limits":["Needs the pack output and package.json you pass; it doesn't run npm","A fix only counts after a fresh pack","Doesn't check the package's runtime behaviour"],"a11y":["File list is a labelled section; the issue list is focusable","Fix buttons are real buttons; waiting state has text","Icons are aria-hidden"]}},{"name":"upi-autopay-review","type":"registry:ui","title":"UPI AutoPay Review","description":"Pre-approval review of a UPI AutoPay mandate: cap, schedule, validity and merchant read back against the plan, flagging a cap far above the price, \"as presented\" schedules, no end date, the wrong merchant and debits above the PIN-free limit, with one-tap fixes.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/upi-autopay-review/autopay.ts","type":"registry:ui"},{"path":"registry/default/upi-autopay-review/upi-autopay-review.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"₹199 plan approved as a ₹9,999 mandate","mistakes":["upi autopay mandate too high","autopay never ends","recurring mandate wrong amount","e-mandate as presented","subscription upi mandate"],"aliases":["upi autopay","e-mandate","subscription","recurring payment","npci","upi pin","mandate","paytm","phonepe"],"flows":["india","checkout"],"fields":["amount"],"summary":"Reads a UPI AutoPay mandate back against the plan before the PIN, flagging a cap far above the price, no end date or the wrong merchant.","server":"The PSP/bank enforces mandate terms; your server should create mandates with the plan's real cap and validity and verify them after registration.","catches":["A cap far above the plan price (capRatio)","No fixed schedule (\"as presented\")","No end date or 30+ year validity","Merchant name that doesn't match the plan","Debits above the PIN-free (AFA) limit"],"limits":["Format and consistency checks only; it does not register or validate the mandate with NPCI or the bank","Merchant match compares names you pass","The ₹15,000 limit is the general case; some categories have ₹1,00,000"],"a11y":["Mandate terms in a <dl>; warnings summarised and linked via aria-describedby","After a fix, focus moves to the next fix or Approve","Approve uses aria-disabled while pending; status is role=status"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI – Digital Payments – E-mandate Framework, 2026 (Apr 21, 2026)","url":"https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=13374&Mode=0"}],"note":"Confirmed: every mandate specifies a validity period the customer can modify or withdraw; issuer pre-debit notice at least 24 hours before; opt-out of a debit; AFA above ₹15,000 (₹1,00,000 for insurance, mutual funds, credit card bills). Informational; check the official source."}}},{"name":"upi-collect-guard","type":"registry:ui","title":"UPI Collect Guard","description":"Collect-request screen that states \"You pay ₹4,999 to…\", asks whether the user expects to receive money (a yes becomes a decline-only stop), and flags refund, prize, OLX-buyer and KYC wording, person-to-person collects, unknown requesters and small test amounts before the PIN.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/upi-collect-guard/collect.ts","type":"registry:ui"},{"path":"registry/default/upi-collect-guard/upi-collect-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Entered UPI PIN to receive money","mistakes":["upi pin to receive money scam","collect request fraud","olx buyer scam","kyc refund upi request","upi request money fraud"],"aliases":["upi collect","request money","upi pin","olx scam","phonepe","google pay","paytm","kyc fraud","npci"],"flows":["india","checkout"],"fields":["amount"],"summary":"Shows a UPI collect request as the payment it really is and asks whether the user expects money, since a UPI PIN only ever sends money.","server":"PSPs and banks must enforce collect rules and fraud checks; treat this as user education before the PIN.","catches":["\"Enter PIN to receive money\" confusion, by asking what the user expects","Refund, prize, buyer and KYC scam wording","Person-to-person collect requests (no longer permitted on UPI)","Requesters not in contacts or not matching the expected name","Small test amounts used to build trust"],"limits":["Heuristic wording and name checks; scams can avoid them","Does not block the UPI app's own PIN screen; it advises before it","Requester names come from what your PSP returns"],"a11y":["Intent question is a roving-tabindex radiogroup with arrow keys","Risks linked via aria-describedby; status is role=status","Choosing \"I expect money\" moves focus to Decline","Pay uses aria-disabled while pending"],"rules":{"verified":"2026-10-17","sources":[{"label":"NPCI – UPI Consolidated Circular v1.0 (18 Sep 2026), 2.16","url":"https://www.npci.org.in/uploads/UPI_Consolidated_Circular_f9c023a0ef.pdf"}],"note":"Confirmed: PSPs and UPI apps must ensure collect on person-to-person transactions is not permitted (consolidating OC-220 of 29 Jul 2025, effective 1 Oct 2025). Informational; check NPCI's current circulars."}}},{"name":"tds-invoice-total","type":"registry:ui","title":"TDS Invoice Total","description":"Invoice calculator for Indian freelancers: fee + GST − TDS = what reaches the bank, using the rate and section for the invoice date (s.393 of the 2025 Act; was 194J/194C) and yearly thresholds, flagging TDS on GST, the wrong rate and the no-PAN rate, with a note for the client.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/tds-invoice-total/tds-invoice-total.tsx","type":"registry:ui"},{"path":"registry/default/tds-invoice-total/tds.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Freelancer short-paid after TDS on GST","mistakes":["tds deducted on gst","wrong tds rate freelancer","194j tds calculation","invoice amount after tds","20 percent tds without pan"],"aliases":["tds","gst","invoice","freelancer","194j","194c","pan","income tax","net payable"],"flows":["india","checkout"],"fields":["amount","id-number"],"summary":"Works out what an Indian freelancer's invoice actually lands as after GST and TDS, and flags TDS taken on GST or at the wrong rate.","catches":["TDS deducted on the GST portion","Wrong rate for the kind of work (professional 10%, technical 2%, contract 1%/2%)","Thresholds (₹50,000 a year for professional/technical; ₹30,000 a sum or ₹1 lakh a year for contracts) and catch-up on crossing","Payers who don't deduct (person paying for personal use, client abroad)","Higher rate when no PAN is shared","Ready-to-send note for the client"],"limits":["Informational; not tax advice","Invoice date is used as a proxy; the Act that applies depends on the earlier of credit or payment","Does not handle lower-deduction certificates, non-resident payees or GST reverse charge"],"a11y":["Every field has a <label>; aria-invalid and descriptions via aria-describedby","Result status is role=status","After a fix, focus moves to the next fix or the fee field","Copy-note button shows a copied state"],"rules":{"verified":"2026-10-17","sources":[{"label":"Income-tax Act, 2025 – Section 393 (TDS)","url":"https://www.incometaxindia.gov.in/w/section-393-5"}],"note":"Confirmed in s.393(1) Table Sl. 6: contracts 1% (individual/HUF) or 2%, thresholds ₹30,000 per sum / ₹1,00,000 aggregate; professional fees 10%, technical services 2%, threshold ₹50,000; s.393(4) Sl. 8–9 personal-purpose exemption. Not re-checked: the 20% no-PAN rate (s.397) and the GST exclusion (CBDT Circular 23/2017). Informational; verify with the official source or a professional."}}},{"name":"link-access-check","type":"registry:ui","title":"Link Access Check","description":"Pre-send check for Google Drive, OneDrive, Dropbox, Notion, Figma and Box links that names which recipients can't open each one, offers fixes (give view access, open to the company, ask the owner), and warns when an anyone-with-the-link doc goes outside your organisation.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/link-access-check/link-access-check.tsx","type":"registry:ui"},{"path":"registry/default/link-access-check/links.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Shared a link they can't open","mistakes":["recipient can't open google drive link","request access email","doc shared publicly by mistake","anyone with link outside org","notion page not shared"],"aliases":["google drive","share link","permissions","request access","notion","figma","dropbox","onedrive","email"],"flows":["messaging"],"fields":["url","email"],"summary":"Before a message is sent, checks each cloud document link against its recipients, so nobody gets a link they can't open and nothing goes public by accident.","server":"Check sharing permissions with each provider's API on the server; never trust the client's view of who can open a document.","catches":["Recipients who can't open a linked document","Anyone-with-the-link documents going to people outside your domains","Links you can't re-share: suggests asking the owner","Links to the same document in different URL forms (?usp=sharing, /edit, /view) are treated as one"],"limits":["Relies on your checkAccess function to ask each storage API; it can't see permissions itself","Only recognises the listed hosts plus extraHosts","Group memberships are only as accurate as your API's answer"],"a11y":["Announcements go to a polite live region","Focus moves to the first fix, then to Send","Loading and retry states have text, with icons aria-hidden"]}},{"name":"reply-all-guard","type":"registry:ui","title":"Reply All Guard","description":"Reply-all check: counts real reach with distribution lists expanded, names external recipients by domain, catches a BCC'd or forwarded-to reader about to reveal themselves and a Reply-To that points elsewhere, with sender-only or internal-only fixes and Undo.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/reply-all-guard/reply-all-guard.tsx","type":"registry:ui"},{"path":"registry/default/reply-all-guard/reply.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Reply all to the whole company","mistakes":["accidental reply all","replied to distribution list","bcc recipient revealed","external people in reply all","reply-to points elsewhere"],"aliases":["reply all","email","distribution list","bcc","cc","outlook","gmail","reply-to"],"flows":["messaging"],"fields":["email"],"summary":"Shows how many people a reply-all really reaches and who is outside the organisation before it goes, with a one-tap reply to the sender only.","catches":["Real reach, with list member counts you supply","External recipients, grouped by domain","You were BCC'd or forwarded the mail, so reply-all would reveal you (not flagged when it came via a list you're on)","A Reply-To that points somewhere other than the sender","Large audiences above largeAt"],"limits":["List sizes and your list memberships come from props; it cannot expand lists itself","BCC detection infers from headers; unusual relays can cause misses or false alarms","Does not send; it changes the recipient set you pass on"],"a11y":["Choice outcome announced in a polite live region","After a fix, focus moves to Undo; after Undo, back to the first fix or returnFocusRef","Fix options are plain buttons with text labels; icons are aria-hidden"]}},{"name":"duplicate-payment-guard","type":"registry:ui","title":"Duplicate Payment Guard","description":"Swallows a double tap (charged once), after a timeout offers Check status before paying again because the first may still land, and for a deliberate repeat says \"You paid Ravi ₹2,400 12 min ago\" and needs a separate second press.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/duplicate-payment-guard/duplicate-payment-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Paid twice after a timeout","mistakes":["double payment","charged twice","payment timeout paid again","double tap pay button","duplicate transaction"],"aliases":["duplicate payment","double charge","idempotency","pay button","upi","payment timeout","check status","razorpay"],"flows":["checkout","india"],"fields":["amount"],"summary":"Tells a double tap, a retry after a timeout and a deliberate repeat payment apart, so the same payee isn't paid twice by accident.","server":"Use idempotency keys and a unique constraint per payment intent on the server; that is what stops a double charge.","catches":["Double taps on the pay button","Retries after a timeout while the first payment may still complete","Repeat payments to the same payee and amount within a window"],"limits":["A disabled or held button reduces accidental repeats; it does not guarantee a payment happens once","Recent payments come from your `payments` prop; payments made elsewhere are unseen","Status checks depend on your onCheckStatus"],"a11y":["The pay button stays focusable (aria-disabled), sets aria-busy while paying and is described by the hint","Escape disarms a second-press state; held Enter repeats are ignored","Focus returns to the button after each phase"]}},{"name":"bulk-send-guard","type":"registry:ui","title":"Bulk Send Guard","description":"Before a blast, states how many it reaches, how many opted out and are excluded, who is in quiet hours in their own time zone (held by default), the cost and any jump over the last send, then needs an explicit confirm, with the count typed when the jump is big.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/bulk-send-guard/bulk-send-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bulk blast sent at 2 AM","mistakes":["bulk sms sent at night","whatsapp broadcast wrong list","email blast to unsubscribed","campaign sent twice","mass message mistake"],"aliases":["bulk sms","whatsapp broadcast","email campaign","newsletter","dnd","opt out","quiet hours","marketing","send confirmation"],"flows":["messaging","danger-zone"],"fields":["phone","email"],"summary":"States who an SMS, WhatsApp or email blast will reach, who is excluded and who is in quiet hours, before an explicit confirm sends it.","server":"Enforce opt-outs, DND registries, quiet hours and send limits in the sending service, with idempotency so one blast can't go out twice.","catches":["Opted-out, STOP and unreachable recipients, counted and left out","Recipients in quiet hours in their own IANA time zone (default 21:00–10:00 for SMS and WhatsApp)","The cost of the send","A send much bigger than the last one, which requires typing the count","Accidental double-click sends, via a separate confirm step"],"limits":["Audience counts, exclusions and time zones come from your props","The default quiet hours follow TRAI's default for promotional SMS in India; other countries and channels have different rules","It does not send, schedule or hold messages; onSend does"],"a11y":["Section labelled by the reach summary; quiet-hours options are a labelled fieldset","Focus moves to the confirm step and back to Review; Escape goes back","Errors use role=\"alert\" and status updates role=\"status\"","Icons are aria-hidden; every number is written in text"],"rules":{"verified":"2026-10-17","sources":[{"label":"TRAI TCCCPR 2018 (Schedule: time bands default OFF)","url":"https://trai.gov.in/sites/default/files/2025-01/RegulationUcc19072018.pdf"}],"note":"Informational; check the official source. TCCCPR 2018 sets time bands 00:00–10:00 and 21:00–24:00 default OFF for promotional communication, matching the 21:00–10:00 default. Later TRAI amendments (2025) were not reviewed for changes to this default."}}},{"name":"pii-post-guard","type":"registry:ui","title":"PII Post Guard","description":"Post composer guard that says who will see the post and finds phone numbers, emails, addresses, Aadhaar, PAN, card numbers and OTPs with real checks (Verhoeff, Luhn, PAN holder type, OTP keywords) rather than loose digit runs, masking one or all the way each format expects.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/pii-post-guard/pii-post-guard.tsx","type":"registry:ui"},{"path":"registry/default/pii-post-guard/pii.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps stop an Aadhaar number being posted in public","mistakes":["posted phone number publicly","aadhaar in review","pan card shared in comment","otp posted publicly","email in public post"],"aliases":["pii","aadhaar","pan","phone number","comment box","review","mask","luhn","verhoeff"],"flows":["messaging","india","forms"],"fields":["id-number","phone","email"],"summary":"Before a public post goes out, finds phone numbers, emails, Aadhaar, PAN, card numbers or OTPs in it and masks them in one tap.","server":"Scan and redact on the server before publishing, and never log the raw text.","catches":["Aadhaar numbers (Verhoeff check)","Card numbers (Luhn check)","PAN numbers, including holder type","Phone numbers, emails and street addresses","OTPs near words like code or OTP"],"limits":["India-focused identifiers; other countries' IDs aren't detected","Addresses are detected heuristically","The post still goes out if the user chooses to; it masks, it doesn't block"],"a11y":["Visible label; textarea is described by the audience line and findings","Announcements go to a polite live region","Focus moves to the next mask button, then back to the field"]}},{"name":"recurring-charge-readback","type":"registry:ui","title":"Recurring Charge Readback","description":"Readback beside a subscribe button: charged today, when the first real charge and any intro price step-up land, and the cancel-by time with zone. Flags trials too short to cancel in time and button text that disagrees with the charge.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/recurring-charge-readback/recurring-charge-readback.tsx","type":"registry:ui"},{"path":"registry/default/recurring-charge-readback/schedule.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Free trial quietly became a paid plan","mistakes":["free trial charged","forgot to cancel trial","intro price increased","cancel by date","subscription surprise charge"],"aliases":["free trial","subscription","recurring billing","cancel anytime","intro price","paywall","pricing","subscribe button"],"flows":["checkout"],"fields":["amount","datetime"],"summary":"Reads back what a subscribe button really commits to: today's charge, first real charge, price step-ups and the cancel-by time.","server":"Charge the amounts and dates your billing system computes, not the client's; send the required pre-debit notices from the server or issuer.","catches":["Amount charged today, including small card checks","Date of the first real charge after a trial","Intro price ending and the step-up amount","Cancel-by time, with time zone, using cancelNoticeHours","Button wording (\"free\", a monthly price on yearly billing) that contradicts the charge","Optional India e-mandate notes (24-hour pre-debit alert, approval above a limit)"],"limits":["Only reads back the plan you describe; it does not charge or schedule anything","Month arithmetic depends on billingTimeZone; pass the zone your biller renews in","The e-mandate notes describe RBI rules as of the verified date; they do not apply outside India"],"a11y":["Renders as a polite live region; give your subscribe button aria-describedby pointing at its id","Warnings use an icon plus text; icons are aria-hidden"],"rules":{"verified":"2026-10-17","sources":[{"label":"RBI – Digital Payments – E-mandate Framework, 2026 (Apr 21, 2026)","url":"https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=13374&Mode=0"}],"note":"Informational. Confirmed: issuer pre-transaction notice at least 24 hours before debit; recurring debits up to ₹15,000 without AFA (₹1,00,000 for insurance premiums, mutual fund subscriptions and credit card bills; the default mandateLimit covers only the general case). Check the official source."}}},{"name":"gst-invoice-check","type":"registry:ui","title":"GST Invoice Check","description":"Compares the supplier GSTIN's state with the place of supply to pick CGST + SGST/UTGST or IGST (SEZ and exports always IGST), checks each rate against the slabs in force on the invoice date, LUT exports, the CGST/SGST split and the totals, with a one-tap fix.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/gst-invoice-check/gst-invoice-check.tsx","type":"registry:ui"},{"path":"registry/default/gst-invoice-check/gst.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/tax-id-input.json"],"meta":{"stops":"Helps catch a GST invoice under the wrong tax head","mistakes":["igst vs cgst sgst wrong","wrong gst rate","place of supply mismatch","gstin state code","gst invoice total wrong"],"aliases":["gst","gstin","igst","cgst","sgst","invoice","place of supply","hsn","tax invoice","lut export"],"flows":["india","checkout","forms"],"fields":["id-number","amount"],"summary":"Checks an Indian GST invoice for the wrong tax head (CGST + SGST vs IGST), a rate not in force on the invoice date, and amounts that don't add up.","catches":["Intra-state vs inter-state head chosen wrongly from GSTIN state and place of supply","UTGST vs SGST for union territories without a legislature","Rate not in the slab set for the invoice date (pre-22 Sep 2025, GST 2.0, from 1 Feb 2026)","Zero-rated LUT exports charged tax","CGST/SGST not split evenly, or line amounts and total that don't add up"],"limits":["Checks that a rate exists in a slab set, not that it is the right rate for the HSN/SAC","Does not validate the GSTIN on the GST portal","Ignores cess, reverse charge and composition cases","Rule tables are baked in; future rate changes need a code update"],"a11y":["Section labelled by its title; tax table uses column headers","Findings list is a polite live region with icons that are decorative","Fix is a real button"],"rules":{"verified":"2026-10-17","sources":[{"label":"Notification 19/2025-Central Tax (Rate), 31 Dec 2025 (28%/14% schedule omitted, tobacco to 40%, biris to 18% from 1 Feb 2026)","url":"https://taxinformation.cbic.gov.in/view-pdf/1010534/ENG/Notifications"},{"label":"CBIC GST goods and services rates","url":"https://cbic-gst.gov.in/gst-goods-services-rates.html"}],"note":"Informational; check the official source. Verified: the 1 Feb 2026 change. The 22 Sep 2025 GST 2.0 slab set (Notification 9/2025-CT(R)) and the UTGST state-code list were not re-fetched this round."}}},{"name":"role-preview","type":"registry:ui","title":"Role Preview","description":"Role picker that shows exactly what the chosen role can and can't do, diffs it against the current role, and suggests the smallest role that keeps the needed abilities when the pick adds dangerous permissions.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/role-preview/role-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Gave a teammate admin by mistake","mistakes":["gave too many permissions","admin role by mistake","what can this role do","over-granted access","rbac role change"],"aliases":["roles","permissions","rbac","team members","invite","admin","access control","member settings"],"flows":["account-settings","danger-zone"],"fields":[],"summary":"Shows what a chosen role can and can't do, the diff from the current role, and a smaller role when the pick adds risky permissions.","server":"Authorize role changes on the server and check permissions per request; this only previews the role.","catches":["Permissions gained and lost versus the current role","Dangerous capabilities (cost money, destroy data, widen access) marked","A smaller role that drops the dangerous extras, with one-tap switch"],"limits":["Only as accurate as the role matrix you pass","Suggestion is mechanical least-privilege; it doesn't know what the person actually needs"],"a11y":["Role select has a visible label","Diff list has an aria-label and renders in a polite live region","Can/Cannot prefixes and \"Can no longer\" are text, not colour alone; icons are aria-hidden"]}},{"name":"visibility-picker","type":"registry:ui","title":"Visibility Picker","description":"Sharing-level picker where narrowing is instant but widening (a riskier level or more people) shows who gains access and needs an explicit confirm, so a double-click or arrow key can't publish; policy-blocked levels say why, and a failed save restores the old level.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/visibility-picker/visibility-picker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Made a private doc public by accident","mistakes":["made it public by mistake","visibility changed accidentally","private to public","oversharing setting","repo made public"],"aliases":["visibility","public","private","sharing","privacy settings","who can see","audience","repo visibility"],"flows":["account-settings","danger-zone"],"fields":[],"summary":"Makes widening who can see something an explicit step that shows who gains access; narrowing stays instant.","server":"Enforce sharing level and admin policy on the server for every access check.","catches":["Widening to a riskier level or a larger audience, held for confirmation","Who gains access, in plain words and counts","Levels blocked by policy, with the reason","Failed saves, reverted with a notice"],"limits":["Audience counts come from your options","The confirm is a UX step; access must be enforced server-side"],"a11y":["Levels are a roving-tabindex radiogroup; arrow keys move focus without applying a widening","Risk level stated as sr-only text; blocked levels use aria-disabled","Widening warning linked via aria-describedby in a polite live region; Escape cancels it"]}},{"name":"recipient-check","type":"registry:ui","title":"Recipient Check","description":"Recipient field that marks external addresses in amber, catches typo'd and lookalike domains with a one-tap fix, blocks duplicates, spots a known name paired with someone else's address, and warns when a message goes to too many people.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/recipient-check/recipient-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Email sent to the wrong address","mistakes":["typo in email domain","sent to wrong recipient","gmial.com typo","external recipient warning","wrong autocomplete contact"],"aliases":["recipients","to field","email","autocomplete","external","typo domain","gmail","compose"],"flows":["messaging","forms"],"fields":["email"],"summary":"Reduces misaddressed email by flagging external recipients, lookalike or typo'd domains, duplicates and names paired with the wrong address.","catches":["Recipients outside your internal domains","Typo'd consumer domains (gmial.com → gmail.com) with a one-tap fix","Duplicate addresses","A known contact's name paired with a different address","More recipients than warnAt, with a one-tap remove-externals"],"limits":["Typo detection uses edit distance against a built-in list and your domains; unusual domains can be missed or wrongly flagged","Does not check that a mailbox exists","Name/address mismatch only works for people in contacts"],"a11y":["Visible <label> tied to the input; aria-invalid when any recipient has an error","Enter, comma, semicolon or Tab commits an address; Backspace on an empty field arms then removes the last chip","Chip issues are spoken through sr-only text; each remove button has an aria-label","Warnings and fixes render in a polite live region; icons are aria-hidden","Undo after a bulk remove"]}},{"name":"secret-paste-guard","type":"registry:ui","title":"Secret Paste Guard","description":"Wraps a text field and scans its value for known key formats (AWS, GitHub, Stripe, Slack, OpenAI, Anthropic, Google, JWTs, private keys) plus long high-entropy strings, offering Redact (keeps the last 4) or Keep.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/secret-paste-guard/secret-paste-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch an API key pasted into a chat","mistakes":["users paste api keys","secret leaked in message","token pasted by mistake","jwt pasted in support chat","private key in form"],"aliases":["api key","token","jwt","secret","redact","support chat","paste","credentials"],"flows":["messaging","devtools"],"fields":["secret"],"summary":"Spots API keys, tokens and private keys as they're pasted or typed into a field and offers one-tap Redact before they're sent.","server":"Scan again server-side before storing or forwarding text, and redact secrets from logs, analytics and error reports.","catches":["Known formats: private key blocks, AWS access keys, GitHub tokens, Stripe live/restricted keys, Slack, OpenAI, Anthropic and Google API keys, JWTs","Long mixed-case-and-digit strings with high Shannon entropy (marked as a guess)","Typed and pasted text alike","URL paths are not mistaken for secrets, but tokens in query strings can be"],"limits":["Pattern and entropy based: new or unusual formats are missed, and random IDs can be flagged","Scans in the browser only; it does not stop a submit, so a user can Keep and send","onDetect receives the matched text, so never log or send those matches as-is"],"a11y":["Warning renders in a polite live region","Redact and Keep are labelled buttons","After either choice, focus returns to the wrapped field"]}},{"name":"external-link","type":"registry:ui","title":"External Link","description":"Confirms off-site links with the real ASCII destination and its registrable domain in bold, and flags punycode, lookalike domains, link text that disagrees with the href, buried brand names, raw IPs and plain http.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/external-link/external-link.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Clicked through to a phishing site","mistakes":["phishing link","lookalike domain","link text different from url","punycode domain","leaving site warning"],"aliases":["external link","phishing","leaving site","punycode","url","link warning","safe browsing","redirect"],"flows":["messaging"],"fields":["url"],"summary":"Confirms off-site links with the real destination and flags lookalike, punycode or mismatched links, so people don't click through to a phishing site.","catches":["Punycode and lookalike domains","Link text that names a different site than the href","Brand names buried in subdomains (paypal.com.example.net)","Raw IP addresses and plain http://"],"limits":["Heuristics only: a well-made phishing domain with no lookalike signals passes","Registrable-domain detection uses a built-in suffix list that may miss newer suffixes","Trusted domains are whatever you pass"],"a11y":["Links that confirm expose aria-haspopup=\"dialog\" and aria-expanded","The confirmation is role=\"dialog\" labelled by its title; Escape closes it and returns focus to the link","Warnings are written in text"]}},{"name":"charge-summary","type":"registry:ui","title":"Charge Summary","description":"Shows today's amount, the first charge and renewal dates, taxes (added or included) and one-time credits upfront, and sums it up in one plain sentence of what you'll pay.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/charge-summary/charge-summary.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Surprise charge at checkout","mistakes":["hidden charges checkout","what will i be charged","renewal date unclear","tax added at end","total not clear"],"aliases":["order summary","checkout","pricing","subscription","renewal","tax","gst","total","billing"],"flows":["checkout"],"fields":["amount","datetime"],"summary":"Shows what will be charged today, when the first and renewal charges happen and what taxes apply, so checkout has no surprise charges.","server":"Charge the amount your billing system computes, not the client's figures.","catches":["A trial that turns into a charge, with the exact date","One-time credits or discounts that won't repeat at renewal","Taxes added on top versus already included","The renewal amount and interval"],"limits":["Displays the lines and tax rates you pass; it doesn't calculate tax rules","Dates use the time zone you pass, or the visitor's"],"a11y":["Wrapped in a region with an aria-label","Line items are a description list (<dl>)","Icons are aria-hidden; everything is stated in text"]}},{"name":"send-time-check","type":"registry:ui","title":"Send Time Check","description":"Send-time check that shows the recipient's local time on a strip of their day, flags night, after-hours, weekend (including Friday–Saturday weekends) and holidays you pass, and offers a one-tap schedule for the start of their next working day.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/send-time-check/send-time-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@date-fns/tz","date-fns","motion"],"meta":{"stops":"Message landed at 2 AM their time","mistakes":["messaged client at night","sent on their weekend","recipient time zone","schedule send","holiday message"],"aliases":["schedule send","time zone","local time","working hours","slack","email","weekend","send later"],"flows":["messaging","scheduling"],"fields":["datetime"],"summary":"Shows the recipient's local time next to Send and offers to schedule for their next working morning when it's night, weekend or a holiday there.","catches":["Night hours in the recipient's zone (default 22:00–07:00)","Outside working hours (default 9–18)","Their weekend, by zone (Friday–Saturday in Riyadh, Saturday in Kathmandu, otherwise Saturday–Sunday)","Holidays you supply","Next working-day start, DST-safe, as a one-tap schedule"],"limits":["Weekend table is built in and coarse (by IANA zone, not by person or employer)","Holidays are not built in; pass them","Needs the recipient's real time zone; guessing it from a country can be wrong"],"a11y":["Status message in a polite live region; the day strip is aria-hidden","Marker glide respects reduced motion (useReducedMotion)","Schedule is a labelled button; icons are aria-hidden"],"rules":{"verified":null,"note":"Weekend-by-zone table (Fri–Sat in much of the Gulf, Sat-only in Nepal, Fri–Sun in Brunei) not re-verified against official sources this round. Check local rules before relying on it."}}},{"name":"fill-before-copy","type":"registry:ui","title":"Fill Before Copy","description":"Turns each placeholder (<YOUR_API_KEY>, {{project_id}}) into an inline field and blocks Copy, or asks once, until all of them are filled.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/fill-before-copy/fill-before-copy.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pasted a command with YOUR_API_KEY in it","mistakes":["copied placeholder api key","curl command with placeholder","forgot to replace your_api_key","docs code snippet placeholders","template variable not filled"],"aliases":["code snippet","copy button","placeholder","docs","curl","api key","template","copy to clipboard"],"flows":["devtools"],"fields":["code","secret"],"summary":"Turns placeholders like <YOUR_API_KEY> in a code sample into fields and holds Copy until they're filled, so broken commands aren't pasted.","catches":["Placeholders left in a command or config snippet","Which placeholder is still empty, by moving focus to it","Custom placeholder syntaxes via `pattern`"],"limits":["Only finds placeholders matching the pattern","Values typed into the fields are in the page; avoid pre-filling real secrets in shared docs"],"a11y":["Each placeholder field has an aria-label from its name, with aria-invalid when asked to fill it","Copy is described by an aria-live=\"polite\" status; blocked copy moves focus to the first empty field","Errors use role=\"alert\""]}},{"name":"photo-location-guard","type":"registry:ui","title":"Photo Location Guard","description":"Pre-upload photo check that reads EXIF and XMP (GPS with its precision, capture time, camera serial, owner name, a hidden uncropped preview) and offers \"Remove location\", which re-encodes the photo upright in the browser and re-checks the result.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/photo-location-guard/exif.ts","type":"registry:ui"},{"path":"registry/default/photo-location-guard/photo-location-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Photo leaked my home location","mistakes":["photo gps location leaked","exif data in upload","remove location from photo","image metadata privacy","camera serial in photo"],"aliases":["exif","gps","photo upload","image metadata","privacy","location","avatar upload","marketplace photos"],"flows":["forms","messaging"],"fields":["file","address"],"summary":"Reads a photo's EXIF and XMP before upload and offers one-tap location removal, so a picture doesn't reveal where and when it was taken.","server":"Strip metadata on the server too before publishing uploads; a client-side strip can be skipped.","catches":["GPS coordinates, with how precise they are","Capture time and time zone","Camera serial number and owner name","Embedded preview that can show the uncropped original","XMP location fields"],"limits":["Reads JPEG metadata; other formats show an unsupported notice","Re-encoding drops all metadata and slightly changes image quality","Can't remove location visible in the picture itself (street signs, landmarks)"],"a11y":["File input has a visible label and buttons to choose or change","Findings are in a polite live region; the details list is labelled","Remove shows aria-busy while working; errors use role=alert"]}},{"name":"currency-hint","type":"registry:ui","title":"Currency Hint","description":"Shows a foreign price with a clearly approximate, dated conversion (\"€49.00 ≈ ₹4,520\"), prices a merchant's dynamic currency conversion against the reference rate with a switch back to local currency, estimates card fees and flags stale rates.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/currency-hint/currency-hint.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Budgeted on the wrong currency abroad","mistakes":["dynamic currency conversion","foreign price in rupees","forex markup card","pay in local currency","exchange rate stale"],"aliases":["currency converter","forex","dcc","exchange rate","inr","usd","international payment","card fees"],"flows":["checkout","india"],"fields":["amount"],"summary":"Shows an approximate, dated conversion of a foreign price and prices a merchant's currency conversion, so budgets abroad use the right number.","catches":["Foreign prices read as home-currency prices","Dynamic currency conversion that costs more than the reference rate","Card foreign-transaction fees and tax on them, when you pass the percentages","Exchange rates older than staleAfterDays"],"limits":["Rates come from your `rates` prop; the result is an estimate, not what the bank will charge","Card fee and fee tax default to 0% unless you pass them"],"a11y":["Payment currency options use labels","Conversion and warnings are aria-live=\"polite\"","Animation respects prefers-reduced-motion"]}},{"name":"mention-guard","type":"registry:ui","title":"Mention Guard","description":"Message box that says how many people @channel, @everyone or a group mention will notify and how many are in other time zones, flags mentioned people who aren't in the conversation, offers fixes, and turns Send into an explicit \"Notify 1,240 people\" confirm.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/mention-guard/mention-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"@channel pinged 1,240 people","mistakes":["accidental @everyone","@channel at night","pinged whole workspace","mentioned person not in channel","mass notification"],"aliases":["@channel","@everyone","@here","mentions","slack","discord","notify","group mention"],"flows":["messaging"],"fields":["text"],"summary":"Says how many people @channel, @everyone or a group mention will notify as you type, and asks for an explicit confirm before a mass ping.","server":"Rate-limit and permission-check broadcast mentions on the server.","catches":["Broadcast mentions over a threshold, with the count","Recipients in other time zones","Mentioned people who aren't in the conversation or can't open it","Double press on Send: the confirm can't be triggered by it","Ignores emails (a@here.com) and code spans"],"limits":["Audience sizes come from your context prop","Doesn't stop sending through other clients or the API","Mentions are parsed with simple rules; custom mention syntax needs adapting"],"a11y":["Visible label; textarea is described by a polite notice region","Escape cancels the armed confirm; IME composition doesn't trigger send","Confirm pulse is skipped under reduced motion","Fixes are buttons; icons are aria-hidden"]}},{"name":"share-link","type":"registry:ui","title":"Share Link","description":"Share-link settings read back as one sentence (\"Anyone with the link can edit until Fri 16 Oct, 5:00 PM\"), with warnings and fixes for public edit access and open-ended public links, a live expiry countdown, a flag when you widen a copied link, and confirm before revoke.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/share-link/share-link.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Anyone with the link could edit","mistakes":["public edit link","share link never expires","anyone with link can edit","revoke shared link","link permission mistake"],"aliases":["share link","anyone with the link","permissions","expiry","revoke","public link","google drive","copy link"],"flows":["messaging","account-settings"],"fields":["url","datetime"],"summary":"Reads every share-link setting back as one sentence and warns about public edit access, links that never expire and widening an already-copied link.","server":"Enforce access level and expiry on every request to the link, and make revoke invalidate the token server-side.","catches":["Anyone-with-the-link edit access, with a one-tap view-only fix","Public links with no expiry, with a suggested expiry","Widening access after the link was already copied","Time left before the link expires","Revoking, confirmed first with Cancel focused"],"limits":["Settings take effect only when your server applies them","Cannot recall copies of the link already shared"],"a11y":["Link field has a <label>; settings are a labelled group of labelled selects","Readback in a polite live region; copy result via role=status","Revoke confirmation focuses Cancel first","Countdown highlight respects reduced motion"]}},{"name":"merge-field-check","type":"registry:ui","title":"Merge Field Check","description":"Template editor for bulk email and SMS that renders the message per recipient and flags unknown fields (with typo suggestions), broken braces and recipients with no value (spaces count as empty), with one-tap fixes and fallbacks, and steps to the recipients who'd get a gap.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/merge-field-check/merge-field-check.tsx","type":"registry:ui"},{"path":"registry/default/merge-field-check/merge-fields.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Email went out as Hi {{first_name}}","mistakes":["hi comma blank name","merge tag not replaced","first name missing in email","template variable broken","mail merge error"],"aliases":["mail merge","merge tags","template","personalization","email campaign","sms template","handlebars","first name"],"flows":["messaging"],"fields":["name","text"],"summary":"Renders a mail-merge template for each real recipient, so nothing goes out as \"Hi ,\" or \"{{frist_name}}\".","catches":["Unknown fields, with typo suggestions (frist_name → first_name)","Broken braces such as {first_name}} or {{first_name,","Recipients with an empty or whitespace-only value","Missing fallbacks: adds {{field | default: \"there\"}}"],"limits":["Understands {{field}} and {{field | default: \"…\"}} only, not full Liquid or Handlebars","Checks the recipients you pass, not your whole list"],"a11y":["Visible label; textarea has aria-invalid for template errors and is described by the issues list","Issues list is a polite live region","Fixes are buttons; focus returns to the textarea"]}},{"name":"forward-guard","type":"registry:ui","title":"Forward Guard","description":"Lists what new recipients will see for the first time (earlier messages, internal notes, attachments, other people's addresses), skips messages they were already on, trims internal notes by default and lets each item be dropped before sending.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/forward-guard/forward-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Forwarded internal notes to a client","mistakes":["forwarded whole thread","internal notes sent to customer","forward revealed emails","old messages in forward","attachment forwarded by mistake"],"aliases":["forward","email thread","internal notes","share thread","attachments","helpdesk","fwd","support ticket"],"flows":["messaging"],"fields":["email"],"summary":"Lists what new recipients will see for the first time when a thread is forwarded, so internal notes and addresses aren't overshared.","catches":["Earlier messages the new recipients haven't seen","Internal notes, trimmed by default","Attachments in the thread","Other people's email addresses, with an option to hide them"],"limits":["Uses the thread and recipient data you pass","Forwarding runs your onForward; the mail system sends what it receives"],"a11y":["Each item has a labelled checkbox (aria-label names it)","The summary is aria-live=\"polite\"","Forward sets aria-busy while sending"]}},{"name":"payee-check","type":"registry:ui","title":"Payee Check","description":"Payee form that asks your backend who holds the account and compares it with the typed name like Confirmation of Payee: match, close match with the real name one tap away, or no match with the name hidden and an explicit pay-anyway, plus first-payee and large-amount callouts.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/payee-check/payee-check.tsx","type":"registry:ui"},{"path":"registry/default/payee-check/payee-match.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Money sent to the wrong account","mistakes":["wrong account number transfer","beneficiary name mismatch","confirmation of payee","new payee first payment","paid wrong person"],"aliases":["payee","beneficiary","confirmation of payee","account number","ifsc","name match","bank transfer","upi id","penny drop"],"flows":["checkout","india"],"fields":["name","id-number"],"summary":"Asks the bank who holds an account and compares that with the name typed, like Confirmation of Payee, so money doesn't go to the wrong account.","server":"Run the name check and payment authorization on the server, and log pay-anyway acknowledgements.","catches":["Typed name that doesn't match the account holder","Close matches and typos (Pirya vs Priya), titles and legal suffixes ignored","First payment to a new payee","Large first payments, with an optional test payment"],"limits":["Needs a lookup backed by your bank or provider's name-check API","Name matching is fuzzy and can't settle every case","Doesn't stop a payer who chooses pay-anyway"],"a11y":["Account, code and name inputs have visible labels","Name input has aria-invalid on no match and is described by a polite result region","Loading state has text; icons are aria-hidden"]}},{"name":"csv-export-guard","type":"registry:ui","title":"CSV Export Guard","description":"Previews how each column opens in a spreadsheet (phone numbers and PIN codes losing leading zeros or +, long IDs becoming 1.23457E+15, \"3-4\" turning into a date, cells starting with = + - @ running as formulas) and offers per-column fixes.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/csv-export-guard/csv-checks.ts","type":"registry:ui"},{"path":"registry/default/csv-export-guard/csv-export-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Excel ate the leading zeros","mistakes":["excel removes leading zeros","phone number lost +91","long id becomes 1.23e+15","csv injection","excel converts to date"],"aliases":["csv export","excel","leading zeros","csv injection","pin code","+91","google sheets","download csv"],"flows":["devtools","india"],"fields":["phone","id-number","number"],"summary":"Previews what each column turns into when the CSV opens in a spreadsheet, so leading zeros, long IDs and dates aren't silently corrupted.","server":"Neutralise formula-leading cells when generating CSV on the server as well, since exports can be produced without this component.","catches":["Leading zeros and + signs dropped from phone numbers and codes","Long numeric IDs turned into scientific notation","Values like \"3-4\" turned into dates","Cells starting with = + - @ that run as formulas (CSV injection)"],"limits":["Predicts typical Excel/Sheets behaviour; locale settings and other apps may differ","Forcing text or prefixing formulas changes the raw CSV, which other importers may not expect","Checks the rows you pass, not a whole export streamed from the server"],"a11y":["Columns are a labelled list; fix toggles use aria-pressed","The \"no problems\" icon has an aria-label; arrows are aria-hidden","Summary is aria-live=\"polite\""]}},{"name":"link-unfurl-check","type":"registry:ui","title":"Link Unfurl Check","description":"Link field that previews the unfurl (title, description, image from your Open Graph fetch) and catches localhost and staging URLs, tracking parameters, redirects (hard when they land on a sign-in page) and missing or undersized images, with fixes.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/link-unfurl-check/link-unfurl-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Posted a localhost link preview","mistakes":["shared localhost link","staging url posted","link preview missing image","og image not showing","utm parameters in link"],"aliases":["link preview","unfurl","open graph","og image","utm","localhost","slack preview","twitter card"],"flows":["messaging","devtools"],"fields":["url"],"summary":"Previews how a link will unfurl before you post it and catches localhost or staging links, tracking parameters and broken preview images.","catches":["localhost and staging URLs, with a swap to publicOrigin","Tracking parameters such as utm_*","Redirects, especially to a sign-in page","Missing or undersized preview images"],"limits":["Needs your server-side fetchPreview; browsers can't fetch most pages cross-origin","Each platform renders previews a little differently and caches them","Doesn't check the page content itself"],"a11y":["Visible label; input is described by the issues list","Issues are a polite live region with text, icons aria-hidden","Preview is a labelled section with aria-busy while loading"]}},{"name":"qr-share","type":"registry:ui","title":"QR Share","description":"QR code generator that reads the exact URL back under the code, re-decodes the matrix it drew to prove nothing was truncated, and warns about localhost, staging, http, invisible characters and dense codes, with one-tap fixes. Dependency-free encoder (byte mode, ECC M).","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/qr-share/qr-encode.ts","type":"registry:ui"},{"path":"registry/default/qr-share/qr-share.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"QR code opens the wrong page","mistakes":["qr code wrong link","qr code localhost","qr truncated url","printed qr broken","dense qr code"],"aliases":["qr code","qr generator","share","url","menu qr","poster","upi qr","print"],"flows":["messaging"],"fields":["url"],"summary":"Reads the exact URL back under a QR code and re-decodes what it drew, so a shared code doesn't open the wrong page.","catches":["localhost or staging URLs, with a swap to publicOrigin","http instead of https","Invisible characters in the URL, marked with their code point","Links long enough to make a dense, hard-to-scan code","Encoder output that doesn't decode back to the URL"],"limits":["Byte mode and error correction M only; no logos or other ECC levels","Self-check proves the matrix, not that printers or screens reproduce it well"],"a11y":["QR SVG is role=img with an aria-label naming the URL","Issues are a polite live region; a decode mismatch uses role=alert","Draw animation is skipped under reduced motion"]}},{"name":"redact-image","type":"registry:ui","title":"Redact Image","description":"Screenshot redactor with solid boxes only (blur and pixelation can be reversed), padded and snapped to whole pixels, placed by drag or keyboard, with suggestions from a pluggable text detector and a flattened, metadata-free PNG export that is re-read to check every box.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/redact-image/redact-image.tsx","type":"registry:ui"},{"path":"registry/default/redact-image/redact.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Redacted screenshot still readable","mistakes":["blur can be reversed","redaction not secure","screenshot leaked info","acropalypse","hide text in image"],"aliases":["redact","screenshot","blur","censor","image editor","privacy","bug report","black box"],"flows":["messaging","forms"],"fields":["file"],"summary":"Helps a redacted screenshot stay redacted: solid boxes only, padded to whole pixels, flattened into a fresh PNG that is re-checked on export.","server":"If the image is uploaded, store only the exported PNG and strip metadata again server-side; never keep the original.","catches":["Blur or pixelation used as redaction (only solid fills are offered)","Box edges that leave half-covered pixels, by padding and snapping outward","Sensitive text your detector finds but no box covers","Metadata and trailing data after the image end (aCropalypse-style), by re-encoding to a fresh PNG","Boxes that are not solid in the exported file, by re-reading it"],"limits":["Only as good as the detector you plug in; it does not run OCR by itself","Cannot catch information outside the boxes you or the detector mark (faces, reflections, context)","The original image stays in memory until you discard it; only the export is cleaned"],"a11y":["Boxes can be moved with arrow keys and removed with Delete or Backspace","Each suggestion and remove button has an aria-label; hints wired with aria-describedby","Scan and export progress announced in polite live regions; export failures use role=alert","Undo and Add-box are regular buttons; icons are aria-hidden"]}},{"name":"sms-segments","type":"registry:ui","title":"SMS Segments","description":"SMS composer that splits text into the parts carriers bill (160/153 GSM-7, 70/67 UCS-2), names the curly quote or emoji that forced UCS-2 (\"this ’ makes it 3 messages instead of 2\"), fixes it in one tap, and reads the total back per recipient count.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/sms-segments/sms-segments.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"One emoji doubled the SMS bill","mistakes":["sms costs double","emoji makes sms ucs-2","sms character limit","curly quote sms","message split into parts"],"aliases":["sms","character count","gsm-7","ucs-2","segments","bulk sms","otp sms","twilio","msg91"],"flows":["messaging","india"],"fields":["text"],"summary":"Shows how many billed SMS parts a text becomes and names the character (curly quote, emoji) that forced Unicode and raised the count.","catches":["Characters outside GSM-7 that force UCS-2 (curly quotes, emoji, look-alikes)","Extension characters (€, [, ~) that use two septets","Part count and cost per recipient count","Never splits a 2-septet character or surrogate pair across parts","One-tap replace or remove of each culprit"],"limits":["Assumes standard concatenation; some carriers or national shift tables differ","Cost uses the price you pass; carriers may bill differently","Replacing characters changes the text the recipient sees"],"a11y":["Visible <label>; meter and warning linked via aria-describedby","Parts are buttons in a labelled group, each with an aria-label to select it","Warning renders in a polite live region; each fix has an aria-label","Highlight animation skipped under reduced motion"],"rules":{"verified":"2026-10-17","sources":[{"label":"ETSI TS 123 038 (3GPP TS 23.038) V17.0.0 – Alphabets and language-specific information","url":"https://www.etsi.org/deliver/etsi_ts/123000_123099/123038/17.00.00_60/ts_123038v170000p.pdf"}],"note":"Confirmed: 160 GSM 7-bit characters in 140 octets and the default alphabet extension table. The 153/67 per-part figures come from the concatenation header in 3GPP TS 23.040, not re-checked this round."}}},{"name":"export-scope","type":"registry:ui","title":"Export Scope","description":"Reads back exactly what the file holds (rows and page, filters, the date range as real dates with a day count, hidden and personal-data columns) with toggles and a one-tap \"Export all\" or \"Mask it\" fix.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/export-scope/export-scope.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Export only had page 1 of 250","mistakes":["export only current page","csv missing rows","exported personal data","export date range wrong","export capped silently"],"aliases":["export","csv","download","data table","filters","date range","pii","excel"],"flows":["devtools"],"fields":["datetime"],"summary":"Reads back exactly what an export file will hold, so it doesn't go out as page 1 of 250, silently capped or full of personal data.","server":"Apply the same scope, row limit and masking when generating the file on the server, and log exports of personal data.","catches":["Exporting only the current page when all rows were meant","Exports silently capped at a row limit","Active filters and the real date range, with a day count","Hidden columns and personal-data columns, with a mask option"],"limits":["Reads back the scope you describe; it doesn't produce the file","Which columns hold personal data is up to your scope definition"],"a11y":["Options are role=\"radiogroup\" segmented controls with roving tabIndex and arrow keys","The readback is written in text"]}},{"name":"invite-check","type":"registry:ui","title":"Invite Check","description":"Pre-send invite check that shows the start in each guest's zone, offers the nearest start that suits everyone, and flags night, weekend or holiday slots, typo'd domains, no video link or location, duplicate guests (+tags, Gmail dots) and resent invites.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/invite-check/invite-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/send-time-check.json"],"meta":{"stops":"Invite lands at 3 AM for guests","mistakes":["meeting invite wrong time zone","invite typo domain","no meet link in invite","duplicate guest","invite on weekend"],"aliases":["calendar invite","meeting","time zone","guests","google meet","zoom link","scheduler","event"],"flows":["scheduling"],"fields":["email","datetime"],"summary":"Shows a meeting's start in every guest's own time zone and flags 3 AM slots, weekends, holidays, typo'd domains and duplicate guests before you send.","catches":["Start time outside a guest's working hours, at night or on their weekend","Guest's holiday, from your holiday source","Company domain typos among guests","No video link and no location","Duplicate guests hidden by +tags or Gmail dots, and an invite already sent"],"limits":["Default working hours (9–18) and weekends are assumptions; pass each guest's real ones","Holidays come only from the source you provide","Needs each guest's IANA zone; it can't guess where they are"],"a11y":["Per-guest start times are a labelled list with text, not only icons","Issues and suggestions are in a polite live region","Every fix (reschedule, remove guest, add link) is a button"]}},{"name":"large-attachment","type":"registry:ui","title":"Large Attachment","description":"Attachment size check against each service's limit, counting email's ~37% Base64 overhead, that marks the fewest files pushing it over and shows the size after sending them as a link or compressing, with an honest estimate for already-compressed files.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/large-attachment/large-attachment.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Email bounced for a big attachment","mistakes":["attachment too large","gmail 25mb limit","email bounced size","whatsapp file limit","send as link instead"],"aliases":["attachments","file size limit","gmail","upload","compress","drive link","base64","email"],"flows":["messaging"],"fields":["file"],"summary":"Checks attachments against each service's real size limit, counting email's Base64 overhead, so a message doesn't bounce for being too big.","catches":["Total over the service limit once Base64 encoding is counted","The fewest (largest-first) files to remove so the rest fits","Size after sending culprits as a link","Compression that won't help already-compressed files"],"limits":["Built-in limits (Gmail 25 MB, Outlook.com 20 MB, Discord free 10 MB) can change and differ for workplace accounts; pass your own","The recipient's server may have a lower limit than the sender's","Compression savings are estimates"],"a11y":["Service picker is a labelled radiogroup with roving tabindex and arrow keys","Status is a polite live region","Attachment list is a labelled list; icons are aria-hidden"]}},{"name":"embed-code","type":"registry:ui","title":"Embed Code","description":"Reads back what visitors will see and what the embed tracks, offers a privacy mode and allowed-site list, and catches drafts, private resources and secrets in the URL (token=, sig=) with a one-tap strip before a deliberate second click copies it.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/embed-code/embed-code.tsx","type":"registry:ui"},{"path":"registry/default/embed-code/embed-snippet.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Embed leaked a private token","mistakes":["iframe embed leaks token","embed tracks visitors","private video embedded","youtube privacy mode","embed code copy"],"aliases":["embed","iframe","embed code","youtube","privacy-enhanced","widget","share","token in url"],"flows":["devtools"],"fields":["url","code","secret"],"summary":"Reads back what an embed will show and track, and strips tokens from its URL, so an embed doesn't leak a private link or secret.","server":"Scope embed tokens to read-only, short-lived access on the server, and enforce allowed origins with frame-ancestors.","catches":["Secrets in the embed URL (token=, sig= and similar)","Draft or private resources about to be embedded","Tracking the embed adds, with a privacy-mode option","Allowed-site entries that aren't valid domains"],"limits":["Exposure and tracking come from the resource description you pass","Stripping a token may break an embed that genuinely needs it"],"a11y":["Labelled options and allowed-domain field with aria-invalid and aria-describedby","Readback rows use icons plus text"]}},{"name":"alt-text-check","type":"registry:ui","title":"Alt Text Check","description":"Checks each image's alt text for missing text, filenames (IMG_2041.jpg, PXL_…), \"image of…\" redundancy, caption copies and excess length, with per-image status, one-tap fixes, a decorative toggle and a reminder when the image has words. Post jumps to the first problem.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/alt-text-check/alt-text-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Unusable alt text gets posted","mistakes":["missing alt text","img_2041.jpg as alt","image of redundancy","accessibility image description","screenshot without alt"],"aliases":["alt text","accessibility","a11y","image upload","screen reader","wcag","post composer","cms"],"flows":["forms","messaging"],"fields":["text","file"],"summary":"Checks each image's alt text before posting, so filenames, \"image of\" and empty alt text don't reach screen-reader users.","catches":["Missing alt text","Filenames, camera names and hashes used as alt text","\"Image of\" / \"picture of\" redundancy","Alt text that repeats the caption, or runs past the length limit","Screenshots and images with text whose alt text may not include that text"],"limits":["It checks wording patterns, not whether the description is accurate","Whether an image contains text comes from your data (OCR or uploader); screenshots are assumed to","Posting with problems is still possible after the first press"],"a11y":["Each alt field has a <label> and aria-describedby pointing at its hint; aria-invalid when there is a problem","Per-image hints and the summary are aria-live=\"polite\"","Post moves focus to the first field with a problem","Status uses text and icons, not colour alone"]}},{"name":"stale-reply","type":"registry:ui","title":"Stale Reply","description":"Warns \"This was 3 weeks ago and 14 newer messages followed\" before you reply, surfaces the newest related messages (and any that already settled it, like \"fixed in 2.4.1\"), and offers to reply to the latest or quote the original.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/stale-reply/stale-reply.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Replied to a thread that moved on","mistakes":["replying to old message","thread already resolved","necro reply","outdated comment reply","reply weeks later"],"aliases":["reply","thread","old message","forum","github issue","comments","slack","quote"],"flows":["messaging"],"fields":["text"],"summary":"Warns before replying to an old message in a thread that has moved on, and shows what was said since.","catches":["Replies to messages older than staleAfter (default 7 days) with at least minNewer newer messages (default 10)","Newer related messages: direct replies, same author or shared keywords","Messages that look like they already resolved it","One-tap reply-to-latest or quote-original"],"limits":["Relatedness is keyword-based and can miss or over-include","\"Resolved\" detection is a guess from wording"],"a11y":["Notice is a region with an aria-label; summary in a polite live region","Newer messages expand with aria-expanded/aria-controls; collapsed content is inert","Dismiss has an aria-label"]}},{"name":"tip-picker","type":"registry:ui","title":"Tip Picker","description":"Tip picker that says whether the percent is on the pre- or post-tax amount (with a switch), shows each preset in money, flags a service charge already on the bill, and holds back accidental huge tips (250 in the % field, 15 as money, 100%) until fixed or confirmed.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/tip-picker/tip-picker.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@number-flow/react","motion"],"meta":{"stops":"Tipped 250% by typo","mistakes":["overtipping","tip on tax amount","service charge already added","tip percentage mistake","huge tip by accident"],"aliases":["tip","gratuity","service charge","checkout","food delivery","restaurant bill","percentage","zomato"],"flows":["checkout"],"fields":["amount","number"],"summary":"Shows tips in money on a stated base and holds back accidental huge tips, like 250 typed in the % field.","catches":["An amount typed in the percent field (250%)","A percent typed in the money field (₹15 for 15%)","Tips at or above highPercent","A service charge already on the bill","Pre-tax vs post-tax base, stated and switchable"],"limits":["Customs differ by country; presets and highPercent are yours to set","Rounds in the given currency; the payment system is the source of truth"],"a11y":["Fieldset with a legend; base switch is a radiogroup","Presets are aria-pressed buttons showing money amounts","Custom input has a <label> and a hint via aria-describedby","Rolling figures respect reduced motion"]}},{"name":"refund-picker","type":"registry:ui","title":"Refund Picker","description":"Refund builder: pick units per item or a capped custom amount, see each restocking and return-shipping deduction, keep non-refundable items visible with the reason, compare each method's net and arrival dates, then read the whole refund back before issuing.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/refund-picker/refund-picker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Refund smaller than the customer expected","mistakes":["refund amount wrong","restocking fee surprise","partial refund","refund to wallet vs card","refund arrival date"],"aliases":["refund","return","partial refund","store credit","restocking fee","order details","admin","returns"],"flows":["checkout"],"fields":["amount","number"],"summary":"Shows the exact refund before it's issued: units per item, every deduction named, non-refundable items explained, and net amount per refund method.","server":"Recompute the refundable amount from order data on the server, cap it, and make the refund request idempotent.","catches":["Units per item, never more than were bought","A custom amount above what the picked items allow","Restocking and return-shipping deductions, each named","Non-refundable items, kept visible with the reason","Net amount and arrival window per refund method","Missing refund method before confirm"],"limits":["Arrival dates come from the ranges you pass; banks may take longer","Policy (fees, eligibility) is yours to supply; it does not know your return rules","Money is computed with JS numbers; round on the server"],"a11y":["Section labelled by its title; each item's stepper is a labelled group with a polite <output>","Custom amount has a <label>, aria-invalid when over the cap, and a polite hint via aria-describedby","Refund methods are a labelled radiogroup of native radios","Confirm sets aria-busy while pending; icons are aria-hidden"]}},{"name":"plan-change-readback","type":"registry:ui","title":"Plan Change Readback","description":"Plan change readback that prorates by day (credit, charge today, next bill and date), says a downgrade credit goes to future bills not the card, compares now vs period end, lists lost features, and shows usage over new limits, acknowledged if destructive.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/plan-change-readback/plan-change-readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Downgrade deleted data over the limit","mistakes":["prorated charge confusion","downgrade lost features","upgrade charge today","plan change data loss","credit not refunded"],"aliases":["upgrade","downgrade","proration","billing","plan","subscription","pricing","usage limits"],"flows":["checkout","account-settings","danger-zone"],"fields":["amount"],"summary":"Before an upgrade or downgrade, reads back today's charge, credits, the next bill and any usage over the new plan's limits, so nobody is surprised by a charge or lost data.","server":"Get the real proration preview from your billing provider and apply limits on the server; require the acknowledgement server-side for destructive downgrades.","catches":["Charge today and credit for unused time, prorated by day","Downgrade credit going to future bills rather than the card","Switching now vs at period end, side by side","Features lost on the new plan","Usage over the new plan's limits, with an acknowledgement when excess is removed"],"limits":["Day-based proration is an estimate; your billing provider's numbers are the real ones","Taxes are not included (prices before tax)","Doesn't change the plan itself; onConfirm does"],"a11y":["Section labelled by its title; timing choice is a labelled radiogroup","Gained and lost features are marked with text as well as icons","The arrow between plans has an accessible label"]}},{"name":"payment-reference","type":"registry:ui","title":"Payment Reference","description":"Reference field that requires the invoice number in the expected format, checks each rail's length and character set (UPI, IMPS, NEFT, Faster Payments), moves a cut-off invoice number to the front, and previews the payee's statement line.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/payment-reference/payment-reference.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Transfer couldn't be matched to the invoice","mistakes":["missing invoice number in transfer","neft remark cut off","payment reference too long","upi remark symbols removed","unmatched payment"],"aliases":["payment reference","remarks","invoice number","neft","upi","imps","narration","bank statement"],"flows":["checkout","india"],"fields":["id-number","text"],"summary":"Checks a bank transfer reference against each rail's length and character limits, so the invoice number reaches the payee intact.","catches":["Missing, mistyped or different invoice number","Reference longer than the rail keeps, with the cut part struck through","Symbols the rail strips","Invoice number moved to the front when it would be cut off","Upper-casing on rails that do it"],"limits":["Rail limits are sensible defaults, not guarantees; banks differ","Doesn't send the payment or reconcile it","Pattern for invoice numbers comes from your pattern prop"],"a11y":["Visible label; input is described by a polite hint region","Rail picker is a labelled radiogroup","Icons are aria-hidden"],"rules":{"verified":null,"note":"Not verified this round: rail limits (UPI ~50 characters, NEFT remark boxes ~30, Faster Payments 18) are defaults from the code. Check your bank's own limits."}}},{"name":"truncation-preview","type":"registry:ui","title":"Truncation Preview","description":"Preview of where LinkedIn, Instagram, X, iOS/Android notifications and Google (by pixel width) cut the text, warning when the key phrase, a link or an @mention X won't notify sits below the fold, with fixes that move that sentence up or remove blank lines.","categories":["Sharing, sending and paying"],"files":[{"path":"registry/default/truncation-preview/truncation-preview.tsx","type":"registry:ui"},{"path":"registry/default/truncation-preview/truncation.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Hook hidden behind \"…more\"","mistakes":["linkedin see more cut","instagram caption truncated","meta description cut off","link hidden below fold","post preview truncation"],"aliases":["linkedin post","instagram caption","tweet","see more","seo title","meta description","push notification","character limit"],"flows":["messaging"],"fields":["text"],"summary":"Shows where social feeds and search results cut a post behind \"…more\", and warns when the key point or a link falls below the fold.","catches":["Key phrase hidden below the fold on a surface","Links hidden behind \"…more\"","Blank lines that each cost a whole preview line","An @mention at the start that X won't notify as intended","Key phrase missing from the text"],"limits":["Fold positions are measured approximations; platforms don't publish them and change them","Pixel widths depend on fonts and devices","Override surfaces for your own targets"],"a11y":["Text and key-point inputs have <label>s","Surfaces are a tablist with arrow-key navigation and a tabpanel","Hidden content is flagged in text, not colour alone","Warnings in a polite live region"]}},{"name":"dockerfile-check","type":"registry:ui","title":"Dockerfile Check","description":"Parses like BuildKit (escape directive, continuations, heredocs, stages) and flags a final image running as root, secrets in ARG or ENV, secret files baked in or copied across stages, COPY . before installs, no .dockerignore, apt-get hygiene, unchecked ADD URLs, curl | sh.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/dockerfile-check/dockerfile-check.tsx","type":"registry:ui"},{"path":"registry/default/dockerfile-check/dockerfile.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Docker image runs as root with secrets","mistakes":["docker runs as root","secret in dockerfile env","slow docker build cache","missing dockerignore","curl pipe sh dockerfile"],"aliases":["dockerfile","docker","buildkit","container","hadolint","multi-stage","devops","image"],"flows":["devtools"],"fields":["code","secret"],"summary":"Reads a Dockerfile the way BuildKit does and flags images that run as root, bake in secrets or rebuild slowly.","server":"Run image scanning and policy checks in CI; this check doesn't see the built image.","catches":["A final stage that runs as root","Secrets in ARG or ENV, and secret files copied in or carried by COPY --from","COPY . before dependency install (slow rebuilds) and a missing .dockerignore","apt-get without -y, --no-install-recommends or list cleanup","ADD from a URL without --checksum, curl | sh and shell-form CMD"],"limits":["Static reading of the file; it doesn't build the image or scan installed packages","Base-image vulnerabilities are out of scope"],"a11y":["Visible <label>; aria-invalid and aria-describedby to a role=\"status\" summary","Jumping to a finding focuses the textarea at that line","After a fix, focus moves to the next fix or back to the field"]}},{"name":"gitignore-check","type":"registry:ui","title":"Gitignore Check","description":"Dependency-free .gitignore checker that applies Git's matching rules to your file list, names the line deciding each file, and flags syntax traps, ineffective ! lines, anchors that miss nested folders, missing .env/key/node_modules entries and already-tracked files.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/gitignore-check/gitignore-check.tsx","type":"registry:ui"},{"path":"registry/default/gitignore-check/gitignore.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"node_modules committed despite .gitignore","mistakes":["gitignore not working",".env still committed","gitignore pattern wrong","git rm cached","node_modules in repo"],"aliases":[".gitignore","git","ignore file",".env","node_modules","git rm --cached","repo","commit"],"flows":["devtools"],"fields":["code","file"],"summary":"Runs Git's real ignore rules against your files so a .gitignore that only looks right doesn't let secrets or node_modules into a commit.","catches":["Inline # comments, trailing tabs, leading spaces, backslashes and ./ that change a pattern","! lines that can't re-include a file under an excluded folder, or are overridden later","Leading / anchors that miss nested folders",".env, private keys or node_modules not ignored","Already-committed files .gitignore won't untrack, with the git rm --cached command"],"limits":["Checks only the one .gitignore and file list you paste; nested .gitignore, .git/info/exclude and global excludes are not read","Reimplements Git's rules in JS; edge cases may differ from your Git version","Does not scan file contents for secrets"],"a11y":["Text areas have labels; the editor has aria-invalid when errors exist and is described by the status","Result summary uses role=status; icons are aria-hidden","Each issue has a button to select its line, and focus returns to a sensible control after a fix"]}},{"name":"workflow-yaml-guard","type":"registry:ui","title":"Workflow YAML Guard","description":"GitHub Actions review: pull_request_target or workflow_run jobs that check out PR code, untrusted ${{ github.event.* }} in run scripts, actions on movable tags, missing or write-all permissions, echoed secrets and fork-reachable self-hosted runners, with one-tap fixes.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/workflow-yaml-guard/workflow-yaml-guard.tsx","type":"registry:ui"},{"path":"registry/default/workflow-yaml-guard/workflow.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch workflows that could leak repo secrets to a pull request","mistakes":["pull_request_target vulnerability","github actions injection","secrets leaked in ci","unpinned action","pwn request"],"aliases":["github actions","workflow yaml","ci/cd","pull_request_target","secrets","permissions","pin sha","self-hosted runner"],"flows":["devtools","danger-zone"],"fields":["code","secret"],"summary":"Flags GitHub Actions workflow patterns that can expose secrets or the repo to pull requests, following GitHub's secure-use guidance.","server":"Also enforce branch protection, required reviews and restricted token permissions in repository settings.","catches":["pull_request_target / workflow_run jobs that check out the PR's code (pwn requests)","Untrusted event fields and head_ref interpolated into run scripts","Actions pinned to tags instead of a full commit SHA (pin via your resolveSha)","Missing or write-all token permissions","Secrets echoed, dumped with toJSON or transformed past log masking","Self-hosted runners reachable from fork PRs in public repos"],"limits":["A lightweight, line-aware YAML reader: anchors, complex flow syntax and reusable workflows may be missed","Pattern-based review; it does not prove a workflow is safe","Composite actions and called workflows are not followed"],"a11y":["Visible <label>; aria-invalid and a role=status issue count","Each issue has a button that selects its line, with an aria-label","After a fix, focus moves to the next fix or back to the textarea"],"rules":{"verified":"2026-10-17","sources":[{"label":"GitHub Docs – Secure use reference (Actions)","url":"https://docs.github.com/en/actions/reference/security/secure-use"}],"note":"Confirmed guidance on using an intermediate environment variable for untrusted input and pinning actions to a full-length commit SHA. GitHub Security Lab's pwn-request articles were not re-fetched this round."}}},{"name":"iam-policy-check","type":"registry:ui","title":"IAM Policy Check","description":"Reads each IAM or S3 bucket policy statement back in plain words (who, what, on what, under which condition) and flags admin and service:* wildcards, Resource \"*\", Principal \"*\" that S3 would call public, NotAction/NotPrincipal traps and wide iam:PassRole, with scoped fixes.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/iam-policy-check/iam-policy-check.tsx","type":"registry:ui"},{"path":"registry/default/iam-policy-check/iam-policy.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch an S3 bucket made public by a policy","mistakes":["s3 bucket public","iam policy too broad","wildcard permissions aws","principal star","passrole all roles"],"aliases":["aws iam","s3 bucket policy","iam policy","least privilege","principal","json policy","cloud security","devops"],"flows":["devtools","danger-zone"],"fields":["code"],"summary":"Reads an AWS IAM or S3 bucket policy back in plain words and flags grants far wider than intended, such as a bucket made public.","server":"Validate policies with IAM Access Analyzer in CI and keep S3 Block Public Access on at the account level.","catches":["Full-admin and service:* action wildcards, and Resource \"*\"","Principal \"*\" that S3 Block Public Access treats as public, including conditions that don't narrow it","Public reads and writes on a bucket","Allow with NotAction, and NotPrincipal traps","iam:PassRole on every role, with an iam:PassedToService fix"],"limits":["Static review of one policy; it can't see SCPs, permission boundaries, session policies or resource policies elsewhere","Doesn't evaluate a request the way the IAM engine does; use IAM Access Analyzer or the policy simulator to confirm","Suggested fixes (your account, used actions) depend on the inputs you pass"],"a11y":["Text area has a label, aria-invalid when errors exist, and is described by the status","Status uses role=status; icons are aria-hidden","Each issue has a labelled button that selects its line; focus returns to a nearby control after a fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"AWS: Blocking public access to your Amazon S3 storage (The meaning of \"public\")","url":"https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html"}],"note":"Informational; check the official source. Confirmed that non-public requires fixed values for principals or aws:SourceIp CIDR sets. The exact CIDR width AWS counts as public and the IAM element reference were not re-checked this round."}}},{"name":"hreflang-check","type":"registry:ui","title":"Hreflang Check","description":"Checks pasted <link> tags or a sitemap for one page or a set, following Google Search Central: a who-links-to-whom grid, missing return links, invalid codes (en-UK, es-419), relative URLs, missing self-reference or x-default, and canonicals to another language, with fixes.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/hreflang-check/hreflang-check.tsx","type":"registry:ui"},{"path":"registry/default/hreflang-check/hreflang.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch hreflang tags Google would ignore","mistakes":["hreflang not working","missing return links","en-uk invalid code","x-default missing","international seo"],"aliases":["hreflang","seo","i18n","sitemap","canonical","multilingual","search console","x-default"],"flows":["devtools"],"fields":["url","code"],"summary":"Checks hreflang tags or a sitemap against Google's rules, so missing return links or bad codes don't get the whole set ignored.","catches":["Missing or near-miss return links between versions","Codes Google doesn't support: en-UK, es-419, region-only codes","Relative or protocol-relative URLs","Missing self-reference or x-default","A canonical pointing at another language's page"],"limits":["Checks only what you paste; it does not crawl live pages or HTTP Link headers","Follows Google's documented rules; other search engines may differ","Can't tell whether page content actually matches the declared language"],"a11y":["Text area has a label, aria-invalid when errors exist, and is described by the hint and status","Status uses role=status; icons are aria-hidden","The link grid is a table with row and column headers and a text description in each cell","Focus returns to a nearby button after a fix"],"rules":{"verified":"2026-10-17","sources":[{"label":"Google Search Central: Tell Google about localized versions of your page","url":"https://developers.google.com/search/docs/specialty/international/localized-versions"}],"note":"Informational; check the official source. Confirmed: return links, self-reference, fully-qualified URLs, ISO 639-1 + ISO 3166-1 alpha-2 (es-419 unsupported, UK not a valid region), optional ISO 15924 script, x-default."}}},{"name":"redirect-rule-input","type":"registry:ui","title":"Redirect Rule Input","description":"Redirect list checker for vercel.json, Netlify _redirects and nginx rewrite/return. Finds loops, chains, shadowed rules, open redirects, wrong status codes, dropped queries and trailing-slash traps, with a GET/POST tester and one-tap fixes.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/redirect-rule-input/redirect-rule-input.tsx","type":"registry:ui"},{"path":"registry/default/redirect-rule-input/redirects.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Redirect loop broke the site","mistakes":["redirect loop","redirect chain seo","open redirect","301 vs 302","query string dropped"],"aliases":["redirects","vercel.json","netlify _redirects","nginx","301","308","seo","rewrite rules"],"flows":["devtools"],"fields":["url","code"],"summary":"Catches redirect rules that loop, chain, shadow each other, open-redirect, drop queries or switch POST to GET, modelled on how each platform matches.","server":"Validate redirect destinations against an allowlist on the server; this checks config text, not live traffic.","catches":["Loops and 2+ hop chains, with the hop path","Rules shadowed by an earlier wildcard","Open redirects whose destination host comes from the request","301/308 on temporary moves and 301/302 turning POST into GET","Dropped query strings, ?query or #hash in the source, missing leading slash, unmatched placeholders","Trailing-slash traps and hops to http"],"limits":["Models each platform from its docs; framework middleware, headers/has conditions and server config elsewhere are not evaluated","nginx support covers rewrite/return and location selection, not every module or include","Open-redirect detection is pattern-based; review any destination built from user input"],"a11y":["Visible <label> on the textarea; aria-invalid when errors exist and status wired via aria-describedby","Issue count is a role=status region","GET/POST tester is a roving-tabindex radiogroup operated with arrow keys","After a fix, focus moves to the next fix button or back to the textarea","Hop outcomes shown with icons and text"],"rules":{"verified":"2026-10-17","sources":[{"label":"Vercel – vercel.json redirects","url":"https://vercel.com/docs/project-configuration/vercel-json#redirects"},{"label":"Netlify – Redirect options","url":"https://docs.netlify.com/manage/routing/redirects/redirect-options/"},{"label":"nginx – ngx_http_rewrite_module","url":"https://nginx.org/en/docs/http/ngx_http_rewrite_module.html"}],"note":"Confirmed on Vercel (permanent defaults to true = 308, source is case-sensitive) and Netlify (default 301, 307 unsupported, query passed for 200/301/302, trailing slash ignored when matching). nginx behaviour not re-checked this round. Platforms change; check their docs."}}},{"name":"set-cookie-check","type":"registry:ui","title":"Set-Cookie Check","description":"Set-Cookie checker that reads back who gets each cookie, over what, cross-site or not and for how long (after the 400-day cap), flagging SameSite=None or Partitioned without Secure, broken __Host-/__Secure- prefixes, wide Domain, missing HttpOnly and size limits.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/set-cookie-check/set-cookie-check.tsx","type":"registry:ui"},{"path":"registry/default/set-cookie-check/set-cookie.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Login cookie silently rejected","mistakes":["cookie not being set","samesite none without secure","cookie not sent cross site","missing httponly","__host prefix"],"aliases":["set-cookie","cookies","samesite","httponly","secure","auth cookie","session cookie","cors"],"flows":["devtools","signup-login"],"fields":["code"],"summary":"Reads back what the browser will really do with each Set-Cookie and flags attributes that get it rejected or exposed.","server":"Set cookie attributes in server code and test them in real browsers; this only lints the header text.","catches":["SameSite=None or Partitioned without Secure","__Host- and __Secure- prefix rule violations","A Domain that shares a session cookie with every subdomain","Missing HttpOnly on session-like cookies","Expires and Max-Age that disagree, and lifetimes past the 400-day cap","Name plus value over 4096 bytes"],"limits":["Follows RFC 6265bis (a draft) and CHIPS; individual browsers may differ","Checks header text, not what your server actually sends","Session-cookie detection uses a name pattern (sessionPattern)"],"a11y":["Visible <label> on the textarea; aria-invalid and a role=status issue count","Each cookie is a section with an aria-label","Fix buttons have aria-labels naming the action and cookie; focus moves to the next fix after one is applied"],"rules":{"verified":"2026-10-17","sources":[{"label":"draft-ietf-httpbis-rfc6265bis (Cookies: HTTP State Management Mechanism)","url":"https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html"}],"note":"Confirmed in the editor's draft: lifetime limit SHOULD NOT exceed 400 days, name+value over 4096 octets is ignored, and the __Host-/__Secure- prefix requirements. CHIPS (Partitioned) not re-checked. It is a draft; check for changes."}}},{"name":"qr-scan-check","type":"registry:ui","title":"QR Scan Check","description":"Post-scan check showing the real domain against what's printed beside the code (with a go-to-printed-site option), flagging lookalike and brand-borrowing domains, short links (via your resolver), app downloads, and for UPI codes the payee, amount and refund scams.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/qr-scan-check/qr-scan-check.tsx","type":"registry:ui"},{"path":"registry/default/qr-scan-check/qr.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Helps catch a fake QR sticker before it opens","mistakes":["qr code scam","quishing","fake parking qr","upi qr refund scam","scan qr to receive money"],"aliases":["qr scanner","scan qr","quishing","upi qr","phishing","short link","parking meter","payment qr"],"flows":["india","checkout"],"fields":["url"],"summary":"Shows where a scanned QR code really goes before anything opens, so a fake sticker over a real code doesn't send you somewhere else.","server":"Check URLs against a safe-browsing service and verify UPI payees on the server before payment.","catches":["Destination that differs from the printed site (sticker over a real code)","Lookalike and brand-borrowing domains","Short links, followed through your resolver","App downloads","UPI codes: payee, pre-filled amount and \"scan to receive a refund\" scams"],"limits":["Doesn't scan the code itself; pass the decoded payload","Brand and shortener lists are defaults; pass trusted and shorteners","Can't tell whether a site you trust has been compromised"],"a11y":["Result is a group labelled by its heading","Verdict is in a polite live region with text, icons aria-hidden","Open and cancel are buttons"]}},{"name":"env-diff","type":"registry:ui","title":"Env Diff","description":"Compares two environments key by key, lists missing and empty keys first, flags production values pointing at localhost, a test key or the staging host, and secrets reused across both, masking every secret behind a short fingerprint.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/env-diff/env-diff.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Prod still pointing at localhost","mistakes":["missing env variable in prod","staging url in production","test key in prod","env drift","compare env files"],"aliases":[".env","environment variables","config","staging","production","vercel env","secrets","deploy"],"flows":["devtools"],"fields":["secret","url"],"summary":"Compares two environments key by key and flags missing keys, localhost in production and reused secrets, masking secrets so the diff is safe to share.","server":"Validate required config at deploy time on the server, and keep secrets in a secrets manager rather than in shared diffs.","catches":["Keys missing or empty in one environment","Production values that point at localhost, a test key or a staging host","Secrets reused across both environments","A copy of the missing keys, ready to fill"],"limits":["Fingerprints are 4-hex-digit FNV hashes: fine for spotting reuse, but they can help confirm guesses of short, weak secrets, so don't share diffs of weak secrets","Which keys are secret is decided by isSecret or name patterns"],"a11y":["The comparison is a labelled list; masked dots are aria-hidden and secrets have a labelled icon","Show-same toggle uses aria-pressed","Copy feedback is aria-live=\"polite\""]}},{"name":"join-preview","type":"registry:ui","title":"Join Preview","description":"Join preview that predicts the row count for each join type, names repeated keys that fan rows out, counts rows dropped for no match or a blank key, and catches keys that never match (number vs text, spaces, case, leading zeros), with fixes that say how many rows they rescue.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/join-preview/join-preview.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"SQL join duplicated every row","mistakes":["join duplicates rows","rows missing after join","left join fan out","join key type mismatch","leading zeros join"],"aliases":["sql join","left join","inner join","merge","pandas merge","vlookup","data pipeline","etl"],"flows":["devtools"],"fields":["id-number"],"summary":"Predicts a join's row count before it runs, so repeated keys that duplicate rows or keys that never match don't go unnoticed.","catches":["Repeated keys that fan rows out (and break an expected m:1)","Rows dropped for having no match or a blank key","Keys that differ only by type: number vs text","Keys that differ by spaces, case or leading zeros","Row count for inner, left, right and outer joins side by side"],"limits":["Runs on the sample rows you pass, in the browser; large tables need a server-side count","Normalisations are opt-in and may merge keys that really are different","Predicts counts; it doesn't run your SQL or know your database's collation and type coercion"],"a11y":["Section is labelled with both table names","Join type is a radiogroup with roving tabindex, arrow keys, Home and End","Counts are in a polite live region; failures use role=alert","Run button shows aria-busy while pending"]}},{"name":"dedupe-key-picker","type":"registry:ui","title":"Dedupe Key Picker","description":"As you pick key columns, previews how many rows merge, names a near-unique column (like a timestamp) that keeps duplicates apart, flags a key so broad one value swallows many rows, keeps blank keys separate and highlights values a merge would discard.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/dedupe-key-picker/dedupe-key-picker.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Dedupe merged the wrong rows","mistakes":["remove duplicates wrong","dedupe merged nothing","duplicate rows","merge contacts mistake","drop duplicates key"],"aliases":["dedupe","remove duplicates","merge records","crm contacts","data cleaning","unique key","pandas","csv import"],"flows":["devtools","danger-zone"],"fields":["email","phone"],"summary":"Previews how many rows a dedupe would merge as you pick key columns, so it doesn't merge nothing or merge too much.","server":"Run the merge in a transaction on the server with a backup or undo path; the preview is computed from the rows you pass.","catches":["Near-unique columns (timestamps, ids) that prevent any merge, with \"leave it out\"","Keys so broad that one value covers a large share of rows (coarseShare)","Blank keys, kept separate instead of merged together","Values a merge would discard, shown in sample collisions"],"limits":["Works on the rows you pass; on a sample, counts are estimates for the full table","Normalisation (case, spaces) is optional and simple"],"a11y":["Column toggles are buttons with aria-pressed and their uniqueness share in text","The preview is aria-live=\"polite\"","Apply sets aria-busy while running; errors use role=\"alert\""]}},{"name":"dual-axis-guard","type":"registry:ui","title":"Dual Axis Guard","description":"Catches zero baselines at different heights and independent ranges that make a +15% line and a +140% line climb together, states each series' real change, and offers one-tap \"index both to 100\" on one scale (your chart library via render prop, or built-in SVG).","categories":["Reading without mistakes"],"files":[{"path":"registry/default/dual-axis-guard/dual-axis-guard.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Dual-axis chart faked a correlation","mistakes":["misleading dual axis chart","two y axes chart","fake correlation chart","index to 100","chart scale misleading"],"aliases":["dual axis","chart","recharts","y axis","dashboard","analytics","line chart","data viz"],"flows":["devtools"],"fields":["number"],"summary":"Shows how much each series in a dual-axis chart really changed and offers to index both to 100, so the chart doesn't imply a correlation that isn't there.","catches":["Zero baselines at different heights on the two axes","Independent ranges that make different growth rates look identical","Each series' real change, stated in the summary"],"limits":["Indexing to 100 needs a positive value at the index point","It flags misleading scales, not whether the series are actually related"],"a11y":["A text summary of each series' change, linked to the chart","The scrubber is a role=\"slider\" with arrow and Page Up/Down keys","Legend swatches are aria-hidden; series are named in text"]}},{"name":"secret-field","type":"registry:ui","title":"Secret Field","description":"Read-only secret display that keeps the readable prefix (sk_live_, ghp_) and last characters, flags Live or Test keys, re-masks on a timer or when the tab is hidden, and makes copying the masked text put the real key on the clipboard.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/secret-field/secret-field.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Masked API key pasted as dots","mistakes":["copied masked api key","api key visible on screen","live vs test key","secret field reveal","sk_live leaked"],"aliases":["api key","secret input","reveal","copy key","stripe key","developer settings","password field","token"],"flows":["devtools","account-settings"],"fields":["secret"],"summary":"Shows an API key masked but recognisable, re-hides it on a timer or when the tab is hidden, and copies the real value instead of dots.","server":"Show full secrets only once at creation if you can, and support rotation and revocation server-side.","catches":["Live versus test keys from the prefix (sk_live_, pk_test_…)","Masked text copied by hand, replaced with the real value on the clipboard","Revealed keys left on screen, re-masked after autoHide (default 10 s)","Tab switches while revealed, re-masked on visibilitychange"],"limits":["The full secret is still in the page's JavaScript and React props; masking is visual","Clipboard contents are outside its control once copied (clipboard managers, sync)","Prefix detection covers common formats only"],"a11y":["Visible <label> on a read-only input; masked state exposed via aria-description \"Hidden\"","Reveal is an aria-pressed toggle with aria-controls and a changing aria-label","Copy button has an aria-label; copy result announced in a polite live region"]}},{"name":"text-diff","type":"registry:ui","title":"Text Diff","description":"Word-level diff (Myers) that calls out changed numbers (30 → 14) and draws invisible changes like doubled spaces, line breaks and zero-width characters, in inline or split view, and stays fast on long documents with small changes.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/text-diff/text-diff.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Approved an edit I didn't see","mistakes":["hidden change in text","zero width characters","number changed in contract","compare two texts","diff checker"],"aliases":["diff","text compare","track changes","review","contract","redline","word diff","diffchecker"],"flows":["devtools","forms"],"fields":["text"],"summary":"Shows a word-level diff that makes changed numbers and invisible whitespace changes visible, so an edit can't slip through unseen.","catches":["Changed numbers, kept whole (1,250.50 is one token) and called out","Whitespace, line-break and zero-width character changes, drawn visibly","Identical texts, said plainly","Counts of words added and removed"],"limits":["Word-level, not semantic: reworded sentences show as remove + add","Very large diffs fall back to a coarser result past the memory budget"],"a11y":["Added and removed runs carry sr-only labels (srAddedLabel/srRemovedLabel)","Invisible characters have a title tooltip","Icons are aria-hidden"]}},{"name":"readback","type":"registry:ui","title":"Readback","description":"Read-out display for license keys, IBANs and Wi-Fi passwords that groups characters, labels every look-alike (zero vs letter O, one vs small L), copies the clean value and offers a large show-to-someone view.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/readback/readback.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Licence key misread, 0 vs O","mistakes":["zero vs o confusion","license key wrong","wifi password misread","iban typo","l vs 1 confusion"],"aliases":["license key","product key","wifi password","iban","recovery code","readback","monospace","confusable characters"],"flows":["forms"],"fields":["code","secret"],"summary":"Groups a licence key, IBAN or Wi-Fi password and labels every look-alike character, so 0 and O or 1 and l aren't misread.","catches":["Zero vs letter O, one vs small L, and other look-alikes","Long values without grouping","Copying spaces or dashes: copies the clean value","Reading it across a room: large view"],"limits":["Display only; it doesn't validate the value","Look-alike list is fixed and Latin-only"],"a11y":["Screen readers get the value spelled out group by group (\"letter O\", \"small L\"); the visual groups are aria-hidden","Large view is a labelled dialog opened by a button with aria-haspopup","Copy result is announced in a polite live region"]}},{"name":"copy-confirm","type":"registry:ui","title":"Copy Confirm","description":"Shows exactly what was copied, refuses to copy an empty value, flags stray whitespace and invisible characters with a one-tap clean copy, and can copy formatted numbers as plain text.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/copy-confirm/copy-confirm.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Pasted the wrong thing from the clipboard","mistakes":["copy button copied nothing","invisible characters in copy","trailing space copied","number copied with commas","what did i copy"],"aliases":["copy to clipboard","copy button","clipboard","whitespace","invisible characters","toast","copied","paste"],"flows":["forms","devtools"],"fields":["text"],"summary":"Shows exactly what was copied and flags stray spaces or hidden characters, so the wrong thing isn't pasted somewhere important.","catches":["Copying an empty value","Leading or trailing whitespace and invisible characters, with a clean copy","Formatted numbers (grouping, currency) copied when plain digits were needed"],"limits":["Can't see what the user pastes afterwards or where","Clipboard access can fail or be blocked by the browser; the failure is shown"],"a11y":["Icon-only button gets an aria-label; it is described by the feedback while open","Feedback is aria-live=\"polite\"; Escape closes it","Icons are aria-hidden; the copied text is shown in a <code> element"]}},{"name":"middle-truncate","type":"registry:ui","title":"Middle Truncate","description":"Text that truncates in the middle to the real rendered width so the distinguishing end (a file extension, the last characters of an ID) stays visible, while copy, title and screen readers always get the full value. Never splits emoji or accents.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/middle-truncate/middle-truncate.tsx","type":"registry:ui"}],"meta":{"stops":"Picked the wrong file by prefix","mistakes":["truncated wallet address","file names cut off","ellipsis hides end","same prefix ids","text-overflow end"],"aliases":["truncate","ellipsis","wallet address","file name","transaction id","hash","text overflow","web3"],"flows":["devtools"],"fields":["id-number","file"],"summary":"Truncates long names and IDs in the middle at the real rendered width, so the end that tells files or wallets apart stays visible.","catches":["Lookalike items that differ only at the end","File extensions cut off","Emoji or accented characters split in half"],"limits":["Measures width in the browser after layout, so server-rendered HTML shows the full value until hydration","Very narrow containers may still show only a few characters"],"a11y":["The full value is in the accessibility tree; the truncated display is aria-hidden","Full value is also in the title when truncated","Copy puts the full value on the clipboard, not the ellipsis"]}},{"name":"precise-number","type":"registry:ui","title":"Precise Number","description":"Compact number display (₹1.2 Cr, $1.2M) that cuts instead of rounding up, puts the exact value in a tooltip that isn't clipped by tables or scroll areas, gives spoken units, offers a lakh/crore toggle that rolls the figure to its new units, and keeps percentages honest.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/precise-number/precise-number.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@floating-ui/react","motion"],"meta":{"stops":"₹1.24 Cr rounded up to ₹1.3 Cr","mistakes":["number rounded up wrongly","compact number format","lakh crore format","1.2m vs exact","tooltip clipped in table"],"aliases":["compact number","lakh","crore","number format","intl","tooltip","dashboard","₹"],"flows":["india","forms"],"fields":["number","amount"],"summary":"Shows big numbers compactly (₹1.2 Cr, $1.2M) without ever rounding up, with the exact value one hover or focus away.","catches":["₹99,99,999 shown as ₹1 Cr: it reads ₹99.9 L instead","Exact value hidden: tooltip on hover or focus","Lakh/crore vs million confusion, with a toggle","Rounded percentages that overstate"],"limits":["Display only; it doesn't parse input","Cutting means the compact figure can be slightly low; the tooltip is the exact value"],"a11y":["Approximate values are focusable and described by the exact-value tooltip","Spoken units are given as text for screen readers","Toggle has an aria-label naming the system it switches to","Roll animation is skipped under reduced motion"]}},{"name":"unambiguous-date","type":"registry:ui","title":"Unambiguous Date","description":"Date display that writes dates as \"3 Oct 2026\" with distance from today, flags numeric inputs that read two ways (DD/MM vs MM/DD) with a one-tap choice, and lets one answer apply to a whole imported column.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/unambiguous-date/unambiguous-date.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"03/04 read as March instead of April","mistakes":["dd mm vs mm dd","date format confusion","csv import date wrong","american date format","ambiguous date"],"aliases":["date format","dd/mm/yyyy","mm/dd/yyyy","date picker","csv import","relative date","dob","locale"],"flows":["forms","india"],"fields":["datetime"],"summary":"Shows numeric dates as \"3 Oct 2026\" and asks when 03/10 could mean two dates, so DD/MM and MM/DD aren't mixed up.","catches":["Numeric dates with two valid readings (03/10/2026)","Impossible dates like 31/02, rejected instead of rolled over","Year-first ISO dates, read unambiguously","One answer applied to every date in a column"],"limits":["Numeric formats only; month names are not parsed here","Two-digit years may be ambiguous"],"a11y":["Readback in a polite live region with a <time dateTime> element","Choice buttons are labelled; focus moves to the chosen reading","Icons are aria-hidden"]}},{"name":"deadline","type":"registry:ui","title":"Deadline","description":"Shows time left plus the exact moment in your zone and the owner's, writes midnight as the end of the day before, and turns amber when close and red when overdue.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/deadline/deadline.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Missed a deadline by a time zone","mistakes":["deadline wrong time zone","midnight deadline confusion","11:59 pm ist","hackathon submission deadline","due date countdown"],"aliases":["deadline","due date","countdown","time zone","submission","hackathon","midnight","ist"],"flows":["scheduling","forms"],"fields":["datetime"],"summary":"Shows time left and the exact moment in both your time zone and the owner's, so a deadline isn't missed to a time-zone slip or a misread midnight.","catches":["Deadlines in another time zone, shown in both zones","Midnight deadlines misread as the following day","Deadlines that are close (urgentWithin) or already overdue"],"limits":["Time left uses the visitor's clock unless you pass `now`","The deadline's zone is whatever you pass in ownerTimeZone"],"a11y":["An overdue deadline is announced with role=\"alert\"","Urgency is stated in words as well as colour","Decorative separators are aria-hidden"]}},{"name":"change-highlight","type":"registry:ui","title":"Change Highlight","description":"Flashes a change once (at most once a second, skipped under reduced motion), keeps a ▲/▼ with good/bad colour and the previous value in a tooltip, and can tell returning visitors what changed since their last visit.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/change-highlight/change-highlight.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Price changed and nobody noticed","mistakes":["missed live price change","value updated silently","stock price flash","what changed since last visit","status changed unnoticed"],"aliases":["live updates","flash","price change","ticker","dashboard","realtime","stock price","crypto"],"flows":["forms"],"fields":["number","amount"],"summary":"Flashes a live price, count or status once when it changes and keeps an up/down marker, so updates aren't missed.","catches":["Silent live updates to numbers and statuses","Whether a move is good or bad, given the direction you mark as good","Changes since the last visit, when storageKey is set"],"limits":["\"Since your last visit\" is stored in this browser's localStorage, so it doesn't follow the user across devices","Screen readers hear changes only when announce is on"],"a11y":["Flashes at most once a second and not at all with prefers-reduced-motion","Direction is given in words as well as ▲/▼ glyphs and colour","The changed value is focusable and described by a role=\"tooltip\" showing the previous value","Optional aria-live=\"polite\" announcement (announce prop); dismiss button has an aria-label"],"rules":{"verified":"2026-10-17","sources":[{"label":"WCAG 2.2 SC 2.3.1 Three Flashes or Below Threshold","url":"https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html"}],"note":"Informational. One flash per second stays under the three-flashes-per-second limit."}}},{"name":"spell-out","type":"registry:ui","title":"Spell Out","description":"Readable display for booking refs, wallet addresses, keys and passwords: grouped characters, look-alikes named under the glyph (0 zero vs O oh, 1/l/I, 5/S), NATO spelling with case when it matters, and Cyrillic look-alikes and invisible characters flagged.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/spell-out/spell-out.tsx","type":"registry:ui"},{"path":"registry/default/spell-out/spell.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Booking ref misread over the phone","mistakes":["pnr misread","spell over phone","nato alphabet","wallet address wrong","cyrillic lookalike"],"aliases":["nato alphabet","spell out","pnr","booking reference","wallet address","confusable","phonetic","support call"],"flows":["forms","india"],"fields":["code","id-number"],"summary":"Shows a code or reference so it can be read aloud and copied correctly: grouped, look-alikes named, NATO spelling and hidden characters flagged.","catches":["Look-alike characters (0/O, 1/l/I, 5/S), named","Non-Latin homoglyphs (Cyrillic, Greek) posing as Latin letters","Invisible characters and leading/trailing spaces","Case that matters, spoken as capital/lowercase","Copying the exact value instead of the grouped display"],"limits":["Look-alike list is a common set, not exhaustive","Secret mode masks visually; the value is still in the page"],"a11y":["View switch uses aria-pressed buttons in a labelled group","Groups are a list; each has its spoken spelling for screen readers","Hidden values announce a hidden label; reveal is an aria-pressed toggle","Copy result and warnings render in polite live regions"]}},{"name":"honest-bar","type":"registry:ui","title":"Honest Bar","description":"Pure-SVG bar chart that, when the axis doesn't start at zero, draws a broken-axis mark, states the distortion (lie factor) with a one-tap Start at 0, tears and labels values past the scale, and reads exact values on hover and keyboard focus.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/honest-bar/honest-bar.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bar chart made 9% look like 3×","mistakes":["misleading bar chart","y axis not starting at zero","truncated axis","chart exaggerates","broken axis"],"aliases":["bar chart","y axis","start at zero","broken axis","svg chart","data viz","dashboard","recharts"],"flows":["devtools"],"fields":["number"],"summary":"A bar chart that admits when its axis doesn't start at zero and says how much that exaggerates, like \"looks 3×, really 1.09×\".","catches":["Truncated value axis that makes small differences look large","Values clipped past the scale: torn and labelled","Exaggeration stated as drawn ratio vs real ratio"],"limits":["One series of bars; not a general charting library","The lie factor compares longest and shortest bars only","Ignores other distortions such as 3D, dual axes or log scales"],"a11y":["Chart is a labelled list; each bar is a focusable listitem with an aria-label holding the exact value","Arrow keys, Home and End move between bars","Status about truncation is in a polite live region","The broken-axis warning has text, not only a visual mark"]}},{"name":"change-readout","type":"registry:ui","title":"Change Readout","description":"Shows a change both ways (₹1,200 → ₹1,500 · +₹300 · +25%), keeps percentage points apart from percent (4% → 6% is +2 pts, +50%), gives no % from a zero base or across zero, divides by |base| for negatives, adds 2.5× when a value at least doubles and marks good or bad.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/change-readout/change-readout.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Percentage points mistaken for percent","mistakes":["percent vs percentage points","growth from zero","percent change wrong","doubled shown as 100%","negative base percent"],"aliases":["percent change","delta","kpi","growth","pts","dashboard","metric card","trend"],"flows":["forms"],"fields":["number","amount"],"summary":"Shows a change both as an amount and a percentage, keeping percentage points apart from percent, so changes aren't misread.","catches":["Percentage points confused with percent","Meaningless percentages from a zero base or a sign change","Misleading signs when the base is negative","\"+150%\" misread as 1.5×, by adding the multiple"],"limits":["Formats and labels only; it doesn't know whether the underlying numbers are right","Good/bad depends on the goodDirection you set"],"a11y":["Arrows and separators are aria-hidden; a text label reads \"to\" between values","Good or bad is stated in words, not only colour","\"pts\" is an <abbr> with an explanatory title"]}},{"name":"size-context","type":"registry:ui","title":"Size Context","description":"File size readback with a relatable comparison, transfer time and the equivalent in the other unit system (including what Windows shows), plus a limit check that flags the grey zone where a \"4 GB\" limit fits only if the service means GiB.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/size-context/size-context.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Upload fails on GB vs GiB","mistakes":["file too large upload","gb vs gib","windows shows smaller size","upload limit exceeded","how long to upload"],"aliases":["file size","gb","gib","upload limit","bytes","storage","transfer time","mb"],"flows":["forms"],"fields":["file","number"],"summary":"Explains a file size with a comparison, transfer time and the GB/GiB equivalent, and checks it against an upload limit.","catches":["Files over the limit","The ambiguous zone where a decimal-GB limit fits only if it means GiB","What Windows will display (1,024-based units labelled GB)","Estimated transfer time at a given speed","A relatable comparison for the size"],"limits":["Transfer time is an estimate from bitsPerSecond","Cannot know which unit a third-party service really uses"],"a11y":["Exact byte count in a title tooltip","Explain toggle uses aria-expanded and aria-controls","Fit status in a polite live region; over-limit uses role=alert","Bar visual is aria-hidden"]}},{"name":"sum-check","type":"registry:ui","title":"Sum Check","description":"Total checker that re-adds rows in exact integer arithmetic, shows the off-by amount, points at rows that would explain it (swapped digits, slipped decimal, flipped sign, debit on the credit side) and says when the gap is only rounding, with a one-tap use-the-sum fix.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/sum-check/sum-check.tsx","type":"registry:ui"}],"dependencies":["lucide-react","@number-flow/react","motion"],"meta":{"stops":"Total doesn't add up","mistakes":["total mismatch","sum is wrong","swapped digits total","rounding error totals","debit credit wrong side"],"aliases":["total","sum","reconciliation","invoice","ledger","spreadsheet","accounting","tally"],"flows":["checkout","forms"],"fields":["amount","number"],"summary":"Re-adds rows in exact arithmetic and points at the likely slip when a total doesn't match, or says it's only rounding.","catches":["Totals that don't equal the sum of rows","Gaps divisible by 9 (transposed digits)","A shifted decimal point, flipped sign or wrong side","A row left out or counted twice","Gaps explained by rounding when decimals are set"],"limits":["Suggests single-row slips only; several errors at once may not be explained","Rounding tolerance depends on the decimals you set"],"a11y":["Status in a polite live region; focus moves to it after a fix","Suspects list has an aria-label; each suspect is a button","Rolling figures respect reduced motion"]}},{"name":"status-chip","type":"registry:ui","title":"Status Chip","description":"Status chip with a distinct icon shape and text per tone, a colour-blind-safe (Okabe-Ito) palette, one shared status map so meanings stay consistent, and a hover/focus tooltip explaining what the status means and what to do next.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/status-chip/status-chip.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Status read wrong by colour-blind users","mistakes":["status color only","colour blind status","red green badge","what does this status mean","inconsistent status colors"],"aliases":["status badge","chip","tag","pill","order status","colour blind","a11y","label"],"flows":["forms"],"fields":[],"summary":"Status chips that never rely on colour alone, with one shared status map and a tooltip explaining what each status means.","catches":["Statuses that differ only by colour (each tone has its own icon)","Inconsistent meanings across screens (one shared map)","Unknown statuses, shown with a fallback meaning","What to do next, explained on hover or focus"],"limits":["Meanings are only as clear as the copy you write","Tooltip content is not reachable on touch without focus"],"a11y":["Chip is a button described by its tooltip (aria-describedby, role=tooltip)","Escape closes the tooltip","Icon shape plus text per tone; danger is the only solid chip","Tooltip motion skipped under reduced motion"]}},{"name":"sort-indicator","type":"registry:ui","title":"Sort Indicator","description":"Sortable header plus a readback that states the sort in plain words (\"Sorted by Due date, oldest first\"), warns when the order comes from a hidden column, empty values or pinned rows, and animates rows to their new place so a re-sort can be followed.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/sort-indicator/sort-indicator.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Misread the table's sort order","mistakes":["table sort unclear","which column is sorted","sorted by hidden column","empty values sort","pinned rows sort"],"aliases":["table sort","sorting","data table","column header","asc desc","tanstack table","ag grid","sort arrow"],"flows":["forms"],"fields":[],"summary":"States a table's sort in plain words and warns when hidden columns, empty values or pinned rows make the order look wrong.","catches":["Sort by a column that isn't on screen, with a reveal action","Empty values, kept last in both directions and counted","Pinned rows that stay first regardless of sort","Direction in words that fit the column (A to Z, oldest first)"],"limits":["Sorting helper uses natural numeric comparison; locale-specific collation may differ","You place aria-sort on your <th> (ariaSort helper provided)"],"a11y":["Header button with a descriptive title; use the ariaSort helper for aria-sort on <th>","Sort readback and warnings render in polite live regions","Row animation (FLIP) is skipped under reduced motion","Reverse and clear are labelled buttons"]}},{"name":"diff-table","type":"registry:ui","title":"Diff Table","description":"Marks cells that actually differ (ignoring case, spacing and 10 GB vs 10GB), offers \"Show only differences\" with a count of hidden identical rows, and shows blank cells as \"Not stated\", never a dash that reads as \"No\".","categories":["Reading without mistakes"],"files":[{"path":"registry/default/diff-table/diff-table.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Picked the wrong plan from a comparison","mistakes":["comparison table confusing","show only differences","dash means no","compare plans","compare products"],"aliases":["comparison table","pricing table","compare plans","product compare","spec sheet","differences","feature matrix","pricing"],"flows":["checkout"],"fields":[],"summary":"Marks the cells that actually differ in a comparison table and shows blanks as \"Not stated\", so people don't pick the wrong plan.","catches":["Real differences hidden among identical rows","Formatting-only differences that aren't real (case, spaces, units spacing)","Blank cells that would be read as \"No\""],"limits":["Normalisation is simple; values that mean the same in different words still show as different","Rows with blank cells stay visible even when showing only differences"],"a11y":["A real <table> with a <caption>","The filter is a role=\"switch\" with aria-checked","Yes/No cells use text plus icons; differing cells say so in text","The hidden-rows note is aria-live=\"polite\""]}},{"name":"amount-in-words","type":"registry:ui","title":"Amount in Words","description":"Names every digit group (2 lakh · 50 thousand), spells the amount cheque-style in lakh/crore or million with paise or cents, can show the other number system alongside, and checks typed words against the figure.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/amount-in-words/amount-in-words.tsx","type":"registry:ui"},{"path":"registry/default/amount-in-words/words.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Extra zero on a transfer","mistakes":["2 lakh vs 20 lakh","amount in words","cheque amount words","rupees in words","lakh crore confusion"],"aliases":["amount in words","cheque","lakh","crore","rupees","number to words","neft","invoice"],"flows":["india","checkout"],"fields":["amount"],"summary":"Spells an amount in words and names each digit group, so ₹2,50,000 isn't written or read as ₹25,00,000.","catches":["An extra or missing zero, by naming each digit group","Typed words that do not match the figure, with a one-tap correction","Confusion between lakh/crore and million/billion, via the other system shown alongside","Mismatches that are a power of ten apart (\"10× more\")"],"limits":["Words are English only, with cheque wording built in for INR, USD, EUR, GBP, AED and SGD","Takes the amount as a JavaScript number in major units, so very large values lose precision","Word matching is a text comparison; unusual spellings may be reported as a mismatch"],"a11y":["The grouped figure has an aria-label with the full amount; the visual group labels are aria-hidden","Words input has a <label>, aria-invalid on mismatch and aria-describedby to its status","Match status is aria-live=\"polite\"; a mismatch uses role=\"alert\"","Copy button has an aria-label that changes to \"Copied\""]}},{"name":"overflow-list","type":"registry:ui","title":"Overflow List","description":"Chip list or avatar stack that collapses to \"+N more\" but always keeps failed items, the signed-in user and pinned items visible, and spells out what the expand button hides (\"+4 more, 1 skipped\") instead of a bare count.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/overflow-list/overflow-list.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Failed item hidden behind \"+3 more\"","mistakes":["+3 more hides error","avatar stack hides me","chip list overflow","show more hides failed","tags overflow"],"aliases":["+n more","avatar group","chips","tags","overflow","facepile","assignees","badges"],"flows":["forms"],"fields":[],"summary":"A \"+3 more\" chip list or avatar stack that always keeps failed items, you and pinned items visible, and says what the overflow hides.","catches":["Failed or error items hidden behind \"+N more\"","The signed-in user folded into the overflow","Bare counts that hide what kind of items are folded","Pinned items: always shown"],"limits":["max is a count, not measured width","Warning items can still fold; only errors are always kept"],"a11y":["Labelled list; each item's text includes its status","Status is shown by icon and text, not colour alone","Avatar images have empty alt with the name in text; initials are aria-hidden"]}},{"name":"honest-rating","type":"registry:ui","title":"Honest Rating","description":"Rating summary showing review count and star breakdown, flagging small samples with a likely range for the true score, calling out love-it-or-hate-it splits, and exposing a disclosed Bayesian score for sorting so a 5.0 from 3 reviews can't outrank a 4.6 from 900.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/honest-rating/honest-rating.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Trusted a 5.0 from three reviews","mistakes":["fake high rating","few reviews high stars","rating count","bayesian rating","polarised reviews"],"aliases":["star rating","reviews","ratings","bayesian average","product page","flipkart","amazon","sort by rating"],"flows":["checkout"],"fields":["number"],"summary":"Shows how many reviews stand behind a star rating and how they split, so a 5.0 from three reviews doesn't look as solid as a 4.6 from 900.","catches":["Ratings backed by very few reviews, with a likely range for the true score","Polarised splits hidden by an average","Sorting by raw average: a Bayesian score (IMDb-style) is offered instead","Optional link to read the low reviews first"],"limits":["Statistics assume reviews are genuine; it does not detect fake or incentivised reviews","The prior and weight you pass change the Bayesian score; defaults may not suit your catalogue","The likely range is an approximation (Dirichlet, 90%), not an exact interval"],"a11y":["Breakdown is a labelled list with text labels for each star level","The likely-range bar is role=img with an aria-label stating the range","Warnings are in a polite live region with text, not only icons","Star filters are real buttons, disabled when a level has no reviews"]}},{"name":"unit-price","type":"registry:ui","title":"Unit Price","description":"Pack comparison on a common unit price (₹ per 100 g, per litre or per piece) that marks the true cheapest and catches bigger packs costing more per unit, multi-buys dearer than singles and packs that shrank at the same price, with a one-tap switch.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/unit-price/unit-price.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Bigger pack costs more per kg","mistakes":["bigger pack costs more","price per kg","shrinkflation","multi buy more expensive","unit price comparison"],"aliases":["unit price","price per 100g","grocery","blinkit","zepto","bigbasket","pack size","shrinkflation","₹ per kg"],"flows":["checkout","india"],"fields":["amount","number"],"summary":"Puts every pack on a common unit price and marks the real cheapest, catching bulk packs and multi-buys that cost more per unit.","catches":["Bigger packs that cost more per unit","Multi-buy offers that cost more than buying singles","Shrinkflation: a smaller pack at the same price","The true cheapest per dimension (weight, volume, count)"],"limits":["Compares only the options and sizes you pass","Ignores quality, expiry and delivery costs"],"a11y":["Options are a roving-tabindex radiogroup with arrow keys","Cheapest and warnings marked with icons and text","Findings in a polite live region"]}},{"name":"abbr-hint","type":"registry:ui","title":"Abbr Hint","description":"Makes each abbreviation a dotted, tappable, keyboard-focusable hint instead of a hover-only <abbr title>, can spell it out on first use, exposes the expansion to screen readers, and flags terms missing from the glossary. The hint is portalled and flips to stay on screen.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/abbr-hint/abbr-hint.tsx","type":"registry:ui"}],"dependencies":["@floating-ui/react","motion"],"meta":{"stops":"Readers guess at jargon like TAT","mistakes":["what does tat mean","abbreviation tooltip","abbr title not on mobile","jargon explanation","acronym glossary"],"aliases":["abbreviation","acronym","glossary","tooltip","abbr","jargon","help text","definition"],"flows":["forms"],"fields":["text"],"summary":"Turns jargon like TAT or SLA into a tappable, focusable hint so readers on touch screens and keyboards can see what it means.","catches":["Abbreviations whose only explanation is a hover-only title attribute","First use of a term in a block of text, which can be spelled out inline","Plurals of known terms (\"SLAs\", \"API's\")","Abbreviations in the text that the glossary does not cover"],"limits":["Matches whole words case-sensitively, so a differently cased spelling is not linked","Only knows the terms you put in the glossary; it does not guess expansions"],"a11y":["Each term is a <button> with aria-expanded, reachable by Tab","The expansion is linked with aria-describedby, so screen readers hear it without opening the hint","Escape closes an open hint","Animation respects prefers-reduced-motion (motion's useReducedMotion)"]}},{"name":"error-explainer","type":"registry:ui","title":"Error Explainer","description":"Says what happened, the likely cause and one next step, waits out Retry-After, warns that an ambiguous timeout on a payment may already have gone through with Check status before Retry, and gives a copyable support reference with technical details tucked away.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/error-explainer/error-explainer.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Paid twice after a payment timeout error","mistakes":["error code meaningless","something went wrong","payment timeout retry","double submit after error","error message help"],"aliases":["error message","error state","500","timeout","retry","support reference","payment failed","error boundary"],"flows":["checkout","forms","devtools"],"fields":["code"],"summary":"Turns a raw error code into what happened, the likely cause and one next step, and warns when a timed-out payment may already have gone through.","server":"Make retried operations idempotent on the server and expose a status endpoint for ambiguous outcomes.","catches":["Raw codes with no explanation, mapped through your catalog","Retrying before Retry-After allows","Retrying a non-idempotent request after an ambiguous timeout (offers Check status first)","Errors that keep failing, with a contact option after maxRetries"],"limits":["Explanations come from the catalog or explanation you pass","Whether a retry is safe depends on the `idempotent` flag you set"],"a11y":["The error is announced with role=\"alert\"","Waiting and status changes are aria-live=\"polite\"","Actions are buttons with visible text; icons are aria-hidden"]}},{"name":"partial-period","type":"registry:ui","title":"Partial Period","description":"Period comparison that compares like for like (yesterday cut at the same minute) by default, warns when the full period is chosen, hatches the bucket still filling, dims the part of yesterday not yet reached, and projects the finish, labelled as an estimate.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/partial-period/partial-period.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"registryDependencies":["https://bollard-ui.vercel.app/r/change-readout.json"],"meta":{"stops":"Today looks like a slump vs yesterday","mistakes":["today vs yesterday unfair","partial day comparison","sales dropped today","incomplete period chart","month to date compare"],"aliases":["analytics","dashboard","today vs yesterday","period comparison","kpi","mtd","chart","projection"],"flows":["devtools"],"fields":["number","datetime"],"summary":"Compares today so far with yesterday at the same minute by default, so a still-filling period doesn't read as a slump.","catches":["Today-so-far compared against all of yesterday","The current bucket shown as if complete: hatched instead","Projection presented as fact: labelled as an estimate","Full-period mode chosen: warning with a one-tap switch back"],"limits":["Projection assumes today follows yesterday's shape","Works with the buckets you pass; it doesn't fetch data","One previous period, not a seasonal baseline"],"a11y":["Mode switch is a labelled radiogroup","Change and warnings are in polite live regions with text direction, not only arrows","The bar chart is aria-hidden; the numbers are given in text"]}},{"name":"gap-aware-line","type":"registry:ui","title":"Gap-Aware Line","description":"Pure-SVG line chart that breaks the line at null readings, shades and labels each gap, keeps a lone reading between gaps as a dot, draws estimated points hollow with dashed segments, and backs every reading with a data table.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/gap-aware-line/gap-aware-line.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Missing readings drawn as zero","mistakes":["missing data shows zero","null values line chart","gap in chart","interpolated data","sensor data missing"],"aliases":["line chart","missing data","null values","time series","gaps","svg chart","monitoring","iot"],"flows":["devtools"],"fields":["number","datetime"],"summary":"A line chart that shows missing readings as gaps instead of drawing them as zero or smoothing over them.","catches":["Missing readings drawn as a drop to zero","Gaps smoothed over into a false trend","A single reading between two gaps disappearing","Estimated or modelled values passed off as measured"],"limits":["Only null means missing; a real 0 is drawn as 0, so your data must distinguish them","Does not detect gaps in timestamps; pass a null for each missing slot","Not a general charting library: one series, no zoom"],"a11y":["Chart is role=img with an aria-label that includes a text summary","Points are reachable by keyboard (Arrow keys, Home, End) and the active point is read out in a polite live region","A toggle (aria-pressed) swaps the chart for a captioned data table; the table stays available to screen readers when hidden","Estimated points differ by shape (hollow, dashed), not only colour"]}},{"name":"fair-rank","type":"registry:ui","title":"Fair Rank","description":"Ranks ties the standard competition way (1, T-2, T-2, 4) at the precision shown so equal-looking scores share a rank, shows movement since last period, and sets aside entries below a minimum sample with the reason and the rank they would have had.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/fair-rank/fair-rank.tsx","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Tied players shown with different ranks","mistakes":["leaderboard tie ranking","same score different rank","ranking ties","min sample leaderboard","rank movement"],"aliases":["leaderboard","ranking","ties","scores","hackathon results","gamification","standings","top players"],"flows":["forms"],"fields":["number"],"summary":"Ranks ties fairly at the precision shown and sets aside entries with too few samples, so leaderboards don't mislead.","catches":["Equal-looking scores given different ranks","Movement since the last period","Entries with too few samples, set aside with the reason"],"limits":["Ties are judged at the displayed precision; finer differences are ignored on purpose","Uses standard competition ranking only"],"a11y":["A real table with a header row","Ties and movement are stated in text (\"tied\", up/down labels), not only colour or arrows","Row animations are skipped with prefers-reduced-motion"]}},{"name":"eta-range","type":"registry:ui","title":"ETA Range","description":"Quotes the range covering 90% of deliveries, with a most likely day only when one stands out (\"Thu 15 – Sat 17 Oct, most likely Fri 16\"), counts business days, holidays and the cutoff in the shipper's zone, previews the post-cutoff range and explains when it widened.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/eta-range/eta-range.tsx","type":"registry:ui"},{"path":"registry/default/eta-range/eta.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Delivery promised on an impossible day","mistakes":["delivery date wrong","eta too precise","delivery after cutoff","holiday delivery delay","estimated delivery range"],"aliases":["delivery estimate","eta","shipping","order tracking","cutoff","business days","checkout","pincode"],"flows":["checkout","scheduling"],"fields":["datetime"],"summary":"Quotes delivery as a realistic date range, naming a likely day only when one stands out, so customers aren't promised an impossible day.","catches":["False precision, by quoting a range instead of one date","Holidays and no-delivery days inside the range","The order cutoff, with the range you get after it","A range that widened since last shown, with the reason"],"limits":["Accuracy depends on the delivery distribution and calendar you pass","The 90% range is a statistical estimate, not a carrier guarantee"],"a11y":["The distribution chart is aria-hidden, with a text version of each day's share","Countdown and changes are aria-live=\"polite\"","aria-busy while the clock hasn't loaded"]}},{"name":"palette-guard","type":"registry:ui","title":"Palette Guard","description":"Chart palette check that simulates protan, deutan and tritan vision (Machado 2009), flags every pair under a CIEDE2000 threshold, and offers the smallest lightness-only fix that clears all clashes, or patterns and direct labels when no colour change can.","categories":["Reading without mistakes"],"files":[{"path":"registry/default/palette-guard/palette-guard.tsx","type":"registry:ui"},{"path":"registry/default/palette-guard/palette-math.ts","type":"registry:ui"}],"dependencies":["lucide-react"],"meta":{"stops":"Chart colours look identical to colour-blind readers","mistakes":["colour blind chart","red green chart","chart colors indistinguishable","deuteranopia palette","accessible chart colors"],"aliases":["color palette","colour blind","chart colors","a11y","deuteranopia","okabe-ito","data viz","contrast"],"flows":["devtools"],"fields":[],"summary":"Simulates colour-blind vision for chart series and flags pairs that become hard to tell apart, with the smallest lightness fix.","catches":["Series pairs that look alike for protan, deutan or tritan vision","Pairs already too close in normal vision","Smallest lightness-only fix that clears every clash","Cases needing patterns or direct labels instead"],"limits":["Simulates full dichromacy (severity 1.0); milder forms are not modelled separately","A colour difference threshold isn't the same as legibility on your background","Doesn't check text contrast"],"a11y":["Series list is labelled; each colour input has an aria-label","Clash status is in a polite live region and marked in text (\", clashes\")","Fix button has an aria-label describing the change"]}}]}
