{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field-motion",
  "title": "Motion field",
  "description": "Styled field: the Motion set. 5 decorative field variations (FocusField, SlideField, FadeField, PopField, RevealField) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/field-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion field family: 5 form fields that share the same skeleton but each carry a\n   different motion (the label lifts on focus, slides while typing, fades while\n   typing, springs open, or the help text opens on focus). Accessibility does not\n   change: the label is bound to the control with htmlFor/id, the description is in\n   the aria-describedby chain, and invalidity falls back to the standard danger\n   appearance ON THE CONTROL via aria-invalid. All motion is disabled through\n   useReducedMotion; the final state is then shown instantly. The ids come from\n   useId. Color comes ONLY from tokens. Renders with no props too. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst gap: Record<StyledSize, string> = {\n  sm: \"gap-1\",\n  md: \"gap-1.5\",\n  lg: \"gap-2\",\n  xl: \"gap-2.5\",\n}\n\nconst labelText: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-sm\",\n  xl: \"text-base\",\n}\n\nconst helpText: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-xs\",\n  lg: \"text-sm\",\n  xl: \"text-sm\",\n}\n\nconst controlHeight: Record<StyledSize, string> = {\n  sm: \"h-8 text-sm\",\n  md: \"h-9 text-sm\",\n  lg: \"h-10 text-base\",\n  xl: \"h-12 text-base\",\n}\n\nconst controlBase =\n  \"w-full rounded-md border border-field-border bg-transparent px-3 py-1 text-foreground outline-none transition-colors placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40\"\n\nconst DEFAULT_LABEL = \"Label\"\nconst DEFAULT_DESCRIPTION = \"Short help text for this field.\"\nconst DEFAULT_PLACEHOLDER = \"Value\"\n\nconst spring = { type: \"spring\" as const, stiffness: 420, damping: 30 }\n\ninterface FieldProps {\n  className?: string\n  size?: StyledSize\n  label?: React.ReactNode\n  description?: React.ReactNode\n}\n\n/* Paylasilan id kumesi. */\nfunction useFieldIds(description: React.ReactNode) {\n  const base = React.useId()\n  const controlId = `${base}-control`\n  const descId = description != null && description !== false ? `${base}-desc` : undefined\n  return { controlId, descId }\n}\n\nfunction FieldLabel({\n  htmlFor,\n  size,\n  children,\n  className,\n}: {\n  htmlFor: string\n  size: StyledSize\n  children: React.ReactNode\n  className?: string\n}) {\n  return (\n    <label\n      htmlFor={htmlFor}\n      data-slot=\"styled-field-label\"\n      className={cn(\"w-fit font-medium leading-none text-foreground select-none\", labelText[size], className)}\n    >\n      {children}\n    </label>\n  )\n}\n\nfunction FieldDescription({ id, size, children, className }: { id?: string; size: StyledSize; children: React.ReactNode; className?: string }) {\n  if (children == null || children === false) return null\n  return (\n    <p id={id} data-slot=\"styled-field-description\" className={cn(\"leading-snug text-muted-foreground\", helpText[size], className)}>\n      {children}\n    </p>\n  )\n}\n\n/* Focus: when the control takes focus the label rises slightly and shifts to the primary tone. State is held at the top, not in the unmounting subtree. */\nexport function FocusField({ className, size = \"md\", label = DEFAULT_LABEL, description = DEFAULT_DESCRIPTION }: FieldProps) {\n  const { controlId, descId } = useFieldIds(description)\n  const reduce = useReducedMotion()\n  const [focused, setFocused] = React.useState(false)\n\n  return (\n    <div data-slot=\"styled-field\" className={cn(\"flex w-full max-w-xs flex-col\", gap[size], className)}>\n      <motion.div\n        animate={reduce ? {} : { y: focused ? -1 : 0 }}\n        transition={spring}\n        className=\"w-fit\"\n      >\n        <FieldLabel htmlFor={controlId} size={size} className={cn(\"transition-colors\", focused && \"text-primary\")}>\n          {label}\n        </FieldLabel>\n      </motion.div>\n      <input\n        id={controlId}\n        placeholder={DEFAULT_PLACEHOLDER}\n        aria-describedby={descId}\n        onFocus={() => setFocused(true)}\n        onBlur={() => setFocused(false)}\n        className={cn(controlBase, controlHeight[size])}\n      />\n      <FieldDescription id={descId} size={size}>\n        {description}\n      </FieldDescription>\n    </div>\n  )\n}\n\n/* Slide: the field enters sliding from the left. No transform under reduced motion, fade only. */\nexport function SlideField({ className, size = \"md\", label = DEFAULT_LABEL, description = DEFAULT_DESCRIPTION }: FieldProps) {\n  const { controlId, descId } = useFieldIds(description)\n  const reduce = useReducedMotion()\n\n  return (\n    <motion.div\n      data-slot=\"styled-field\"\n      initial={reduce ? { opacity: 0 } : { opacity: 0, x: -16 }}\n      animate={reduce ? { opacity: 1 } : { opacity: 1, x: 0 }}\n      transition={{ duration: 0.28, ease: \"easeOut\" }}\n      className={cn(\"flex w-full max-w-xs flex-col\", gap[size], className)}\n    >\n      <FieldLabel htmlFor={controlId} size={size}>\n        {label}\n      </FieldLabel>\n      <input id={controlId} placeholder={DEFAULT_PLACEHOLDER} aria-describedby={descId} className={cn(controlBase, controlHeight[size])} />\n      <FieldDescription id={descId} size={size}>\n        {description}\n      </FieldDescription>\n    </motion.div>\n  )\n}\n\n/* Fade: parcalar sirayla solarak girer (etiket, kontrol, aciklama). */\nexport function FadeField({ className, size = \"md\", label = DEFAULT_LABEL, description = DEFAULT_DESCRIPTION }: FieldProps) {\n  const { controlId, descId } = useFieldIds(description)\n  const reduce = useReducedMotion()\n  const stagger = reduce ? 0 : 0.08\n\n  return (\n    <motion.div\n      data-slot=\"styled-field\"\n      initial=\"hidden\"\n      animate=\"shown\"\n      variants={{ hidden: {}, shown: { transition: { staggerChildren: stagger } } }}\n      className={cn(\"flex w-full max-w-xs flex-col\", gap[size], className)}\n    >\n      <motion.div variants={{ hidden: { opacity: 0 }, shown: { opacity: 1 } }} transition={{ duration: 0.25, ease: \"easeOut\" }} className=\"w-fit\">\n        <FieldLabel htmlFor={controlId} size={size}>\n          {label}\n        </FieldLabel>\n      </motion.div>\n      <motion.input\n        id={controlId}\n        placeholder={DEFAULT_PLACEHOLDER}\n        aria-describedby={descId}\n        variants={{ hidden: { opacity: 0 }, shown: { opacity: 1 } }}\n        transition={{ duration: 0.25, ease: \"easeOut\" }}\n        className={cn(controlBase, controlHeight[size])}\n      />\n      {description != null && description !== false ? (\n        <motion.p\n          id={descId}\n          data-slot=\"styled-field-description\"\n          variants={{ hidden: { opacity: 0 }, shown: { opacity: 1 } }}\n          transition={{ duration: 0.25, ease: \"easeOut\" }}\n          className={cn(\"leading-snug text-muted-foreground\", helpText[size])}\n        >\n          {description}\n        </motion.p>\n      ) : null}\n    </motion.div>\n  )\n}\n\n/* Pop: the field enters growing with a spring. No scale under reduced motion, fade only. */\nexport function PopField({ className, size = \"md\", label = DEFAULT_LABEL, description = DEFAULT_DESCRIPTION }: FieldProps) {\n  const { controlId, descId } = useFieldIds(description)\n  const reduce = useReducedMotion()\n\n  return (\n    <motion.div\n      data-slot=\"styled-field\"\n      initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.94 }}\n      animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n      transition={reduce ? { duration: 0.2, ease: \"easeOut\" } : spring}\n      className={cn(\"flex w-full max-w-xs flex-col origin-top\", gap[size], className)}\n    >\n      <FieldLabel htmlFor={controlId} size={size}>\n        {label}\n      </FieldLabel>\n      <input id={controlId} placeholder={DEFAULT_PLACEHOLDER} aria-describedby={descId} className={cn(controlBase, controlHeight[size])} />\n      <FieldDescription id={descId} size={size}>\n        {description}\n      </FieldDescription>\n    </motion.div>\n  )\n}\n\n/* Reveal: the help text opens only on focus. An invisible copy stays in the DOM as sr-only so the text is always present and in aria-describedby. */\nexport function RevealField({ className, size = \"md\", label = DEFAULT_LABEL, description = DEFAULT_DESCRIPTION }: FieldProps) {\n  const base = React.useId()\n  const controlId = `${base}-control`\n  const hasDescription = description != null && description !== false\n  const descId = hasDescription ? `${base}-desc` : undefined\n  const reduce = useReducedMotion()\n  const [focused, setFocused] = React.useState(false)\n\n  return (\n    <div data-slot=\"styled-field\" className={cn(\"flex w-full max-w-xs flex-col\", gap[size], className)}>\n      <FieldLabel htmlFor={controlId} size={size}>\n        {label}\n      </FieldLabel>\n      <input\n        id={controlId}\n        placeholder={DEFAULT_PLACEHOLDER}\n        aria-describedby={descId}\n        onFocus={() => setFocused(true)}\n        onBlur={() => setFocused(false)}\n        className={cn(controlBase, controlHeight[size])}\n      />\n      {hasDescription ? (\n        <>\n          <span id={descId} className=\"sr-only\">\n            {description}\n          </span>\n          <AnimatePresence initial={false}>\n            {focused ? (\n              <motion.p\n                aria-hidden=\"true\"\n                data-slot=\"styled-field-description\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n                animate={reduce ? { opacity: 1 } : { opacity: 1, height: \"auto\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n                transition={{ duration: 0.2, ease: \"easeOut\" }}\n                className={cn(\"overflow-hidden leading-snug text-muted-foreground\", helpText[size])}\n              >\n                {description}\n              </motion.p>\n            ) : null}\n          </AnimatePresence>\n        </>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/field-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "field"
  ],
  "type": "registry:component"
}