{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"list-box","title":"List Box","description":"React Aria single-selection list with check or row-fill appearance, keyboard navigation and rich options.","dependencies":["react-aria-components@1.20.0"],"registryDependencies":["@siteplane/base","@siteplane/button"],"files":[{"path":"packages/ui/src/components/list-box/list-box.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport {\n  ListBox as ListBoxPrimitive,\n  ListBoxItem as ListBoxItemPrimitive,\n  type Selection,\n} from \"react-aria-components\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\nimport { Button, type ButtonBaseProps } from \"../button\";\n\nimport \"./list-box.css\";\n\nexport type ListBoxSelectionAppearance = \"check\" | \"row\";\n\ntype ListBoxVisualContextValue = {\n  selectionAppearance: ListBoxSelectionAppearance;\n  size: SiteplaneControlSize;\n};\n\nconst ListBoxVisualContext =\n  React.createContext<ListBoxVisualContextValue | null>(null);\n\nfunction getSelectionValue(selection: Selection): string | undefined {\n  if (selection === \"all\") return undefined;\n  const value = selection.values().next().value;\n  return value === undefined ? undefined : String(value);\n}\n\nfunction getTextValue(children: React.ReactNode): string {\n  return React.Children.toArray(children)\n    .map((child) =>\n      typeof child === \"string\" || typeof child === \"number\"\n        ? String(child)\n        : React.isValidElement<{ children?: React.ReactNode }>(child)\n          ? getTextValue(child.props.children)\n          : \"\",\n    )\n    .join(\" \")\n    .trim();\n}\n\nexport interface ListBoxProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"aria-orientation\" | \"defaultValue\" | \"role\"\n> {\n  defaultValue?: string | undefined;\n  onValueChange?: ((value: string) => void) | undefined;\n  selectionAppearance?: ListBoxSelectionAppearance | undefined;\n  size?: SiteplaneControlSize | undefined;\n  value?: string | undefined;\n}\n\nexport function ListBox({\n  children,\n  className,\n  defaultValue,\n  onValueChange,\n  selectionAppearance = \"check\",\n  size,\n  value,\n  ...props\n}: ListBoxProps): React.ReactElement {\n  const controlled = value !== undefined;\n  const resolvedSize = useResolvedControlSize(size);\n  const visualContext = React.useMemo(\n    () => ({ selectionAppearance, size: resolvedSize }),\n    [resolvedSize, selectionAppearance],\n  );\n\n  return (\n    <ListBoxVisualContext.Provider value={visualContext}>\n      <ListBoxPrimitive\n        {...(props as React.ComponentProps<typeof ListBoxPrimitive>)}\n        className={cn(\"sp-list-box sp-base-list-box\", className)}\n        data-foundation=\"list-box\"\n        data-selection-appearance={selectionAppearance}\n        data-size={resolvedSize}\n        data-slot=\"list-box\"\n        {...(controlled || defaultValue === undefined\n          ? {}\n          : { defaultSelectedKeys: [defaultValue] })}\n        disallowEmptySelection\n        onSelectionChange={(selection) => {\n          const nextValue = getSelectionValue(selection);\n          if (nextValue !== undefined) onValueChange?.(nextValue);\n        }}\n        orientation=\"vertical\"\n        {...(controlled ? { selectedKeys: [value] } : {})}\n        selectionBehavior=\"replace\"\n        selectionMode=\"single\"\n        shouldFocusWrap\n      >\n        {children}\n      </ListBoxPrimitive>\n    </ListBoxVisualContext.Provider>\n  );\n}\n\nexport interface ListBoxItemProps extends Omit<\n  ButtonBaseProps,\n  | \"align\"\n  | \"aria-disabled\"\n  | \"aria-selected\"\n  | \"onClick\"\n  | \"onKeyDown\"\n  | \"role\"\n  | \"render\"\n  | \"size\"\n  | \"tabIndex\"\n  | \"value\"\n  | \"variant\"\n  | \"width\"\n> {\n  \"data-slot\"?: string;\n  textValue?: string;\n  value: string;\n}\n\nexport function ListBoxItem({\n  \"data-slot\": dataSlot = \"list-box-item\",\n  children,\n  className,\n  disabled = false,\n  multiline = true,\n  textValue,\n  value,\n  ...props\n}: ListBoxItemProps): React.ReactElement {\n  const context = React.useContext(ListBoxVisualContext);\n\n  if (!context) {\n    throw new Error(\"ListBoxItem must be rendered inside ListBox.\");\n  }\n\n  return (\n    <Button\n      {...props}\n      align=\"start\"\n      className={cn(\n        \"group sp-list-box-item\",\n        // The row owns the selected state (list-box.css); its check draws\n        // through the shared check motion (motion.css), which reads that state\n        // from the row.\n        context.selectionAppearance === \"check\" && \"sp-check-item\",\n        className,\n      )}\n      data-selection-appearance={context.selectionAppearance}\n      data-slot={dataSlot}\n      disabled={disabled}\n      multiline={multiline}\n      render={\n        <ListBoxItemPrimitive\n          id={value}\n          isDisabled={disabled}\n          textValue={textValue ?? getTextValue(children)}\n        />\n      }\n      size={context.size}\n      variant=\"ghost\"\n      width=\"full\"\n    >\n      {/* The check leads the row, as in Select, Combobox and the menus (I8).\n          Every option keeps its slot, so every label starts at the same x. */}\n      {context.selectionAppearance === \"check\" ? (\n        <svg\n          aria-hidden=\"true\"\n          className=\"sp-list-box-check sp-check-glyph-motion size-3.5 shrink-0 fill-none stroke-current\"\n          data-slot=\"list-box-check\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2.25\"\n          viewBox=\"0 0 24 24\"\n        >\n          <path\n            className=\"sp-check-path-motion\"\n            d=\"M5.252 12.7 10.2 18.63 18.748 5.37\"\n          />\n        </svg>\n      ) : null}\n      {children}\n    </Button>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/list-box/list-box.tsx"},{"path":"packages/ui/src/components/list-box/list-box.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  .sp-list-box-item[data-chassis][data-size=\"sm\"] {\n    min-height: var(--sp-rw-control-sm);\n  }\n\n  .sp-list-box-item[data-chassis][data-size=\"md\"] {\n    min-height: var(--sp-rw-control-md);\n  }\n\n  .sp-list-box-item[data-chassis][data-size=\"lg\"] {\n    min-height: var(--sp-rw-control-lg);\n  }\n\n  .sp-list-box-item[data-chassis][data-size=\"xl\"] {\n    min-height: var(--sp-rw-control-xl);\n  }\n\n  /* The selected option: the row's own fill and text, set through the\n   * Button's fill and text, so a className on the item still restyles it. */\n  :root .sp-list-box-item[data-chassis][data-selected] {\n    --sp-rw-button-fill: var(--sp-semantic-color-secondary);\n    --sp-rw-button-text: var(--sp-semantic-color-secondary-foreground);\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/list-box/list-box.css"},{"path":"packages/ui/src/components/list-box/index.ts","content":"export * from \"./list-box\";\n","type":"registry:ui","target":"~/src/siteplane/components/list-box/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/list-box.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid when choices should stay collapsed until the user opens a Select or Combobox."],"description":"React Aria single-selection list with check or row-fill appearance, keyboard navigation and rich options.","knownLimitations":["The list submits no native form value; serialize the selected state yourself."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["list-box","selection","options"],"useWhen":["Use when a persistent visible list of options allows exactly one selection."]}}