{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"password-input","type":"registry:ui","title":"PasswordInput","description":"A password field with a show/hide reveal toggle and an optional strength meter that never relies on colour alone","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["forms"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/password-input/PasswordInput.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, MouseEvent, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useField } from \"../../internals/field\";\nimport { mergeIds } from \"../../internals/merge-ids\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { preset } from \"../../internals/motion/transitions\";\nimport { prefersReducedMotion } from \"../../internals/motion/anchored\";\nimport { DURATIONS } from \"../../internals/motion/tokens\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport \"./password-input.css\";\n\n/** Result of scoring a password's strength. */\nexport interface PasswordStrengthResult {\n\t/** 0 = very weak, 4 = strongest. The bars fill left to right by this count. */\n\tscore: 0 | 1 | 2 | 3 | 4;\n\t/** Carries the actual meaning — see the component README on why colour alone never does. */\n\tlabel: string;\n}\n\nexport interface PasswordInputOwnProps {\n\t/**\n\t * Current value. Controlled when supplied: pair it with `onValueChange`,\n\t * the React counterpart of the Svelte source's `bind:value`.\n\t */\n\tvalue?: string;\n\t/** Initial value when uncontrolled (no `value` prop given). */\n\tdefaultValue?: string;\n\t/** Called with the new value on every input event. */\n\tonValueChange?: (value: string) => void;\n\t/** Shown while the field is empty. */\n\tplaceholder?: string;\n\t/** Blocks focus and typing; excluded from form submission. Overridden by a surrounding FormField. */\n\tdisabled?: boolean;\n\t/** Blocks typing but stays focusable and is still submitted, unlike `disabled`. */\n\treadonly?: boolean;\n\t/** Native `required`. Overridden by a surrounding FormField. */\n\trequired?: boolean;\n\t/** Drives the error border and `aria-invalid`. Overridden by a surrounding FormField. */\n\tinvalid?: boolean;\n\t/** Element id. Overridden by a surrounding FormField's own `controlId`. */\n\tid?: string;\n\t/** Native `name`, read on form submission. */\n\tname?: string;\n\t/** Accessible name — for a control with no visible Label next to it. */\n\tlabel?: string;\n\t/** Native `autocomplete` hint. `\"new-password\"` opts a password manager into offering to generate one. */\n\tautocomplete?: \"current-password\" | \"new-password\" | \"off\";\n\t/** Renders the show/hide toggle button. Defaults to `true`. */\n\tshowToggle?: boolean;\n\t/** Renders the strength meter and label once there's a value. Defaults to `false`. */\n\tshowStrength?: boolean;\n\t/** Overrides the built-in heuristic scorer — see the README before trusting the default one. */\n\tstrength?: (value: string) => PasswordStrengthResult;\n\t/** Additional CSS classes, merged onto the root field surface (not the bare `<input>`). */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/**\n * Own props plus every native `<input>` attribute and listener; the rest are\n * spread onto the inner `<input>` (see the component README for the rules).\n */\nexport interface PasswordInputProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"input\">, keyof PasswordInputOwnProps | \"type\">,\n\t\tPasswordInputOwnProps {}\n\n// The reveal toggle swaps one 16px glyph for another in place. A hard cut\n// at that scale reads as a flicker, so the two icons cross-fade over\n// `micro` (80ms) — a beat that only registers as a beat.\n//\n// This is the one bidirectional transition in this pass, and it earns it:\n// a cross-fade needs both layers mounted at once, which an enter-only\n// animation cannot give. It is safe because nothing observes the icon's\n// unmount — `aria-label` and `aria-pressed` live on the <button> and flip\n// synchronously, and both glyphs are `aria-hidden`, so a screen reader\n// never sees the overlap. The two-layer grid stack below is what keeps the\n// overlap free of layout impact.\n//\n// `prefersReducedMotion()` is called from the params FACTORY rather than\n// stored here: `usePresence` reads a factory at the instant a leg starts,\n// so the preference is read when the transition begins, never at\n// construction and never during SSR. `duration: 0` makes the runner skip\n// `element.animate()` outright.\nconst iconFade = preset(\"fade\");\nconst fadeParams = () => ({\n\tduration: prefersReducedMotion() ? 0 : DURATIONS.micro,\n});\n\n/**\n * Length plus character-class variety — nothing more. This is a UX\n * heuristic for nudging a user toward a better password while they type,\n * not a security control: it has no notion of dictionary words, leaked\n * password lists, or keyboard-walk patterns, so a password like\n * \"Password1!\" scores far better here than it should. A real deployment\n * needs a proper strength estimator run server-side — pass `strength` to\n * replace this scorer entirely rather than trusting it for anything that\n * matters.\n */\nfunction defaultStrength(password: string): PasswordStrengthResult {\n\tlet classes = 0;\n\tif (/[a-z]/.test(password)) classes++;\n\tif (/[A-Z]/.test(password)) classes++;\n\tif (/[0-9]/.test(password)) classes++;\n\tif (/[^a-zA-Z0-9]/.test(password)) classes++;\n\n\tconst lengthBonus = password.length >= 12 ? 2 : password.length >= 8 ? 1 : 0;\n\tconst raw = classes + lengthBonus;\n\tconst score = (raw <= 1 ? 0 : raw === 2 ? 1 : raw === 3 ? 2 : raw === 4 ? 3 : 4) as\n\t\t| 0\n\t\t| 1\n\t\t| 2\n\t\t| 3\n\t\t| 4;\n\tconst labels = [\"Very weak\", \"Weak\", \"Fair\", \"Good\", \"Strong\"] as const;\n\treturn { score, label: labels[score] };\n}\n\nconst eyeIcon: ReactNode = (\n\t<svg\n\t\tclassName=\"size-4\"\n\t\tviewBox=\"0 0 24 24\"\n\t\tfill=\"none\"\n\t\tstroke=\"currentColor\"\n\t\tstrokeWidth=\"2\"\n\t\tstrokeLinecap=\"round\"\n\t\tstrokeLinejoin=\"round\"\n\t\taria-hidden=\"true\"\n\t>\n\t\t<path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z\" />\n\t\t<circle cx=\"12\" cy=\"12\" r=\"3\" />\n\t</svg>\n);\n\nconst eyeOffIcon: ReactNode = (\n\t<svg\n\t\tclassName=\"size-4\"\n\t\tviewBox=\"0 0 24 24\"\n\t\tfill=\"none\"\n\t\tstroke=\"currentColor\"\n\t\tstrokeWidth=\"2\"\n\t\tstrokeLinecap=\"round\"\n\t\tstrokeLinejoin=\"round\"\n\t\taria-hidden=\"true\"\n\t>\n\t\t<path d=\"M9.9 4.24A9.1 9.1 0 0 1 12 4c7 0 11 8 11 8a17.6 17.6 0 0 1-2.16 3.19m-3.22 2.62A9.2 9.2 0 0 1 12 20c-7 0-11-8-11-8a17.7 17.7 0 0 1 4.09-5.41\" />\n\t\t<path d=\"M14.12 14.12a3 3 0 1 1-4.24-4.24\" />\n\t\t<line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" />\n\t</svg>\n);\n\ninterface CapturedSelection {\n\tstart: number | null;\n\tend: number | null;\n\tdirection: \"forward\" | \"backward\" | \"none\" | undefined;\n}\n\n/**\n * A password field with a reveal toggle and an optional strength meter.\n *\n * The element reference arrives through the ref channel (the Svelte source\n * declares `ref = $bindable(null)`), pointing at the native `<input>`.\n */\nexport const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(\n\t(\n\t\t{\n\t\t\tvalue: valueProp,\n\t\t\tdefaultValue = \"\",\n\t\t\tonValueChange,\n\t\t\tplaceholder,\n\t\t\tdisabled = false,\n\t\t\treadonly = false,\n\t\t\trequired = false,\n\t\t\tinvalid = false,\n\t\t\tid,\n\t\t\tname,\n\t\t\tlabel,\n\t\t\tautocomplete,\n\t\t\tshowToggle = true,\n\t\t\tshowStrength = false,\n\t\t\tstrength,\n\t\t\tclassName,\n\t\t\tsound = false,\n\t\t\tautoComplete: autoCompleteAttr,\n\t\t\tonChange,\n\t\t\t\"aria-describedby\": consumerDescribedBy,\n\t\t\t\"aria-label\": consumerAriaLabel,\n\t\t\t\"aria-invalid\": consumerAriaInvalid,\n\t\t\treadOnly: readOnlyAttr,\n\t\t\t// An `<input>` is a void element: a stray `children` must not reach it.\n\t\t\tchildren: _children,\n\t\t\t...rest\n\t\t},\n\t\tforwardedRef\n\t) => {\n\t\t// The Svelte side's `value = $bindable(\"\")` becomes the standard\n\t\t// controlled/uncontrolled split: uncontrolled by default, controlled\n\t\t// the moment a `value` prop is passed.\n\t\tconst isControlled = valueProp !== undefined;\n\t\tconst [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n\t\tconst value = isControlled ? valueProp : uncontrolledValue;\n\n\t\t// Undefined outside a FormField — every fallback below then uses this\n\t\t// component's own props instead of the context, so the control works\n\t\t// standalone exactly as it does wrapped.\n\t\tconst field = useField();\n\n\t\tconst effectiveId = field?.controlId ?? id;\n\t\tconst effectiveDisabled = field?.disabled ?? disabled;\n\t\tconst effectiveRequired = field?.required ?? required;\n\t\tconst effectiveInvalid = field?.invalid ?? invalid;\n\n\t\tconst [revealed, setRevealed] = useState(false);\n\t\tconst type = revealed ? \"text\" : \"password\";\n\n\t\tconst uid = useFancyId();\n\t\tconst strengthId = `${uid}-strength`;\n\n\t\tconst inputRef = useRef<HTMLInputElement | null>(null);\n\t\tconst composedRef = useComposedRefs(forwardedRef, inputRef);\n\n\t\tconst playCue = useSoundCue(sound);\n\n\t\t// One clock per icon layer, both fed the same bidirectional fade —\n\t\t// during a toggle both layers are mounted at once and both animate,\n\t\t// which is exactly what the Svelte `transition:` pair does.\n\t\tconst revealedIcon = usePresence(revealed);\n\t\tconst hiddenIcon = usePresence(!revealed);\n\n\t\tconst scoreFn = strength ?? defaultStrength;\n\t\t// No result at all while the field is empty — an unfilled meter under an\n\t\t// empty field would be noise, not a signal, and there is nothing yet to\n\t\t// describe.\n\t\tconst result = showStrength && value ? scoreFn(value) : undefined;\n\n\t\t// The strength label rides along on aria-describedby instead of a live\n\t\t// region. A screen reader announces a field's description once, when\n\t\t// focus lands on it — wiring the label into the description reports the\n\t\t// current strength on focus without re-announcing it on every keystroke\n\t\t// the way a polite live region would while the user is still typing. The\n\t\t// visible label span below *is* the description target, so there is no\n\t\t// separate hidden node to keep in sync with it. React also only touches\n\t\t// that span's text when the string actually changes, so a screen reader\n\t\t// that does treat aria-describedby text as live content still would not\n\t\t// hear it churn on every character.\n\t\tconst describedBy = mergeIds(\n\t\t\tfield?.describedBy,\n\t\t\tconsumerDescribedBy,\n\t\t\tresult ? strengthId : undefined\n\t\t);\n\n\t\tconst tierClass = !result\n\t\t\t? \"\"\n\t\t\t: result.score <= 1\n\t\t\t\t? \"bg-destructive\"\n\t\t\t\t: result.score === 2\n\t\t\t\t\t? \"bg-muted-foreground\"\n\t\t\t\t\t: \"ft-password-strength-bar--strong\";\n\n\t\tconst wrapperClasses = cn(\n\t\t\t\"ft-password-input flex w-full items-center gap-2 rounded-[8px] border border-input bg-background px-[12px] py-[9px] text-[13px] transition-colors\",\n\t\t\teffectiveDisabled && \"cursor-not-allowed opacity-50\",\n\t\t\teffectiveInvalid && \"border-destructive/50\",\n\t\t\tclassName\n\t\t);\n\n\t\t// The single place `value` changes. A native `disabled` input never fires\n\t\t// `input` from real typing, but a synthetic dispatch walks straight past\n\t\t// that guard the same way a synthetic click does on a button, so the\n\t\t// early return is repeated here rather than trusted to the attribute\n\t\t// alone. Putting the DOM's value back keeps a disabled control's visible\n\t\t// text from drifting away from the app's own model.\n\t\tfunction handleInput(event: ChangeEvent<HTMLInputElement>) {\n\t\t\tif (effectiveDisabled) {\n\t\t\t\tevent.currentTarget.value = value;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst next = event.currentTarget.value;\n\t\t\tif (!isControlled) setUncontrolledValue(next);\n\t\t\tonValueChange?.(next);\n\t\t\tonChange?.(event);\n\t\t}\n\n\t\t// Swapping `type` between \"password\" and \"text\" resets selection in some\n\t\t// browsers even though it's the same element and the same characters —\n\t\t// the caret (or a real selection) is captured here and restored once the\n\t\t// new `type` has actually reached the DOM. The layout effect below is\n\t\t// what makes \"actually reached the DOM\" true: the `type` attribute is\n\t\t// driven by the `revealed` state, and that write is batched like any\n\t\t// other state write — reading `el.selectionStart` right after flipping\n\t\t// `revealed` would still see the old `type`.\n\t\tconst pendingSelection = useRef<CapturedSelection | null>(null);\n\n\t\tfunction toggleReveal() {\n\t\t\tif (effectiveDisabled) return;\n\t\t\tconst el = inputRef.current;\n\t\t\tpendingSelection.current = {\n\t\t\t\tstart: el?.selectionStart ?? null,\n\t\t\t\tend: el?.selectionEnd ?? null,\n\t\t\t\tdirection: el?.selectionDirection ?? undefined,\n\t\t\t};\n\t\t\t// The cue is named after the state the flip lands ON, not the one it\n\t\t\t// leaves: revealing plays `toggle-on`, hiding plays `toggle-off`. It\n\t\t\t// fires here, in the synchronous part of the click handler, rather\n\t\t\t// than from the selection-restore effect below — a cue that lands\n\t\t\t// after the gesture has ended is outside the user activation an\n\t\t\t// unlocked audio context is granted on.\n\t\t\tconst next = !revealed;\n\t\t\tsetRevealed(next);\n\t\t\tplayCue(next ? \"toggle-on\" : \"toggle-off\");\n\t\t}\n\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tconst captured = pendingSelection.current;\n\t\t\tpendingSelection.current = null;\n\t\t\tconst el = inputRef.current;\n\t\t\tif (el && captured && captured.start !== null && captured.end !== null) {\n\t\t\t\tel.setSelectionRange(captured.start, captured.end, captured.direction ?? undefined);\n\t\t\t}\n\t\t}, [revealed]);\n\n\t\t// A mouse click on the toggle would otherwise move focus onto the button\n\t\t// before `toggleReveal` runs its capture step — this keeps focus (and the\n\t\t// visible caret) on the input throughout a mouse-driven toggle. Keyboard\n\t\t// activation still moves focus to the button as normal; the selection is\n\t\t// still restored on the input either way, so it's correct the moment\n\t\t// focus returns there.\n\t\tfunction preventFocusSteal(event: MouseEvent<HTMLButtonElement>) {\n\t\t\tevent.preventDefault();\n\t\t}\n\n\t\treturn (\n\t\t\t<div className=\"ft-password-input-wrapper flex w-full flex-col gap-1.5\">\n\t\t\t\t<div className={wrapperClasses}>\n\t\t\t\t\t<input\n\t\t\t\t\t\t{...rest}\n\t\t\t\t\t\tref={composedRef}\n\t\t\t\t\t\ttype={type}\n\t\t\t\t\t\tplaceholder={placeholder}\n\t\t\t\t\t\tname={name}\n\t\t\t\t\t\tautoComplete={autocomplete ?? autoCompleteAttr ?? \"current-password\"}\n\t\t\t\t\t\tid={effectiveId}\n\t\t\t\t\t\tvalue={value}\n\t\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t\t\treadOnly={readonly || readOnlyAttr}\n\t\t\t\t\t\trequired={effectiveRequired}\n\t\t\t\t\t\taria-invalid={effectiveInvalid ? \"true\" : consumerAriaInvalid}\n\t\t\t\t\t\taria-describedby={describedBy}\n\t\t\t\t\t\taria-label={label ?? consumerAriaLabel}\n\t\t\t\t\t\tclassName=\"ft-password-input-field text-foreground placeholder:text-muted-foreground min-w-0 flex-1 border-0 bg-transparent p-0 outline-none disabled:cursor-not-allowed\"\n\t\t\t\t\t\tonChange={handleInput}\n\t\t\t\t\t/>\n\t\t\t\t\t{showToggle && (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"ft-password-input-toggle text-muted-foreground hover:text-foreground flex shrink-0 items-center justify-center disabled:cursor-not-allowed disabled:opacity-50\"\n\t\t\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t\t\t\taria-pressed={revealed}\n\t\t\t\t\t\t\taria-label={revealed ? \"Hide password\" : \"Show password\"}\n\t\t\t\t\t\t\tonMouseDown={preventFocusSteal}\n\t\t\t\t\t\t\tonClick={toggleReveal}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"ft-password-input-eye\">\n\t\t\t\t\t\t\t\t{revealedIcon.mounted && (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-password-input-eye-layer\"\n\t\t\t\t\t\t\t\t\t\tref={revealedIcon.register(iconFade, fadeParams)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{eyeOffIcon}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t{hiddenIcon.mounted && (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"ft-password-input-eye-layer\"\n\t\t\t\t\t\t\t\t\t\tref={hiddenIcon.register(iconFade, fadeParams)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{eyeIcon}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t{result && (\n\t\t\t\t\t<div className=\"ft-password-strength flex flex-col gap-1\">\n\t\t\t\t\t\t{/* `ft-password-strength-bar` is a styling hook, not a state class:\n\t\t\t\t\t\t    every segment carries it at every tier, so the colour transition\n\t\t\t\t\t\t    in the stylesheet has one selector to attach to instead of four. */}\n\t\t\t\t\t\t<div className=\"flex gap-1\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"ft-password-strength-bar h-1 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\t\tresult.score >= 1 ? tierClass : \"bg-border\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"ft-password-strength-bar h-1 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\t\tresult.score >= 2 ? tierClass : \"bg-border\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"ft-password-strength-bar h-1 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\t\tresult.score >= 3 ? tierClass : \"bg-border\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"ft-password-strength-bar h-1 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\t\tresult.score >= 4 ? tierClass : \"bg-border\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<span id={strengthId} className=\"text-[11px]\">\n\t\t\t\t\t\t\t{result.label}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n);\n\nPasswordInput.displayName = \"PasswordInput\";\n","type":"registry:ui","target":"@ui/fancy/components/password-input/PasswordInput.tsx"},{"path":"react/src/components/password-input/index.ts","content":"\"use client\";\n\nexport { PasswordInput } from \"./PasswordInput\";\nexport type { PasswordInputProps, PasswordStrengthResult } from \"./PasswordInput\";\n","type":"registry:ui","target":"@ui/fancy/components/password-input/index.ts"},{"path":"react/src/components/password-input/password-input.css","content":"/*\n * The brand accent has no semantic token, so it is declared locally with a\n * light-dark() fallback — the same shape Input and Textarea use for their\n * own accent ring. The ring lives on the wrapper via :focus-within rather\n * than on the bare input, because the input itself has no border or\n * background of its own here — the wrapper is the visual field surface.\n */\n.ft-password-input:focus-within {\n\t--ft-field-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n\tborder-color: var(--ft-field-accent);\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-field-accent) 25%, transparent);\n}\n\n/*\n * `--ft-status-done` is the family's \"operation landed\"/success\n * vocabulary — the same token FormField, CopyButton, ToolCall and others\n * read for their own success state. Reusing it here (fallback hue\n * included, not a fresh literal) means a strong-password cue sitting near\n * any of those on the same page reads as one palette.\n */\n.ft-password-strength-bar--strong {\n\tbackground: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)));\n}\n\n/*\n * Deliberately NOT inside `@media (prefers-reduced-motion: no-preference)`:\n * a colour change is not motion, nothing here travels or scales, and gating\n * it would make a tier change snap for exactly the users who asked for a\n * calmer interface. The meter is the one part of this component that\n * restates itself on every keystroke, so easing the colour is what keeps a\n * password crossing a tier boundary from reading as a flash.\n *\n * `background-color`, not `width` or `scaleX`: the bars are a four-segment\n * meter, and growing them would read as a progress bar filling rather than\n * a tier changing.\n *\n * 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =\n * tokens.EASINGS.inout — a reversible state flip, since a tier can go back\n * down as easily as up.\n */\n.ft-password-strength-bar {\n\ttransition: background-color\n\t\tvar(--ft-password-strength-duration, var(--ft-duration-fast, 150ms))\n\t\tvar(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));\n}\n\n/*\n * The reveal toggle's two icon layers share one grid cell, so both can be\n * mounted during the cross-fade without either one moving the other or\n * changing the button's width mid-swap. `grid-area: 1 / 1` is what makes\n * the overlap free: the wrapper sizes to the larger of the two glyphs (they\n * are the same 16px box) and stays that size whether one layer is mounted\n * or two.\n */\n.ft-password-input-eye {\n\tdisplay: grid;\n}\n\n.ft-password-input-eye-layer {\n\tgrid-area: 1 / 1;\n\tdisplay: inline-flex;\n}\n","type":"registry:ui","target":"@ui/fancy/components/password-input/password-input.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/password-input?framework=react"}}