{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field",
  "type": "registry:ui",
  "title": "Field",
  "description": "Base UI Field wrappers — label, description, and an error that shakes in, with the error haptic.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/motion.json",
    "https://seamui.dev/r/haptics.json",
    "https://seamui.dev/r/input.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Field as BaseField } from \"@base-ui/react/field\"\nimport { Fieldset as BaseFieldset } from \"@base-ui/react/fieldset\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { reduced, shake, useMounted, useReducedMotion } from \"@/lib/motion\"\nimport { useHaptics } from \"@/lib/haptics\"\nimport { Input } from \"./input\"\n\nfunction Field({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseField.Root>) {\n  return (\n    <BaseField.Root\n      data-slot=\"field\"\n      className={cn(\"flex w-full flex-col gap-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction FieldLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseField.Label>) {\n  return (\n    <BaseField.Label\n      data-slot=\"field-label\"\n      className={cn(\n        \"text-sm leading-none font-medium select-none\",\n        \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n// Renders the seam Input by default (dogfooding: debossed well, focus ring,\n// invalid styling for free); pass `render` to wire any other control into the\n// field, e.g. <FieldControl render={<Textarea />} />. Dropping a seam\n// Input/Textarea directly inside <Field> works too — they're Field-aware.\nfunction FieldControl({\n  render,\n  ...props\n}: React.ComponentProps<typeof BaseField.Control>) {\n  return (\n    <BaseField.Control\n      data-slot=\"field-control\"\n      render={render ?? <Input />}\n      {...props}\n    />\n  )\n}\n\nfunction FieldDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseField.Description>) {\n  return (\n    <BaseField.Description\n      data-slot=\"field-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\n// motion.create() per element type, cached so re-renders reuse components\n// (same shape as button.tsx's consumer-render wrapping).\nconst motionTagCache = new Map<string, React.ElementType>()\nconst motionTypeCache = new WeakMap<object, React.ElementType>()\nfunction asMotion(type: React.ElementType): React.ElementType {\n  const cache = typeof type === \"string\" ? motionTagCache : motionTypeCache\n  let cached = cache.get(type as never)\n  if (!cached) {\n    cached = motion.create(type as React.ComponentType) as React.ElementType\n    cache.set(type as never, cached as never)\n  }\n  return cached\n}\n\nfunction hasRenderableContent(children: React.ReactNode): boolean {\n  if (children == null || typeof children === \"boolean\") return false\n  if (typeof children === \"string\") return children.length > 0\n  if (Array.isArray(children)) return children.some(hasRenderableContent)\n  return true\n}\n\n// One error signal per rejection: surfaces mounting in the same validation\n// pass (multi-field submit) coalesce into a single haptic — overlapping\n// navigator.vibrate() calls would cancel each other into a garbled buzz.\nlet lastErrorHapticAt = 0\n\ntype FieldErrorSurfaceProps = React.ComponentProps<typeof motion.div> & {\n  /** False while the page is first painting — a pre-existing error (server\n   *  errors, forced `invalid`, `match`) renders statically instead of\n   *  buzzing/shaking a page the user hasn't touched. */\n  fresh?: boolean\n  /** Consumer-supplied render element to wrap with the error motion. */\n  consumerEl?: React.ReactElement | null\n}\n\n// The seam error pattern (CLAUDE.md §3: shake, paired with reduced.flash\n// under reduced motion, plus the error haptic) — fired when an error message\n// APPEARS: on a post-first-paint mount, or when a message lands in an\n// already-mounted error (Form `errors`, `match`). A message merely changing\n// text doesn't re-fire, matching the OTP field's flip-on-invalid wiring.\nconst FieldErrorSurface = React.forwardRef<HTMLElement, FieldErrorSurfaceProps>(\n  function FieldErrorSurface({ fresh = true, consumerEl, ...props }, ref) {\n    const reduceMotion = useReducedMotion() ?? false\n    const { trigger } = useHaptics()\n\n    const hasMessage = hasRenderableContent(props.children as React.ReactNode)\n    const [signal, setSignal] = React.useState(() =>\n      fresh && hasMessage ? 1 : 0\n    )\n    const prevHasMessage = React.useRef(hasMessage)\n    React.useEffect(() => {\n      if (hasMessage && !prevHasMessage.current) setSignal((n) => n + 1)\n      prevHasMessage.current = hasMessage\n    }, [hasMessage])\n\n    const firedFor = React.useRef(0)\n    React.useEffect(() => {\n      if (signal === 0 || firedFor.current === signal) return\n      firedFor.current = signal\n      const now = performance.now()\n      if (now - lastErrorHapticAt > 64) {\n        lastErrorHapticAt = now\n        trigger(\"error\")\n      }\n    }, [signal, trigger])\n\n    const Comp = consumerEl\n      ? asMotion(consumerEl.type as React.ElementType)\n      : motion.div\n    const elProps = (consumerEl?.props ?? {}) as { className?: string }\n    const merged = {\n      ...elProps,\n      ...props,\n      className: cn(elProps.className, props.className),\n      ref,\n    }\n\n    if (signal === 0) return <Comp {...merged} />\n    return (\n      // key: a fresh signal remounts the surface so the keyframes re-run\n      // when a new error lands in an already-mounted slot.\n      <Comp\n        key={signal}\n        animate={reduceMotion ? reduced.flash.animate : shake.animate}\n        transition={reduceMotion ? reduced.flash.transition : shake.transition}\n        {...merged}\n      />\n    )\n  }\n)\n\nfunction FieldError({\n  className,\n  render,\n  ...props\n}: React.ComponentProps<typeof BaseField.Error>) {\n  // True only after first paint: errors present at initial render (restored\n  // server errors, forced invalid) appear statically; errors that happen\n  // later get the full shake + haptic signal.\n  const mounted = useMounted()\n  return (\n    <BaseField.Error\n      data-slot=\"field-error\"\n      className={cn(\"text-destructive text-sm\", className)}\n      render={\n        // Function-form render passes through untouched (no error motion),\n        // matching button.tsx; element-form is motion-wrapped so a custom\n        // element keeps the seam error feedback.\n        typeof render === \"function\" ? (\n          render\n        ) : (\n          <FieldErrorSurface fresh={mounted} consumerEl={render} />\n        )\n      }\n      {...props}\n    />\n  )\n}\n\n// Render-prop access to the control's ValidityState — no DOM of its own.\nconst FieldValidity = BaseField.Validity\n\nfunction FieldItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseField.Item>) {\n  return (\n    <BaseField.Item\n      data-slot=\"field-item\"\n      className={cn(\"flex flex-col gap-1\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction Fieldset({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseFieldset.Root>) {\n  return (\n    <BaseFieldset.Root\n      data-slot=\"fieldset\"\n      className={cn(\"flex w-full flex-col gap-4\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction FieldsetLegend({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseFieldset.Legend>) {\n  return (\n    <BaseFieldset.Legend\n      data-slot=\"fieldset-legend\"\n      className={cn(\"text-sm leading-none font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldControl,\n  FieldDescription,\n  FieldError,\n  FieldValidity,\n  FieldItem,\n  Fieldset,\n  FieldsetLegend,\n}\n",
      "type": "registry:ui",
      "target": "components/ui/field.tsx"
    }
  ]
}