{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-form",
  "type": "registry:component",
  "title": "Inline form",
  "description": "Pairs one field with an embedded submit action. The button appears after validation.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/vlak-lib.json",
    "https://vlak.dev/r/icons.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/inline-form.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Icon } from \"./icons\";\n\nexport interface InlineFormProps\n  extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  placeholder?: string;\n  buttonLabel?: React.ReactNode;\n  successLabel?: React.ReactNode;\n  pendingLabel?: React.ReactNode;\n  errorLabel?: React.ReactNode;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** The action only appears once this returns true. Defaults to a loose e-mail check. */\n  validate?: (value: string) => boolean;\n  onSubmit?: (value: string) => void | Promise<void>;\n  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;\n}\n\nconst fadeIn = stylex.keyframes({\n  from: { opacity: 0 },\n  to: { opacity: 1 },\n});\n\nconst styles = stylex.create({\n  field: {\n    boxSizing: \"border-box\",\n    display: \"flex\",\n    flexWrap: \"wrap\",\n    alignItems: {\n      default: \"center\",\n      [mq.phone]: \"stretch\",\n    },\n    gap: \"0.5rem\",\n    width: \"100%\",\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: {\n      default: vlak.divider,\n      \":focus-within\": vlak.accent,\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    backgroundColor: \"var(--bg)\",\n    paddingBlock: {\n      default: \"0.25rem\",\n      [mq.phone]: 0,\n    },\n    paddingInlineStart: {\n      default: \"0.75rem\",\n      [mq.phone]: 0,\n    },\n    paddingInlineEnd: {\n      default: \"0.375rem\",\n      [mq.phone]: 0,\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    transition: `border-color ${vlak.durationSnap} ${vlak.ease}`,\n  },\n  input: {\n    boxSizing: \"border-box\",\n    flexGrow: 1,\n    minWidth: 0,\n    appearance: \"none\",\n    WebkitAppearance: \"none\",\n    borderWidth: 0,\n    backgroundColor: \"transparent\",\n    color: \"var(--text)\",\n    caretColor: \"var(--text)\",\n    fontFamily: \"inherit\",\n    fontSize: {\n      default: \"0.875rem\",\n      [mq.phone]: \"1rem\",\n    },\n    letterSpacing: \"-0.01em\",\n    outline: \"none\",\n    paddingBlock: {\n      default: \"0.4375rem\",\n      [mq.phone]: 0,\n    },\n    paddingInline: {\n      default: 0,\n      [mq.phone]: \"0.875rem\",\n    },\n    minHeight: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    \"::placeholder\": {\n      color: vlak.gray,\n    },\n  },\n  reveal: {\n    maxWidth: 0,\n    opacity: 0,\n    overflow: \"hidden\",\n    transition: `max-width ${vlak.duration} ${vlak.ease}, opacity ${vlak.duration} ${vlak.ease}`,\n  },\n  revealIn: {\n    maxWidth: \"10rem\",\n    opacity: 1,\n  },\n  btn: {\n    height: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    paddingInline: {\n      default: \"1rem\",\n      [mq.phone]: \"1rem\",\n    },\n    fontSize: {\n      default: \"0.78125rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    minWidth: vlak.hit,\n    borderRadius: {\n      default: 0,\n      [mq.phone]: 0,\n    },\n    whiteSpace: \"nowrap\",\n    fontFamily: \"inherit\",\n    fontWeight: 600,\n    letterSpacing: \"-0.01em\",\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    cursor: \"pointer\",\n    backgroundColor: vlak.ink,\n    color: vlak.paper,\n    borderWidth: 0,\n  },\n  subscribed: {\n    display: \"flex\",\n    alignItems: \"center\",\n    gap: \"0.5rem\",\n    fontSize: \"0.875rem\",\n    fontWeight: 600,\n    letterSpacing: \"-0.01em\",\n    color: vlak.ink,\n    animationName: fadeIn,\n    animationDuration: vlak.durationConfirm,\n    animationTimingFunction: vlak.ease,\n  },\n  error: {\n    flexBasis: \"100%\",\n    margin: 0,\n    padding: \"0.5rem\",\n    fontSize: vlak.controlFs,\n    color: vlak.ink,\n  },\n});\n\n/** One field, one action; asynchronous actions only confirm after they resolve. */\nexport const InlineForm = React.forwardRef<HTMLFormElement, InlineFormProps>(function InlineForm({\n  placeholder = \"Your e-mail\",\n  buttonLabel = \"Subscribe\",\n  successLabel = \"You're on the list\",\n  pendingLabel = \"Submitting…\",\n  errorLabel = \"Could not submit. Please try again.\",\n  value: controlledValue,\n  defaultValue = \"\",\n  onValueChange,\n  validate = (v) => /.+@.+\\..+/.test(v),\n  onSubmit,\n  className,\n  style,\n  inputProps,\n  ...props\n}, ref) {\n  const [inner, setInner] = React.useState(defaultValue);\n  const value = controlledValue ?? inner;\n  const [state, setState] = React.useState<\"idle\" | \"pending\" | \"success\" | \"error\">(\"idle\");\n  const submitting = React.useRef(false);\n  const errorId = React.useId();\n  const valid = validate(value);\n  const pending = state === \"pending\";\n  const sx = rs([\"rs-inline-field\", className], styles.field);\n  const input = rs([\"rs-inline-input\", inputProps?.className], styles.input);\n  const reveal = rs([\"rs-reveal\", valid && \"rs-reveal-in\"], styles.reveal, valid && styles.revealIn);\n  const btn = rs([\"rs-btn-primary\", \"rs-inline-btn\", \"rs-inline-field-btn\"], styles.btn);\n  const ok = rs([\"rs-subscribed\"], styles.subscribed);\n  const error = rs([\"rs-inline-error\"], styles.error);\n\n  return (\n    <form\n      ref={ref}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n      aria-busy={pending || undefined}\n      onSubmit={async (e) => {\n        e.preventDefault();\n        if (!valid || submitting.current) return;\n        if (!onSubmit) { setState(\"error\"); return; }\n        submitting.current = true;\n        setState(\"pending\");\n        try {\n          await onSubmit(value);\n          setState(\"success\");\n        } catch {\n          setState(\"error\");\n        } finally {\n          submitting.current = false;\n        }\n      }}\n    >\n      {state === \"success\" ? (\n        <div role=\"status\" className={ok.className} style={ok.style}>\n          <Icon name=\"check\" size={16} />{successLabel}\n        </div>\n      ) : <>\n        <input\n          {...inputProps}\n          className={input.className}\n          style={{ ...input.style, ...inputProps?.style }}\n          placeholder={placeholder}\n          aria-label={inputProps?.[\"aria-label\"] ?? placeholder}\n          aria-describedby={[inputProps?.[\"aria-describedby\"], state === \"error\" ? errorId : undefined].filter(Boolean).join(\" \") || undefined}\n          value={value}\n          disabled={pending || inputProps?.disabled}\n          onChange={(e) => {\n            inputProps?.onChange?.(e);\n            if (e.defaultPrevented) return;\n            if (controlledValue === undefined) setInner(e.target.value);\n            onValueChange?.(e.target.value);\n            if (state === \"error\") setState(\"idle\");\n          }}\n        />\n        <span className={reveal.className} style={reveal.style}>\n          <button type=\"submit\" className={btn.className} style={btn.style} disabled={!valid || pending || inputProps?.disabled} tabIndex={valid ? 0 : -1}>\n            {pending ? pendingLabel : buttonLabel}\n          </button>\n        </span>\n        {state === \"error\" && <p id={errorId} role=\"alert\" className={error.className} style={error.style}>{errorLabel}</p>}\n      </>}\n    </form>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/inline-form.tsx"
    },
    {
      "path": "vlak/styles/inline-form.css",
      "content": "/* ── inline-form: generated from packages/react/src/components/inline-form.tsx ── */\n@keyframes rs-anim-fade-in{from{opacity:0}to{opacity:1}}\n.rs-inline-field{box-sizing:border-box;display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem;width:100%;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius-sm);background-color:var(--bg);padding-block:0.25rem;padding-inline-start:0.75rem;padding-inline-end:0.375rem;transition:border-color var(--duration-snap) var(--ease)}\n.rs-inline-field:focus-within{border-color:var(--accent)}\n@media (max-width: 640px){.rs-inline-field{align-items:stretch;border-radius:var(--radius-sm);padding-block:0;padding-inline-start:0;padding-inline-end:0;min-height:var(--hit)}}\n.rs-inline-input{box-sizing:border-box;flex-grow:1;min-width:0;appearance:none;-webkit-appearance:none;border-width:0;background-color:transparent;color:var(--text);caret-color:var(--text);font-family:inherit;font-size:0.875rem;letter-spacing:-0.01em;outline:none;padding-block:0.4375rem;padding-inline:0;min-height:var(--hit)}\n.rs-inline-input::placeholder{color:var(--text-secondary)}\n@media (max-width: 640px){.rs-inline-input{font-size:1rem;padding-block:0;padding-inline:0.875rem;min-height:var(--hit)}}\n.rs-reveal{max-width:0;opacity:0;overflow:hidden;transition:max-width var(--duration) var(--ease), opacity var(--duration) var(--ease)}\n.rs-reveal-in{max-width:10rem;opacity:1}\n.rs-btn-primary.rs-inline-btn.rs-inline-field-btn{height:var(--hit);padding-inline:1rem;font-size:0.78125rem;min-width:var(--hit);border-radius:0;white-space:nowrap;font-family:inherit;font-weight:600;letter-spacing:-0.01em;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;background-color:var(--text);color:var(--bg);border-width:0}\n@media (max-width: 640px){.rs-btn-primary.rs-inline-btn.rs-inline-field-btn{height:var(--hit);min-height:var(--hit);padding-inline:1rem;font-size:var(--control-fs);border-radius:0}}\n.rs-subscribed{display:flex;align-items:center;gap:0.5rem;font-size:0.875rem;font-weight:600;letter-spacing:-0.01em;color:var(--text);animation-name:rs-anim-fade-in;animation-duration:var(--duration-confirm);animation-timing-function:var(--ease)}\n.rs-inline-error{flex-basis:100%;margin:0;padding:0.5rem;font-size:var(--control-fs);color:var(--text)}\n",
      "type": "registry:file",
      "target": "styles/vlak/inline-form.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "patterns",
      "classes": [
        "rs-inline-field",
        "rs-inline-input",
        "rs-inline-btn",
        "rs-reveal",
        "rs-reveal-in",
        "rs-subscribed",
        "rs-inline-field-btn",
        "rs-inline-error"
      ],
      "snippet": "<div class=\"rs-inline-field\"><input class=\"rs-inline-input\" placeholder=\"Your e-mail\" /><span class=\"rs-reveal rs-reveal-in\"><button class=\"rs-btn-primary rs-inline-btn\">Subscribe</button></span></div>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "vlak-lib",
        "icons"
      ],
      "aliases": [
        "Inline form",
        "Newsletter form",
        "Subscribe form",
        "Single-field form"
      ],
      "example": "import { InlineForm } from \"@noorddev/vlak-react\";\n\n<InlineForm\n  placeholder=\"Your e-mail\"\n  buttonLabel=\"Subscribe\"\n  inputProps={{ \"aria-label\": \"E-mail\", type: \"email\" }}\n  onSubmit={(email) => subscribe(email)}\n/>",
      "usage": {
        "use": [
          "One value and one action: newsletter, invite by e-mail, join a waitlist.",
          "validate to decide when the action appears; the default is a loose e-mail check."
        ],
        "avoid": [
          "More than one field; use Form with Field.",
          "Actions needing a separate confirmation step; compose a Dialog before submitting."
        ]
      },
      "keyboard": [
        {
          "keys": "Enter",
          "does": "Submits once the value validates"
        },
        {
          "keys": "Tab",
          "does": "Reaches the button only after the value validates"
        }
      ],
      "a11y": [
        "The input is named by inputProps aria-label, falling back to the placeholder; use a clear persistent name.",
        "The submit button stays out of the tab order (tabIndex -1) until validate returns true.",
        "Submission awaits the onSubmit promise. aria-busy and pendingLabel indicate progress; duplicate submits are ignored, failure is an alert, and retry preserves the value.",
        "Success is a live status inside the same form, preserving its ref. Without an onSubmit handler the form cannot claim success.",
        "Controlled with value and onValueChange, or uncontrolled with defaultValue; native inputProps onChange is composed with internal state."
      ]
    }
  }
}
