{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "glass-form", "type": "registry:ui", "title": "Glass Form", "description": "iOS 26 liquid-glass form system — grouped fields, toggles, segmented controls, and action buttons with backdrop blur.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/glass-form.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } 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 ─────────────────────────────────────────────── */\n/* Light: frosted-white panels, soft outer shadows */\n/* Dark : original white-tint glass, inner glow */\nconst GLASS_CSS = `\n.gf{\n --gf-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n --gf-border:rgba(0,0,0,0.06);\n --gf-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 --gf-sep:rgba(0,0,0,0.06);\n --gf-dim:rgba(0,0,0,0.42);\n --gf-mid:rgba(0,0,0,0.55);\n --gf-hi:rgba(0,0,0,0.88);\n --gf-hover:rgba(0,0,0,0.025);\n --gf-toggle-on:#007AFF;\n --gf-toggle-off:rgba(0,0,0,0.1);\n --gf-seg-bg:rgba(0,0,0,0.04);\n --gf-seg-active:rgba(0,0,0,0.07);\n --gf-knob:#fff;\n --gf-knob-border:rgba(0,0,0,0.08);\n --gf-knob-shadow:0 1px 3px rgba(0,0,0,0.12);\n --gf-ghost:rgba(0,0,0,0.03);\n --gf-ghost-h:rgba(0,0,0,0.06)\n}\n.dark .gf,[data-theme=\"dark\"] .gf{\n --gf-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n --gf-border:rgba(255,255,255,0.07);\n --gf-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n --gf-sep:rgba(255,255,255,0.06);\n --gf-dim:rgba(255,255,255,0.28);\n --gf-mid:rgba(255,255,255,0.5);\n --gf-hi:rgba(255,255,255,0.88);\n --gf-hover:rgba(255,255,255,0.015);\n --gf-toggle-on:#0A84FF;\n --gf-toggle-off:rgba(255,255,255,0.1);\n --gf-seg-bg:rgba(255,255,255,0.04);\n --gf-seg-active:rgba(255,255,255,0.09);\n --gf-knob:#fff;\n --gf-knob-border:rgba(255,255,255,0.06);\n --gf-knob-shadow:0 1px 3px rgba(0,0,0,0.15);\n --gf-ghost:rgba(255,255,255,0.02);\n --gf-ghost-h:rgba(255,255,255,0.05)\n}`;\n\n/* ── palette ───────────────────────────────────────────── */\nconst P = {\n glass: \"var(--gf-glass)\",\n glassBorder: \"var(--gf-border)\",\n glassShadow: \"var(--gf-shadow)\",\n separator: \"var(--gf-sep)\",\n dim: \"var(--gf-dim)\",\n mid: \"var(--gf-mid)\",\n hi: \"var(--gf-hi)\",\n accent: \"hsl(var(--primary))\",\n accentFg: \"hsl(var(--primary-foreground))\",\n toggleOn: \"var(--gf-toggle-on)\",\n toggleOff: \"var(--gf-toggle-off)\",\n segBg: \"var(--gf-seg-bg)\",\n segActive: \"var(--gf-seg-active)\",\n hoverRow: \"var(--gf-hover)\",\n knob: \"var(--gf-knob)\",\n knobBorder: \"var(--gf-knob-border)\",\n knobShadow: \"var(--gf-knob-shadow)\",\n ghost: \"var(--gf-ghost)\",\n ghostHover: \"var(--gf-ghost-h)\",\n};\n\n/* ── context ───────────────────────────────────────────── */\nconst FormCtx = React.createContext({ sound: true });\n\n/* ═══════════════════════════════════════════════════════════\n GlassForm — outer
wrapper\n ═══════════════════════════════════════════════════════════ */\ninterface GlassFormProps {\n children: React.ReactNode;\n onSubmit?: (e: React.FormEvent) => void;\n sound?: boolean;\n style?: React.CSSProperties;\n}\n\nfunction GlassForm({\n children,\n onSubmit,\n sound = true,\n style,\n}: GlassFormProps) {\n return (\n \n \n {\n e.preventDefault();\n if (sound) tick();\n onSubmit?.(e);\n }}\n style={{\n width: \"100%\",\n maxWidth: 380,\n display: \"flex\",\n flexDirection: \"column\",\n gap: 24,\n ...style,\n }}\n >\n {children}\n \n \n );\n}\n\n/* ═══════════════════════════════════════════════════════════\n FormGroup — rounded glass container grouping rows\n ═══════════════════════════════════════════════════════════ */\ninterface FormGroupProps {\n title?: string;\n footer?: string;\n children: React.ReactNode;\n}\n\nfunction FormGroup({ title, footer, children }: FormGroupProps) {\n const items = React.Children.toArray(children).filter(Boolean);\n\n return (\n
\n {title && (\n \n {title}\n
\n )}\n \n {items.map((child, i) => (\n \n {i > 0 && (\n \n )}\n {child}\n \n ))}\n \n {footer && (\n \n {footer}\n \n )}\n \n );\n}\n\n/* ═══════════════════════════════════════════════════════════\n FormField — label + text input row\n ═══════════════════════════════════════════════════════════ */\ninterface FormFieldProps {\n label: string;\n type?: string;\n placeholder?: string;\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n autoFocus?: boolean;\n}\n\nfunction FormField({\n label,\n type = \"text\",\n placeholder,\n value,\n defaultValue,\n onChange,\n disabled,\n autoFocus,\n}: FormFieldProps) {\n const [showPass, setShowPass] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const isPassword = type === \"password\";\n\n return (\n setHover(true)}\n onMouseLeave={() => setHover(false)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n padding: \"0 16px\",\n minHeight: 44,\n opacity: disabled ? 0.35 : 1,\n background: hover && !disabled ? P.hoverRow : \"transparent\",\n transition: \"background 120ms\",\n }}\n >\n \n {label}\n \n onChange?.(e.target.value)}\n disabled={disabled}\n autoFocus={autoFocus}\n style={{\n flex: 1,\n fontSize: 15,\n color: P.hi,\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n textAlign: \"right\",\n padding: \"11px 0\",\n fontFamily: \"inherit\",\n }}\n />\n {isPassword && (\n setShowPass(!showPass)}\n style={{\n background: \"none\",\n border: \"none\",\n color: P.dim,\n cursor: \"pointer\",\n padding: \"0 0 0 8px\",\n fontSize: 12,\n fontFamily: \"inherit\",\n transition: \"color 150ms\",\n }}\n >\n {showPass ? \"Hide\" : \"Show\"}\n \n )}\n \n );\n}\n\n/* ═══════════════════════════════════════════════════════════\n FormToggle — label + iOS-style toggle\n ═══════════════════════════════════════════════════════════ */\ninterface FormToggleProps {\n label: string;\n description?: string;\n checked?: boolean;\n defaultChecked?: boolean;\n onChange?: (checked: boolean) => void;\n}\n\nfunction FormToggle({\n label,\n description,\n checked,\n defaultChecked,\n onChange,\n}: FormToggleProps) {\n const { sound } = React.useContext(FormCtx);\n const [internal, setInternal] = React.useState(defaultChecked ?? false);\n const [hover, setHover] = React.useState(false);\n const active = checked !== undefined ? checked : internal;\n\n const toggle = () => {\n if (sound) tick();\n const next = !active;\n if (onChange) onChange(next);\n else setInternal(next);\n };\n\n return (\n {\n if (e.key === \" \" || e.key === \"Enter\") {\n e.preventDefault();\n toggle();\n }\n }}\n onMouseEnter={() => setHover(true)}\n onMouseLeave={() => setHover(false)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n padding: \"0 16px\",\n minHeight: 44,\n cursor: \"pointer\",\n userSelect: \"none\",\n background: hover ? P.hoverRow : \"transparent\",\n transition: \"background 120ms\",\n }}\n >\n
\n
{label}
\n {description && (\n
\n {description}\n
\n )}\n
\n \n \n \n \n );\n}\n\n/* ═══════════════════════════════════════════════════════════\n FormSegment — label + segmented control\n ═══════════════════════════════════════════════════════════ */\ninterface SegmentOption {\n label: string;\n value: string;\n}\n\ninterface FormSegmentProps {\n label: string;\n options: SegmentOption[];\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n}\n\nfunction FormSegment({\n label,\n options,\n value,\n defaultValue,\n onChange,\n}: FormSegmentProps) {\n const { sound } = React.useContext(FormCtx);\n const id = React.useId();\n const [internal, setInternal] = React.useState(\n defaultValue || options[0]?.value,\n );\n const active = value !== undefined ? value : internal;\n\n const select = (v: string) => {\n if (sound) tick();\n if (onChange) onChange(v);\n else setInternal(v);\n };\n\n return (\n \n
{label}
\n \n {options.map((opt) => {\n const isActive = active === opt.value;\n return (\n select(opt.value)}\n style={{\n position: \"relative\",\n zIndex: 1,\n padding: \"5px 14px\",\n fontSize: 13,\n fontWeight: isActive ? 500 : 400,\n color: isActive ? P.hi : P.dim,\n background: \"transparent\",\n border: \"none\",\n borderRadius: 6,\n cursor: \"pointer\",\n fontFamily: \"inherit\",\n transition: \"color 150ms\",\n }}\n >\n {isActive && (\n \n )}\n {opt.label}\n \n );\n })}\n \n \n );\n}\n\n/* ═══════════════════════════════════════════════════════════\n FormButton — action button\n ═══════════════════════════════════════════════════════════ */\ninterface FormButtonProps {\n children: React.ReactNode;\n variant?: \"primary\" | \"glass\" | \"text\";\n type?: \"submit\" | \"button\";\n onClick?: () => void;\n disabled?: boolean;\n}\n\nfunction FormButton({\n children,\n variant = \"primary\",\n type = \"submit\",\n onClick,\n disabled,\n}: FormButtonProps) {\n const { sound } = React.useContext(FormCtx);\n const [hover, setHover] = React.useState(false);\n\n const isPrimary = variant === \"primary\";\n const isText = variant === \"text\";\n\n return (\n {\n if (disabled) return;\n if (sound) tick();\n onClick?.();\n }}\n onMouseEnter={() => setHover(true)}\n onMouseLeave={() => setHover(false)}\n disabled={disabled}\n style={{\n width: \"100%\",\n padding: isText ? \"8px 0\" : \"12px 0\",\n fontSize: 15,\n fontWeight: isPrimary ? 600 : 500,\n letterSpacing: isPrimary ? \"-0.01em\" : undefined,\n color: isPrimary ? P.accentFg : isText ? P.accent : P.hi,\n background: isPrimary\n ? P.accent\n : isText\n ? \"transparent\"\n : hover\n ? P.ghostHover\n : P.ghost,\n filter: isPrimary && hover ? \"brightness(1.1)\" : undefined,\n border: isPrimary || isText ? \"none\" : `1px solid ${P.glassBorder}`,\n borderRadius: isText ? 0 : 14,\n cursor: disabled ? \"default\" : \"pointer\",\n fontFamily: \"inherit\",\n transition: \"all 150ms\",\n opacity: disabled ? 0.35 : 1,\n boxShadow: isPrimary\n ? \"0 1px 3px rgba(0,0,0,0.12)\"\n : isText\n ? \"none\"\n : P.glassShadow,\n }}\n >\n {children}\n \n );\n}\n\n/* ── exports ───────────────────────────────────────────── */\nexport { GlassForm, FormGroup, FormField, FormToggle, FormSegment, FormButton };\nexport type {\n GlassFormProps,\n FormGroupProps,\n FormFieldProps,\n FormToggleProps,\n FormSegmentProps,\n FormButtonProps,\n};\n", "type": "registry:ui", "target": "components/ruixen/glass-form.tsx" } ] }