{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-input","type":"registry:component","title":"Input (Motion)","description":"Input with motion: a short shake when it turns invalid, and its error message grows in under the box and folds away when it's fixed.","dependencies":["motion"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/motion-field.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/field.json","https://dashral.com/r/input.json"],"files":[{"path":"registry/dashral/motion/input.tsx","type":"registry:file","target":"components/dashral/motion/input.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { animate } from \"motion/react\"\nimport { Field, joinIds } from \"../field\"\nimport { Input as KitInput, type InputProps } from \"../input\"\nimport { cn } from \"../lib/utils\"\n\nimport { FieldMessage } from \"../lib/motion-field\"\nimport { useReducedMotion } from \"../lib/motion-reduced\"\n\nexport type { InputProps } from \"../input\"\n\n/** What the error says, for telling a new error from a re-render of the same one; null when there's none. */\nfunction errorKey(error: InputProps[\"error\"]) {\n  if (!error) return null\n  return typeof error === \"string\" || typeof error === \"number\" ? String(error) : typeof error === \"boolean\" ? \"\" : \"node\"\n}\n\n/**\n * The kit's Input with motion: the same label, description, prefix, suffix, format and props. When it turns invalid\n * (or the error changes), and again at every try to submit its form while it's still invalid, the box gives a short\n * shake; the error message grows in under it, the words below gliding\n * down instead of jumping, and folds away when it's fixed; a new message crosses over the old one. With reduced\n * motion nothing shakes and messages only fade.\n */\nexport function Input({ label, description, error, labelAction, withAsterisk, id, required, disabled, classNames, ref, ...props }: InputProps) {\n  const reduced = useReducedMotion()\n  const generatedId = React.useId()\n  const fieldId = id ?? generatedId\n  const input = React.useRef<HTMLInputElement | null>(null)\n  const [element, setElement] = React.useState<HTMLInputElement | null>(null)\n  // Its own ref (to find the box to shake) and the one it's given (a form library's), both.\n  const setRef = React.useCallback(\n    (node: HTMLInputElement | null) => {\n      input.current = node\n      setElement(node)\n      if (typeof ref === \"function\") ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref]\n  )\n\n  const key = errorKey(error)\n  const shown = React.useRef(key)\n  const shake = React.useCallback(() => {\n    // The box you see: the input itself, or the box around it and its affixes.\n    const box = input.current?.closest<HTMLElement>(\"[data-slot=input]\")\n    if (reduced || !box) return\n    animate(box, { x: [0, -6, 5, -3, 2, 0] }, { duration: 0.36, ease: \"easeOut\" })\n  }, [reduced])\n  // It shakes when it turns invalid or the error changes…\n  React.useEffect(() => {\n    if (shown.current === key) return\n    shown.current = key\n    if (key !== null) shake()\n  }, [key, shake])\n  // …and again at every try to submit its form while it's still invalid, so a second click on Submit isn't met with\n  // nothing; the browser's own check (required, type=\"email\") finding it invalid shakes it too.\n  React.useEffect(() => {\n    const form = element?.form\n    if (!element) return\n    // Before the form's own handler (capture), so it's the error from the last try that counts; a new one shakes above.\n    const onSubmit = () => {\n      if (shown.current !== null) shake()\n    }\n    form?.addEventListener(\"submit\", onSubmit, true)\n    element.addEventListener(\"invalid\", shake)\n    return () => {\n      form?.removeEventListener(\"submit\", onSubmit, true)\n      element.removeEventListener(\"invalid\", shake)\n    }\n  }, [element, shake])\n\n  const message = error && typeof error !== \"boolean\" ? error : null\n  const descriptionId = description ? `${fieldId}-description` : undefined\n  const errorId = message ? `${fieldId}-error` : undefined\n  const control = (\n    <KitInput\n      {...props}\n      ref={setRef}\n      id={fieldId}\n      required={required}\n      disabled={disabled}\n      aria-invalid={props[\"aria-invalid\"] ?? (error ? true : undefined)}\n      aria-describedby={joinIds(props[\"aria-describedby\"], descriptionId, errorId)}\n      classNames={classNames}\n    />\n  )\n\n  if (!label && !description && !message) return control\n\n  return (\n    <Field\n      id={fieldId}\n      label={label}\n      labelAction={labelAction}\n      withAsterisk={withAsterisk}\n      required={required}\n      disabled={disabled}\n      error={Boolean(error)}\n      className={classNames?.root}\n      classNames={{ label: classNames?.label }}\n    >\n      {/* No gap in here: each line under the box brings its own space, so a message's space grows in with it. */}\n      <div className=\"grid min-w-0\">\n        {control}\n        {description && (\n          <p id={descriptionId} data-slot=\"field-description\" className={cn(\"pt-2 text-xs text-muted-foreground\", classNames?.description)}>\n            {description}\n          </p>\n        )}\n        <FieldMessage id={errorId} message={message} className={classNames?.error} />\n      </div>\n    </Field>\n  )\n}\n"}]}