{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-invalid-shake",
  "title": "useInvalidShake",
  "description": "Shakes a form control when a submit attempt finds it invalid, and never while someone is still typing.",
  "registryDependencies": [
    "https://hextaui.com/r/motion.json"
  ],
  "files": [
    {
      "path": "hooks/use-invalid-shake.ts",
      "content": "import * as React from \"react\"\n\nimport { prefersReducedMotion } from \"@/lib/motion\"\n\ntype ShakeControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\n\nconst submitSelector =\n  'button:not([type]), button[type=\"submit\"], input[type=\"submit\"], input[type=\"image\"]'\n\nfunction isInvalid(control: ShakeControl) {\n  return (\n    control.getAttribute(\"aria-invalid\") === \"true\" ||\n    control.hasAttribute(\"data-invalid\") ||\n    control.matches(\":user-invalid\")\n  )\n}\n\nfunction useInvalidShake(\n  ref: React.RefObject<ShakeControl | null>,\n  enabled = true\n) {\n  React.useEffect(() => {\n    const control = ref.current\n    const form = control?.form\n    if (!enabled || !control || !form) {\n      return\n    }\n\n    let attempt = 0\n    let shaken = 0\n    let attemptedAt = -Infinity\n    let timer: ReturnType<typeof setTimeout> | undefined\n    let frame = 0\n\n    const shake = () => {\n      if (shaken === attempt || prefersReducedMotion()) {\n        return\n      }\n      shaken = attempt\n      const target =\n        control.closest<HTMLElement>(\"[data-slot=input-group]\") ?? control\n      target.removeAttribute(\"data-shake\")\n      void target.offsetWidth\n      target.setAttribute(\"data-shake\", \"\")\n      clearTimeout(timer)\n      timer = setTimeout(() => target.removeAttribute(\"data-shake\"), 400)\n    }\n\n    const recent = () => performance.now() - attemptedAt < 600\n\n    const check = () => {\n      if (recent() && isInvalid(control)) {\n        shake()\n      }\n    }\n\n    const onAttempt = () => {\n      attempt += 1\n      attemptedAt = performance.now()\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(check)\n    }\n\n    const onClick = (event: MouseEvent) => {\n      const target = event.target as Element | null\n      if (target?.closest(submitSelector)) {\n        onAttempt()\n      }\n    }\n\n    const onKeyDown = (event: KeyboardEvent) => {\n      const target = event.target as Element | null\n      if (event.key === \"Enter\" && target instanceof HTMLInputElement) {\n        onAttempt()\n      }\n    }\n\n    const onInvalid = () => {\n      if (recent()) {\n        shake()\n      }\n    }\n\n    const attributes = new MutationObserver(check)\n    attributes.observe(control, {\n      attributes: true,\n      attributeFilter: [\"aria-invalid\", \"data-invalid\"],\n    })\n\n    form.addEventListener(\"click\", onClick, true)\n    form.addEventListener(\"keydown\", onKeyDown, true)\n    form.addEventListener(\"submit\", onAttempt, true)\n    control.addEventListener(\"invalid\", onInvalid)\n\n    return () => {\n      attributes.disconnect()\n      form.removeEventListener(\"click\", onClick, true)\n      form.removeEventListener(\"keydown\", onKeyDown, true)\n      form.removeEventListener(\"submit\", onAttempt, true)\n      control.removeEventListener(\"invalid\", onInvalid)\n      cancelAnimationFrame(frame)\n      clearTimeout(timer)\n    }\n  }, [ref, enabled])\n}\n\nexport { useInvalidShake }\nexport type { ShakeControl }\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}