{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-field", "type": "registry:ui", "title": "Password Field", "description": "iOS 26 liquid-glass password input with animated strength bar, validation checklist, and one-click generation.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/password-field.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/* ── sound ── */\nlet _a: AudioContext, _b: AudioBuffer;\nconst tick = () => {\n if (typeof window === \"undefined\") return;\n if (!_a) {\n _a = new AudioContext();\n _b = _a.createBuffer(1, (_a.sampleRate * 0.003) | 0, _a.sampleRate);\n const d = _b.getChannelData(0);\n for (let i = 0; i < d.length; i++)\n d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n }\n const s = _a.createBufferSource();\n s.buffer = _b;\n const g = _a.createGain();\n g.gain.value = 0.08;\n s.connect(g).connect(_a.destination);\n s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.pf{\n --pf-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n --pf-border:rgba(0,0,0,0.06);\n --pf-shadow:0 0 1px rgba(0,0,0,0.04),0 2px 8px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.8);\n --pf-dim:rgba(0,0,0,0.42);\n --pf-mid:rgba(0,0,0,0.55);\n --pf-hi:rgba(0,0,0,0.88);\n --pf-focus:rgba(0,0,0,0.12);\n --pf-sep:rgba(0,0,0,0.06);\n --pf-btn:rgba(0,0,0,0.03);\n --pf-btn-h:rgba(0,0,0,0.07);\n --pf-btn-border:rgba(0,0,0,0.06);\n --pf-track:rgba(0,0,0,0.06);\n --pf-weak:rgba(239,68,68,0.7);\n --pf-medium:rgba(234,179,8,0.7);\n --pf-strong:rgba(59,130,246,0.7);\n --pf-max:rgba(34,197,94,0.7);\n --pf-ok:rgba(34,197,94,0.8);\n --pf-fail:rgba(0,0,0,0.2)\n}\n.dark .pf,[data-theme=\"dark\"] .pf{\n --pf-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n --pf-border:rgba(255,255,255,0.07);\n --pf-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n --pf-dim:rgba(255,255,255,0.28);\n --pf-mid:rgba(255,255,255,0.5);\n --pf-hi:rgba(255,255,255,0.88);\n --pf-focus:rgba(255,255,255,0.12);\n --pf-sep:rgba(255,255,255,0.06);\n --pf-btn:rgba(255,255,255,0.03);\n --pf-btn-h:rgba(255,255,255,0.07);\n --pf-btn-border:rgba(255,255,255,0.06);\n --pf-track:rgba(255,255,255,0.06);\n --pf-weak:rgba(248,113,113,0.8);\n --pf-medium:rgba(250,204,21,0.8);\n --pf-strong:rgba(96,165,250,0.8);\n --pf-max:rgba(74,222,128,0.8);\n --pf-ok:rgba(74,222,128,0.9);\n --pf-fail:rgba(255,255,255,0.15)\n}`;\n\n/* ── helpers ── */\nconst CHECKS = [\n { label: \"8+ characters\", test: (v: string) => v.length >= 8 },\n { label: \"Uppercase\", test: (v: string) => /[A-Z]/.test(v) },\n { label: \"Number\", test: (v: string) => /\\d/.test(v) },\n {\n label: \"Special char\",\n test: (v: string) => /[!@#$%^&*()_+\\-=[\\]{};':\"\\\\|,.<>/?]/.test(v),\n },\n];\n\nconst STRENGTH_COLORS = [\n \"var(--pf-track)\",\n \"var(--pf-weak)\",\n \"var(--pf-medium)\",\n \"var(--pf-strong)\",\n \"var(--pf-max)\",\n];\n\nconst STRENGTH_LABELS = [\"\", \"Weak\", \"Fair\", \"Strong\", \"Very strong\"];\n\nconst CHARSET =\n \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*\";\n\n/* ── types ── */\nexport interface PasswordFieldProps {\n label?: string;\n placeholder?: string;\n showChecklist?: boolean;\n allowGenerate?: boolean;\n onChange?: (value: string) => void;\n sound?: boolean;\n style?: React.CSSProperties;\n}\n\n/* ── component ── */\nexport default function PasswordField({\n label = \"Password\",\n placeholder = \"Enter password\",\n showChecklist = true,\n allowGenerate = true,\n onChange,\n sound = true,\n style,\n}: PasswordFieldProps) {\n const [value, setValue] = React.useState(\"\");\n const [visible, setVisible] = React.useState(false);\n const [focused, setFocused] = React.useState(false);\n const inputRef = React.useRef(null);\n\n const results = CHECKS.map((c) => c.test(value));\n const passed = results.filter(Boolean).length;\n\n const toggleVisibility = () => {\n setVisible((v) => !v);\n if (sound) tick();\n };\n\n const generate = () => {\n let pw = \"\";\n for (let i = 0; i < 16; i++)\n pw += CHARSET[Math.floor(Math.random() * CHARSET.length)];\n setValue(pw);\n onChange?.(pw);\n if (sound) tick();\n };\n\n return (\n <>\n