{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-input",
  "title": "Floating Input",
  "description": "Astro text fields with a label that moves when focused or filled.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "cn@0.4.0"
  ],
  "registryDependencies": [
    "@gnosticai/input",
    "@gnosticai/label",
    "@gnosticai/textarea"
  ],
  "files": [
    {
      "path": "packages/ui/src/components/ui/floating-input/floating-field.astro",
      "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"cn\"\nimport { Label } from \"../label/index.ts\"\nimport {\n  floatingInputControlClass,\n  floatingInputDescriptionClass,\n  floatingInputDescriptionId,\n  floatingInputErrorClass,\n  floatingInputErrorId,\n  floatingInputRootClass,\n  floatingLabelVariants,\n  type FloatingFieldProps,\n  type FloatingLabelVariantProps,\n} from \"./floating-input.ts\"\n\ntype Props = FloatingFieldProps & {\n  class?: HTMLAttributes<\"div\">[\"class\"]\n  /** `data-slot` value for this field. */\n  dataSlot?: string\n  /** Chooses the empty-state position of the label. */\n  control?: FloatingLabelVariantProps[\"control\"]\n}\n\nconst {\n  class: className,\n  label,\n  id,\n  description,\n  error,\n  size = \"md\",\n  control = \"input\",\n  dataSlot = \"floating-input\",\n} = Astro.props\n\nconst descriptionId = description ? floatingInputDescriptionId(id) : undefined\nconst errorId = error ? floatingInputErrorId(id) : undefined\n---\n\n{/**\n  The control slot comes first.\n  The label follows it so the peer styles can see the control.\n  Both sit in one relative box. The empty label uses half of that box.\n  The description stays outside the box, so it does not pull the label down.\n*/}\n\n<div\n  data-slot={dataSlot}\n  data-size={size}\n  data-invalid={error ? \"true\" : undefined}\n  class={cn(floatingInputRootClass, className)}\n>\n  <div class={floatingInputControlClass}>\n    <slot />\n    <Label class={floatingLabelVariants({ size, control })} for={id}>\n      {label}\n    </Label>\n  </div>\n  {descriptionId && (\n    <p\n      id={descriptionId}\n      data-slot=\"floating-input-description\"\n      class={floatingInputDescriptionClass}\n    >\n      {description}\n    </p>\n  )}\n  {errorId && (\n    <p\n      id={errorId}\n      role=\"alert\"\n      data-slot=\"floating-input-error\"\n      class={floatingInputErrorClass}\n    >\n      {error}\n    </p>\n  )}\n</div>\n",
      "type": "registry:file",
      "target": "~/src/components/ui/floating-input/floating-field.astro"
    },
    {
      "path": "packages/ui/src/components/ui/floating-input/floating-input.astro",
      "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { Input } from \"../input/index.ts\"\nimport FloatingField from \"./floating-field.astro\"\nimport {\n  assertFloatingField,\n  floatingInputDescribedBy,\n  floatingInputVariants,\n  type FloatingFieldProps,\n} from \"./floating-input.ts\"\n\ntype Props = Omit<HTMLAttributes<\"input\">, \"size\"> & FloatingFieldProps\n\nconst {\n  class: className,\n  label,\n  id,\n  description,\n  error,\n  size = \"md\",\n  placeholder = \" \",\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n  ...props\n} = Astro.props\n\nassertFloatingField(\"FloatingInput\", { id, label })\n\n// An empty placeholder keeps the label inside the control.\n// The placeholder stays invisible.\nconst placeholderText = placeholder || \" \"\nconst describedBy =\n  typeof ariaDescribedBy === \"string\" ? ariaDescribedBy : undefined\n---\n\n<FloatingField\n  class={className}\n  label={label}\n  id={id}\n  description={description}\n  error={error}\n  size={size}\n  control=\"input\"\n  dataSlot=\"floating-input\"\n>\n  <Input\n    {...props}\n    id={id}\n    class={floatingInputVariants({ size })}\n    placeholder={placeholderText}\n    aria-invalid={error ? \"true\" : ariaInvalid}\n    aria-describedby={floatingInputDescribedBy(id, {\n      description,\n      error,\n      describedBy,\n    })}\n  />\n</FloatingField>\n",
      "type": "registry:file",
      "target": "~/src/components/ui/floating-input/floating-input.astro"
    },
    {
      "path": "packages/ui/src/components/ui/floating-input/floating-input.ts",
      "content": "import { cva, type VariantProps } from 'class-variance-authority'\n\n/**\n * Classes for a control with a floating label.\n *\n * The control and the label use `cva` because they have size variants.\n * The root, the description, and the error are plain class strings.\n * The label must come after the control. The control uses `peer`.\n *\n * `py-0` clears the padding on `Input` and `Textarea`.\n * The size classes use `!` so their padding wins over that padding.\n *\n * The empty label uses `top-1/2`. That value is half the height of the\n * nearest relative parent. That parent must wrap only the control and\n * the label. Helper text outside that parent does not move the label.\n */\n\n/** Text that identifies the control, plus the optional help and error text. */\nexport interface FloatingFieldProps {\n    /** Text that floats above the control. */\n    label: string\n    /** Id of the control. The label uses this id. */\n    id: string\n    /** Helper text under the control. */\n    description?: string\n    /** Validation message. The field is invalid when this text is set. */\n    error?: string\n    /** `sm` is shorter. `md` is the default. */\n    size?: 'sm' | 'md'\n}\n\nconst floatingControlClass = [\n    'peer block h-auto w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-2.5 py-0 text-sm text-foreground shadow-none',\n    'focus:border-primary focus:ring-0 focus:outline-none',\n    'focus-visible:border-primary focus-visible:ring-0',\n    'placeholder:opacity-0',\n    'aria-invalid:ring-0',\n    'dark:bg-transparent',\n]\n\n/** Classes for the single-line control. */\nexport const floatingInputVariants = cva(floatingControlClass, {\n    variants: {\n        size: {\n            sm: 'pt-3! pb-1.5!',\n            md: 'pt-4! pb-2.5!',\n        },\n    },\n    defaultVariants: {\n        size: 'md',\n    },\n})\n\n/**\n * Classes for the multiline control.\n * `field-sizing-fixed` keeps the `rows` height. The shared `Textarea`\n * uses `field-sizing-content`, which would shrink the field to one line.\n */\nexport const floatingTextAreaVariants = cva(\n    [...floatingControlClass, 'field-sizing-fixed min-h-16 resize-y'],\n    {\n        variants: {\n            size: {\n                sm: 'pt-4! pb-1.5!',\n                md: 'pt-4! pb-2.5!',\n            },\n        },\n        defaultVariants: {\n            size: 'md',\n        },\n    },\n)\n\n/**\n * Classes for the floating label.\n *\n * The resting position sits on the border.\n * `placeholder-shown` moves the label into the empty control.\n * `focus` is ordered after `placeholder-shown`, so a focused empty\n * control keeps the label on the border.\n *\n * `translate` and `scale` are separate properties in Tailwind 4,\n * so the transition lists those properties by name.\n */\nexport const floatingLabelVariants = cva(\n    [\n        'bg-background text-muted-foreground absolute start-1 z-10 w-fit origin-left transform cursor-text px-2 text-sm leading-normal font-normal whitespace-nowrap transition-[top,translate,scale,color,background-color] duration-300 select-none',\n        'peer-focus:bg-background peer-focus:text-primary peer-focus:px-2',\n        'peer-placeholder-shown:bg-transparent',\n        'rtl:origin-right rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4',\n    ],\n    {\n        variants: {\n            size: {\n                sm: 'top-1 -translate-y-3 scale-75 peer-focus:top-1 peer-focus:-translate-y-3 peer-focus:scale-75',\n                md: 'top-2 -translate-y-4 scale-75 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75',\n            },\n            control: {\n                input: 'peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100',\n                textarea:\n                    'peer-placeholder-shown:top-4 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100',\n            },\n        },\n        defaultVariants: {\n            size: 'md',\n            control: 'input',\n        },\n    },\n)\n\n/**\n * Classes for the field.\n * The error uses this element as its position parent.\n */\nexport const floatingInputRootClass =\n    'relative w-full text-left data-invalid:mb-6'\n\n/**\n * Classes for the box that holds the control and the label.\n * `top-1/2` on the empty label uses this box, not the helper text.\n */\nexport const floatingInputControlClass = 'relative'\n\n/** Classes for the validation message. It sits under the field. */\nexport const floatingInputErrorClass =\n    'text-destructive absolute top-full right-2 left-2 mt-px text-sm'\n\n/** Classes for the helper text under the control. */\nexport const floatingInputDescriptionClass =\n    'text-muted-foreground mt-1 px-2 text-sm font-normal'\n\nexport type FloatingInputVariantProps = VariantProps<\n    typeof floatingInputVariants\n>\n\nexport type FloatingLabelVariantProps = VariantProps<\n    typeof floatingLabelVariants\n>\n\n/** Id of the helper text for `id`. */\nexport function floatingInputDescriptionId(id: string) {\n    return `${id}-description`\n}\n\n/** Id of the validation message for `id`. */\nexport function floatingInputErrorId(id: string) {\n    return `${id}-error`\n}\n\n/**\n * Value for `aria-describedby` on the control.\n * Includes the helper text, the validation message, and any extra id.\n */\nexport function floatingInputDescribedBy(\n    id: string,\n    options: {\n        description?: string | undefined\n        error?: string | undefined\n        describedBy?: string | undefined\n    },\n) {\n    const ids = [\n        options.description ? floatingInputDescriptionId(id) : undefined,\n        options.error ? floatingInputErrorId(id) : undefined,\n        options.describedBy,\n    ].filter((value): value is string => Boolean(value))\n\n    if (ids.length === 0) return undefined\n    return ids.join(' ')\n}\n\n/** Stop render when the label cannot point at a control. */\nexport function assertFloatingField(\n    component: string,\n    props: { id?: string | undefined; label?: string | undefined },\n) {\n    if (!props.id) {\n        throw new Error(\n            `${component} requires an id so the label can reference the control.`,\n        )\n    }\n\n    if (!props.label?.trim()) {\n        throw new Error(`${component} requires a label.`)\n    }\n}\n",
      "type": "registry:file",
      "target": "~/src/components/ui/floating-input/floating-input.ts"
    },
    {
      "path": "packages/ui/src/components/ui/floating-input/floating-textarea.astro",
      "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { Textarea } from \"../textarea/index.ts\"\nimport FloatingField from \"./floating-field.astro\"\nimport {\n  assertFloatingField,\n  floatingInputDescribedBy,\n  floatingTextAreaVariants,\n  type FloatingFieldProps,\n} from \"./floating-input.ts\"\n\ntype Props = HTMLAttributes<\"textarea\"> & FloatingFieldProps\n\nconst {\n  class: className,\n  label,\n  id,\n  description,\n  error,\n  size = \"md\",\n  placeholder = \" \",\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n  ...props\n} = Astro.props\n\nassertFloatingField(\"FloatingTextarea\", { id, label })\n\n// An empty placeholder keeps the label inside the control.\n// The placeholder stays invisible.\nconst placeholderText = placeholder || \" \"\nconst describedBy =\n  typeof ariaDescribedBy === \"string\" ? ariaDescribedBy : undefined\n---\n\n<FloatingField\n  class={className}\n  label={label}\n  id={id}\n  description={description}\n  error={error}\n  size={size}\n  control=\"textarea\"\n  dataSlot=\"floating-textarea\"\n>\n  <Textarea\n    {...props}\n    id={id}\n    class={floatingTextAreaVariants({ size })}\n    placeholder={placeholderText}\n    aria-invalid={error ? \"true\" : ariaInvalid}\n    aria-describedby={floatingInputDescribedBy(id, {\n      description,\n      error,\n      describedBy,\n    })}\n  />\n</FloatingField>\n",
      "type": "registry:file",
      "target": "~/src/components/ui/floating-input/floating-textarea.astro"
    },
    {
      "path": "packages/ui/src/components/ui/floating-input/index.ts",
      "content": "export { default as FloatingInput } from './floating-input.astro'\nexport { default as FloatingTextarea } from './floating-textarea.astro'\n\nexport {\n    floatingInputControlClass,\n    floatingInputDescribedBy,\n    floatingInputDescriptionClass,\n    floatingInputDescriptionId,\n    floatingInputErrorClass,\n    floatingInputErrorId,\n    floatingInputRootClass,\n    floatingInputVariants,\n    floatingLabelVariants,\n    floatingTextAreaVariants,\n    type FloatingFieldProps,\n    type FloatingInputVariantProps,\n    type FloatingLabelVariantProps,\n} from './floating-input.ts'\n",
      "type": "registry:file",
      "target": "~/src/components/ui/floating-input/index.ts"
    }
  ],
  "meta": {
    "framework": "astro",
    "source": "@gnosticai/ui"
  },
  "docs": "https://ui.gnosticai.com/components/floating-input",
  "categories": [
    "ui"
  ],
  "type": "registry:ui"
}
