{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"input-group","title":"Input Group","description":"Composite for related Input and Textarea controls, add-ons, segments, and focus routing.","dependencies":["class-variance-authority@0.7.1"],"registryDependencies":["@siteplane/base","@siteplane/input","@siteplane/textarea"],"files":[{"path":"packages/ui/src/components/input-group/input-group.tsx","content":"\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  formControlSurfaceClassNames,\n  formControlTextClassNames,\n  surfaceClassNames,\n  type FormControlTextSize,\n  type FormControlVariant,\n} from \"../../system/styles/surface\";\nimport { cn } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\nimport type { InputProps } from \"../input\";\nimport { KitInput } from \"../input/input-internal\";\nimport { type TextareaProps } from \"../textarea\";\nimport { KitTextarea } from \"../textarea/textarea-internal\";\n\nimport \"./input-group.css\";\n\nconst inputGroupInteractiveSelector =\n  \"button, a, input, select, textarea, [contenteditable='true'], [role='button'], [role='combobox'], [role='listbox'], [role='spinbutton'], [data-slot='select-trigger']\";\n\n// An addon's cursor, line height, its order and padding by alignment, and the\n// first-line alignment beside a textarea are the kit base (design-rework.css).\n// The addon spaces the icons it holds; design-rework.css sizes them, but not\n// the icon of a Button in it, which the Button's own icon rule sizes.\nconst inputGroupAddonVariants = cva(\n  \"flex h-auto select-none justify-center gap-2 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80\",\n  {\n    defaultVariants: {\n      align: \"inline-start\",\n    },\n    variants: {\n      align: {\n        \"block-end\": \"w-full justify-start\",\n        \"block-start\": \"w-full justify-start\",\n        \"inline-end\": \"\",\n        \"inline-start\": \"\",\n      },\n    },\n  },\n);\n\n/** Whether the InputGroup opens with an addon (see `opensWithAddon`). */\nconst InputGroupInsetStartContext = React.createContext(false);\n\n/** Whether the InputGroup keeps the end of its control clear (`insetEnd`). */\nconst InputGroupInsetEndContext = React.createContext(false);\n\n/**\n * Whether an InputGroupAddon at the inline start, its default alignment, is\n * among the group's children, also inside a Fragment: an icon or a text\n * prefix before the input. Read from the children while rendering, so the\n * server markup is already right.\n */\nfunction opensWithAddon(children: React.ReactNode): boolean {\n  return React.Children.toArray(children).some((child) => {\n    if (\n      !React.isValidElement<{ align?: unknown; children?: React.ReactNode }>(\n        child,\n      )\n    )\n      return false;\n    if (child.type === React.Fragment)\n      return opensWithAddon(child.props.children);\n    return (\n      child.type === InputGroupAddon &&\n      (child.props.align ?? \"inline-start\") === \"inline-start\"\n    );\n  });\n}\n\n/**\n * A group that opens with an addon tells its input, whose start edge then\n * adjoins the addon (Input `insetStart`). In a floating Field that input keeps\n * the label above the group: a floating field takes no leading icon.\n */\nexport function InputGroup({\n  children,\n  className,\n  insetEnd = false,\n  onMouseDown,\n  textSize,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /**\n   * Keeps the end of the input or textarea clear for an action placed over\n   * it, such as a square Button 8 px inside the top-end corner of a textarea.\n   * The text ends 8 px before a Button of the control size.\n   */\n  insetEnd?: boolean | undefined;\n  textSize?: FormControlTextSize | undefined;\n  variant?: FormControlVariant | undefined;\n}): React.ReactElement {\n  // The room follows the control size, which a Button in the group takes too.\n  const controlSize = useResolvedControlSize<SiteplaneControlSize>(undefined);\n  return (\n    <div\n      className={cn(\n        slotClass(\"input-group\", props),\n        baseClass(\"input-group\", props),\n        variant === \"soft\"\n          ? surfaceClassNames.formControlCompositeSoft\n          : cn(\n              surfaceClassNames.formControlComposite,\n              formControlSurfaceClassNames[variant],\n            ),\n        \"has-[textarea]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col\",\n        textSize && formControlTextClassNames[textSize],\n        className,\n      )}\n      data-foundation=\"input-group\"\n      data-inset-end={insetEnd ? controlSize : undefined}\n      data-slot=\"input-group\"\n      data-text-size={textSize}\n      data-variant={variant}\n      onMouseDown={(event) => {\n        onMouseDown?.(event);\n        if (\n          event.defaultPrevented ||\n          (event.target as HTMLElement).closest(inputGroupInteractiveSelector)\n        ) {\n          return;\n        }\n\n        const controls = Array.from(\n          event.currentTarget.querySelectorAll<HTMLElement>(\n            \"input:not([hidden]):not([type='hidden']):not(:disabled), textarea:not(:disabled), [contenteditable='true'], [role='spinbutton']:not([aria-disabled='true'])\",\n          ),\n        );\n        const control =\n          controls.find(\n            (candidate) =>\n              event.clientX <= candidate.getBoundingClientRect().right,\n          ) ?? controls.at(-1);\n        if (!control) return;\n\n        event.preventDefault();\n        control.focus();\n        if (\n          control instanceof HTMLInputElement &&\n          control.closest(\".sp-input-segment\")\n        ) {\n          control.select();\n        }\n      }}\n      role=\"group\"\n      {...props}\n    >\n      <InputGroupInsetStartContext.Provider value={opensWithAddon(children)}>\n        <InputGroupInsetEndContext.Provider value={insetEnd}>\n          {children}\n        </InputGroupInsetEndContext.Provider>\n      </InputGroupInsetStartContext.Provider>\n    </div>\n  );\n}\n\nexport function InputGroupAddon({\n  className,\n  align = \"inline-start\",\n  divider = false,\n  padding = \"default\",\n  ...props\n}: React.ComponentProps<\"div\"> &\n  VariantProps<typeof inputGroupAddonVariants> & {\n    divider?: boolean;\n    padding?: \"default\" | \"compact\" | \"none\";\n  }): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        slotClass(\"input-group-addon\", props),\n        inputGroupAddonVariants({ align }),\n        className,\n      )}\n      data-align={align}\n      data-divider={divider || undefined}\n      data-padding={padding}\n      data-slot=\"input-group-addon\"\n      {...props}\n    />\n  );\n}\n\nexport function InputGroupText({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  // The text sits on a 1em line, and the kit font draws descenders below it,\n  // so a long text is clipped at the sides only. design-rework.css sizes its\n  // icons beside a field control.\n  return (\n    <span\n      className={cn(\n        // The text takes the slot of its host (a Date Range Picker's\n        // separator), and its class with it; sp-input-group-text marks the\n        // text in every host.\n        slotClass(undefined, props),\n        \"sp-input-group-text flex items-center gap-2 overflow-x-clip whitespace-nowrap text-muted-foreground leading-none [&_svg]:pointer-events-none [&_svg]:-mx-0.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function InputGroupInput({\n  className,\n  insetStart,\n  ...props\n}: Omit<InputProps, \"profile\" | \"unstyled\">): React.ReactElement {\n  // An addon before the input takes its start edge.\n  const addonBefore = React.useContext(InputGroupInsetStartContext);\n\n  return (\n    <KitInput\n      {...props}\n      className={className}\n      insetStart={insetStart || addonBefore}\n      profile=\"input-group\"\n      unstyled\n    />\n  );\n}\n\nexport function InputGroupSegment({\n  className,\n  insetStart = false,\n  onFocus,\n  width = \"2-digit\",\n  ...props\n}: Omit<InputProps, \"profile\" | \"unstyled\"> & {\n  insetStart?: boolean;\n  width?: \"2-digit\" | \"4-digit\" | \"fill\";\n}): React.ReactElement {\n  return (\n    <KitInput\n      {...props}\n      className={cn(\"sp-input-segment\", className)}\n      data-segment-inset-start={insetStart || undefined}\n      data-segment-width={width}\n      onFocus={(event) => {\n        event.currentTarget.select();\n        onFocus?.(event);\n      }}\n      profile=\"input-group\"\n      unstyled\n    />\n  );\n}\n\nexport function InputGroupTextarea({\n  className,\n  ...props\n}: Omit<TextareaProps, \"profile\" | \"unstyled\">): React.ReactElement {\n  // A group with insetEnd keeps the end of the textarea clear.\n  const insetEnd = React.useContext(InputGroupInsetEndContext);\n\n  return (\n    <KitTextarea\n      {...props}\n      className={className}\n      insetEnd={insetEnd}\n      profile=\"input-group\"\n      unstyled\n    />\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/input-group/input-group.tsx"},{"path":"packages/ui/src/components/input-group/input-group.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* The field scope. Every InputGroup carries data-variant; the rules below\n   * name it, also beside a variant value, so their specificity stays in step\n   * with each other. */\n  .sp-base-input-group[data-variant] {\n    --input-group-surface-subtle: rgb(0 0 0 / 4%);\n    --input-segment-background: color-mix(\n      in srgb,\n      var(--color-blue-500) 16%,\n      transparent\n    );\n    --input-segment-padding-block: calc(var(--spacing) * 0.5);\n    --input-segment-padding-inline: var(--spacing);\n    --sp-semantic-color-background: var(--color-white);\n    --sp-semantic-color-border: rgb(0 0 0 / 8%);\n    --sp-semantic-color-destructive: var(--color-red-500);\n    --sp-semantic-color-foreground: var(--color-neutral-800);\n    --sp-semantic-color-input: rgb(0 0 0 / 10%);\n    --sp-semantic-color-muted-foreground: color-mix(\n      in srgb,\n      var(--color-neutral-500) 90%,\n      var(--color-black)\n    );\n    --sp-semantic-color-ring: var(--color-neutral-400);\n  }\n\n  /* InputGroup exposes only composite context. Public child profiles decide how\n   * these signals map onto their own internal control anatomy. */\n  .sp-base-input-group {\n    --sp-composite-control-height: initial;\n    --sp-composite-control-padding-block-end: initial;\n    --sp-composite-control-padding-block-start: initial;\n    --sp-composite-control-padding-inline-end: initial;\n    --sp-composite-control-padding-inline-start: initial;\n  }\n\n  .sp-base-input-group:has(> .sp-input-group-addon[data-align=\"inline-start\"]) {\n    --sp-composite-control-padding-inline-start: calc(var(--spacing) * 1.5);\n  }\n\n  .sp-base-input-group:has(> .sp-input-group-addon[data-align=\"inline-end\"]) {\n    --sp-composite-control-padding-inline-end: calc(var(--spacing) * 1.5);\n  }\n\n  .sp-base-input-group:has(\n    > .sp-input-group-addon[data-align=\"block-start\"],\n    > .sp-input-group-addon[data-align=\"block-end\"]\n  ) {\n    --sp-composite-control-height: auto;\n  }\n\n  .sp-base-input-group:has(> .sp-input-group-addon[data-align=\"block-start\"]) {\n    --sp-composite-control-padding-block-end: calc(var(--spacing) * 1.5);\n  }\n\n  .sp-base-input-group:has(> .sp-input-group-addon[data-align=\"block-end\"]) {\n    --sp-composite-control-padding-block-start: calc(var(--spacing) * 1.5);\n  }\n\n  /* insetEnd: the end of the control stays clear for an action placed over\n   * it, such as a square Button 8 px inside the top-end corner of a textarea.\n   * The text ends 8 px before that Button, which takes the control size the\n   * attribute names. The end signal carries the room to the Input and\n   * Textarea profiles. */\n  .sp-base-input-group[data-inset-end] {\n    --input-group-inset-end: calc(\n      var(--sp-rw-control-lg) + var(--sp-rw-space-4)\n    );\n    --sp-composite-control-padding-inline-end: var(--input-group-inset-end);\n  }\n\n  .sp-base-input-group[data-inset-end=\"sm\"] {\n    --input-group-inset-end: calc(\n      var(--sp-rw-control-sm) + var(--sp-rw-space-4)\n    );\n  }\n\n  .sp-base-input-group[data-inset-end=\"md\"] {\n    --input-group-inset-end: calc(\n      var(--sp-rw-control-md) + var(--sp-rw-space-4)\n    );\n  }\n\n  .sp-base-input-group[data-inset-end=\"xl\"] {\n    --input-group-inset-end: calc(\n      var(--sp-rw-control-xl) + var(--sp-rw-space-4)\n    );\n  }\n\n  .dark .sp-base-input-group[data-variant],\n  [data-theme=\"dark\"] .sp-base-input-group[data-variant] {\n    --input-group-surface-subtle: rgb(255 255 255 / 4%);\n    --sp-semantic-color-background: color-mix(\n      in srgb,\n      var(--color-neutral-950) 95%,\n      var(--color-white)\n    );\n    --sp-semantic-color-border: rgb(255 255 255 / 6%);\n    --sp-semantic-color-destructive: color-mix(\n      in srgb,\n      var(--color-red-500) 90%,\n      var(--color-white)\n    );\n    --sp-semantic-color-foreground: var(--color-neutral-100);\n    --sp-semantic-color-input: rgb(255 255 255 / 8%);\n    --sp-semantic-color-muted-foreground: color-mix(\n      in srgb,\n      var(--color-neutral-500) 90%,\n      var(--color-white)\n    );\n    --sp-semantic-color-ring: var(--color-neutral-500);\n  }\n\n  .sp-base-input-group[data-variant][data-variant=\"soft\"] {\n    background-color: var(--input-group-surface-subtle);\n  }\n\n  .sp-base-input-group[data-variant][data-variant=\"soft\"]:focus-within {\n    background-color: var(--sp-semantic-color-background);\n  }\n\n  /* Disabled with its own control, not with a disabled action inside it\n   * (design-rework.css, the field disabled rule). */\n  .sp-base-input-group[data-variant]:has(\n      :is(input:not([type=\"hidden\"]), textarea):disabled,\n      [role=\"spinbutton\"][aria-disabled=\"true\"]\n    ) {\n    background-color: var(--input-group-surface-subtle);\n  }\n\n  .sp-base-input-group[data-variant]:has(:autofill) {\n    background-color: color-mix(\n      in oklab,\n      var(--sp-semantic-color-foreground) 4%,\n      transparent\n    );\n  }\n\n  :is(.dark, [data-theme=\"dark\"])\n    .sp-base-input-group[data-variant]:has(:autofill) {\n    background-color: color-mix(\n      in oklab,\n      var(--sp-semantic-color-foreground) 8%,\n      transparent\n    );\n  }\n\n  .sp-base-input-group:has(.sp-input-segment, .sp-base-date-field) {\n    cursor: text;\n  }\n\n  .sp-input-segment > input {\n    background-clip: content-box;\n    border-radius: calc(\n      var(--radius-md) + var(--input-segment-padding-block)\n    ) !important;\n    caret-color: var(--color-primary);\n    cursor: text;\n    padding-block: var(--input-segment-padding-block) !important;\n    padding-inline: var(--input-segment-padding-inline) !important;\n    text-align: center;\n  }\n\n  .sp-input-segment > input:focus {\n    background-color: var(--input-segment-background);\n  }\n\n  .sp-input-segment > input[data-segment-width=\"2-digit\"] {\n    flex: none !important;\n    width: calc(var(--spacing) * 8) !important;\n  }\n\n  .sp-input-segment > input[data-segment-width=\"4-digit\"] {\n    flex: none !important;\n    width: calc(var(--spacing) * 12) !important;\n  }\n\n  .sp-input-segment > input[data-segment-width=\"fill\"] {\n    flex: 1 1 0% !important;\n    width: auto !important;\n  }\n\n  .sp-input-segment > input[data-segment-inset-start=\"true\"] {\n    margin-inline-start: calc(var(--spacing) * 2);\n  }\n\n  .sp-input-segment > input::selection {\n    background-color: transparent;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/input-group/input-group.css"},{"path":"packages/ui/src/components/input-group/index.ts","content":"export * from \"./input-group\";\n","type":"registry:ui","target":"~/src/siteplane/components/input-group/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/input-group.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid for a plain input with a label; use Input.","Avoid for joined buttons without a text field; use Group."],"description":"Composite for related Input and Textarea controls, add-ons, segments, and focus routing.","knownLimitations":["Addon text is not a label or a description; connect them with Label, Field or aria-describedby.","The group manages no values, does not disable its children and neither validates nor submits."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["input-group","input","group"],"useWhen":["Use to attach text, icons, buttons or other addons to an Input or Textarea so that they share one surface and focus ring."]}}