{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cascader",
  "title": "Cascader",
  "description": "Pick from nested options: columns on wide screens, one level at a time on phones, with search, full keyboard, lazy children and the path shown in the trigger.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json",
    "https://miseui.junsu4621.workers.dev/r/popover.json",
    "https://miseui.junsu4621.workers.dev/r/spinner.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/cascader.tsx",
      "content": "import type * as React from \"react\"\nimport { useId, useRef, useState } from \"react\"\nimport { ChevronsUpDown } from \"lucide-react\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/registry/miseui/ui/popover\"\nimport { INLINE_FRAME_CLASS, TRIGGER_BASE_CLASS, TRIGGER_CLASS, type CascaderEffect, type CascaderLayout, type CascaderVariant } from \"@/registry/miseui/ui/cascader-classes\"\nimport { ColumnsView, StepsView } from \"@/registry/miseui/ui/cascader-columns\"\nimport { labelsFor, type CascaderOption, type CascaderPath } from \"@/registry/miseui/ui/cascader-math\"\nimport { SearchBox } from \"@/registry/miseui/ui/cascader-search\"\nimport { useCascaderState, useContainerNarrow, useViewportNarrow, type CascaderState, type LoadChildren } from \"@/registry/miseui/ui/cascader-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface CascaderBaseProps extends Omit<React.ComponentProps<\"button\">, \"value\" | \"defaultValue\" | \"children\"> {\n  options: readonly CascaderOption[]\n  /** `effect` slides between levels on phones, along the direction you moved. */\n  effect?: CascaderEffect\n  /** `auto` shows columns on wide screens and steps on phones. */\n  layout?: CascaderLayout\n  placeholder?: string\n  searchable?: boolean\n  searchPlaceholder?: string\n  emptyText?: string\n  loadingText?: string\n  errorText?: string\n  backLabel?: string\n  /** Fetches the children of an option marked `lazy` the first time it opens. A rejected promise shows a retry. */\n  loadChildren?: LoadChildren\n  /** Posts each chosen leaf value with the surrounding form. */\n  name?: string\n}\n\ntype CascaderSingleProps = CascaderBaseProps & {\n  /** `plain` a field with a popover, `inline` the panel in the page, `compact` a tighter field and narrower columns, `multiple` tick many. */\n  variant?: \"plain\" | \"inline\" | \"compact\"\n  value?: CascaderPath | null\n  defaultValue?: CascaderPath | null\n  onValueChange?: (path: string[] | null) => void\n}\n\ntype CascaderMultipleProps = CascaderBaseProps & {\n  variant: \"multiple\"\n  value?: readonly CascaderPath[]\n  defaultValue?: readonly CascaderPath[]\n  onValueChange?: (paths: string[][]) => void\n}\n\ntype CascaderProps = CascaderSingleProps | CascaderMultipleProps\n\nconst toList = (props: CascaderProps, value: CascaderProps[\"value\"]) => {\n  if (value === null || value === undefined) return []\n  return props.variant === \"multiple\" ? (value as readonly CascaderPath[]) : [value as CascaderPath]\n}\n\nfunction useConfiguredState(props: CascaderProps, inline: boolean) {\n  const multiple = props.variant === \"multiple\"\n  return useCascaderState({\n    options: props.options,\n    loadChildren: props.loadChildren,\n    inline,\n    multiple,\n    controlled: props.value === undefined ? undefined : toList(props, props.value),\n    initial: toList(props, props.defaultValue),\n    emit: (next) => {\n      if (props.variant === \"multiple\") props.onValueChange?.(next.map((path) => [...path]))\n      else props.onValueChange?.(next[0] ? [...next[0]] : null)\n    },\n  })\n}\n\n/** Parents give way before the leaf: the last part is what was chosen, so it is the last to be cut. */\nfunction PathText({ labels, placeholder }: { labels: readonly string[]; placeholder: string }) {\n  if (labels.length === 0) return <span className=\"min-w-0 flex-1 truncate text-muted-foreground\">{placeholder}</span>\n  const parents = labels.slice(0, -1).join(\" / \")\n  return (\n    <span data-slot=\"cascader-path\" className=\"flex min-w-0 flex-1 items-baseline gap-1\">\n      {parents ? <span className=\"min-w-0 truncate text-muted-foreground\">{parents} /</span> : null}\n      <span className=\"max-w-full shrink-0 truncate\">{labels[labels.length - 1]}</span>\n    </span>\n  )\n}\n\nfunction triggerLabels(state: CascaderState) {\n  const [first, ...rest] = state.selected\n  if (!first) return []\n  const labels = labelsFor(state.resolved, first)\n  if (rest.length === 0) return labels\n  // Several picks: the leaves are what tell them apart, and the count says the rest exist.\n  return [`${labels[labels.length - 1]} +${rest.length}`]\n}\n\ninterface PanelProps {\n  state: CascaderState\n  props: CascaderProps\n  variant: CascaderVariant\n  stepped: boolean\n  label: string\n  inputRef: React.RefObject<HTMLInputElement | null>\n}\n\nfunction CascaderPanel({ state, props, variant, stepped, label, inputRef }: PanelProps) {\n  const { effect, searchable = true, searchPlaceholder = \"Search\", emptyText = \"No results.\", loadingText = \"Loading\", errorText = \"Could not load. Try again\", backLabel = \"Back\" } = props\n  const shared = { state, variant, label, searchable, loadingText, errorText, focusSearch: () => inputRef.current?.focus() }\n  const searching = searchable && state.query !== \"\"\n  return (\n    <>\n      {searchable ? <SearchBox state={state} variant={variant} placeholder={searchPlaceholder} emptyText={emptyText} inputRef={inputRef} /> : null}\n      {searching ? null : stepped ? <StepsView {...shared} slide={effect === \"slide\"} backLabel={backLabel} /> : <ColumnsView {...shared} />}\n    </>\n  )\n}\n\nfunction usesSteps(layout: CascaderLayout, narrow: boolean) {\n  if (layout === \"auto\") return narrow\n  return layout === \"steps\"\n}\n\nfunction HiddenFields({ name, selected }: { name?: string; selected: readonly CascaderPath[] }) {\n  if (!name) return null\n  return selected.map((path) => <input key={path.join(\"/\")} type=\"hidden\" name={name} value={path[path.length - 1]} />)\n}\n\nfunction InlineCascader({ props, state, label, className }: { props: CascaderProps; state: CascaderState; label: string; className?: string }) {\n  const inputRef = useRef<HTMLInputElement>(null)\n  const [frame, setFrame] = useState<HTMLDivElement | null>(null)\n  const narrow = useContainerNarrow(frame)\n  const labels = labelsFor(state.resolved, state.selected[0] ?? [])\n  return (\n    <div data-slot=\"cascader\" data-variant=\"inline\" data-effect={props.effect} className={cn(\"grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2\", className)}>\n      <HiddenFields name={props.name} selected={state.selected} />\n      <p aria-live=\"polite\" className=\"flex min-h-5 min-w-0 text-[13px] leading-5\">\n        <PathText labels={labels} placeholder={props.placeholder ?? \"Nothing chosen yet\"} />\n      </p>\n      <div ref={setFrame} className={INLINE_FRAME_CLASS}>\n        <CascaderPanel state={state} props={props} variant=\"inline\" stepped={usesSteps(props.layout ?? \"auto\", narrow)} label={label} inputRef={inputRef} />\n      </div>\n    </div>\n  )\n}\n\nfunction PopoverCascader({ props, state, variant, label, trigger }: { props: CascaderProps; state: CascaderState; variant: Exclude<CascaderVariant, \"inline\">; label: string; trigger: React.ComponentProps<\"button\"> }) {\n  const inputRef = useRef<HTMLInputElement>(null)\n  const narrow = useViewportNarrow()\n  const stepped = usesSteps(props.layout ?? \"auto\", narrow)\n  const searchable = props.searchable ?? true\n  const valueId = useId()\n  const labels = triggerLabels(state)\n  const { \"aria-label\": ariaLabel, \"aria-labelledby\": labelledBy, className: triggerClass, ...rest } = trigger\n  // A name replaces a button's content, so the chosen path has to be added to it or it is never announced.\n  const named = ariaLabel && labels.length > 0 ? `${ariaLabel}, ${labels.join(\", \")}` : ariaLabel\n  const labelled = labelledBy && labels.length > 0 ? `${labelledBy} ${valueId}` : labelledBy\n  return (\n    <div data-slot=\"cascader\" data-variant={variant} data-effect={props.effect} className={cn(\"w-full min-w-0\", props.className)}>\n      <HiddenFields name={props.name} selected={state.selected} />\n      <Popover open={state.open} onOpenChange={state.setOpen}>\n        <PopoverTrigger data-slot=\"cascader-trigger\" aria-label={named} aria-labelledby={labelled} className={cn(TRIGGER_BASE_CLASS, TRIGGER_CLASS[variant], triggerClass)} {...rest}>\n          <span id={valueId} className=\"flex min-w-0 flex-1\">\n            <PathText labels={labels} placeholder={props.placeholder ?? \"Select\"} />\n          </span>\n          <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 opacity-50\" />\n        </PopoverTrigger>\n        <PopoverContent\n          align=\"start\"\n          aria-label={label}\n          className={cn(\"max-w-(--radix-popover-content-available-width) overflow-hidden p-0\", stepped ? \"w-(--radix-popover-trigger-width) min-w-64\" : \"w-auto\")}\n          onEscapeKeyDown={(event) => {\n            // Radix listens on the document in the capture phase, ahead of the box, so the first Escape is caught here.\n            if (state.query === \"\") return\n            event.preventDefault()\n            state.setQuery(\"\")\n          }}\n          onOpenAutoFocus={(event) => {\n            event.preventDefault()\n            // On a phone, focusing the box would raise the keyboard over the list people opened this to see.\n            if (searchable && !stepped) inputRef.current?.focus()\n            // Read from the panel itself: the tree in state only arrives with the next render.\n            else (event.target as HTMLElement).querySelector<HTMLElement>(\"[role='treeitem'][tabindex='0']\")?.focus()\n          }}\n        >\n          <CascaderPanel state={state} props={props} variant={variant} stepped={stepped} label={label} inputRef={inputRef} />\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}\n\nconst OWN_PROPS: ReadonlySet<string> = new Set([\n  \"options\", \"variant\", \"effect\", \"layout\", \"placeholder\", \"searchable\", \"searchPlaceholder\", \"emptyText\", \"loadingText\",\n  \"errorText\", \"backLabel\", \"loadChildren\", \"name\", \"value\", \"defaultValue\", \"onValueChange\", \"className\",\n])\n\nfunction Cascader(props: CascaderProps) {\n  const variant = props.variant ?? \"plain\"\n  const state = useConfiguredState(props, variant === \"inline\")\n  const label = props[\"aria-label\"] ?? props.placeholder ?? \"Options\"\n  if (variant === \"inline\") return <InlineCascader props={props} state={state} label={label} className={props.className} />\n  const trigger = Object.fromEntries(Object.entries(props).filter(([key]) => !OWN_PROPS.has(key))) as React.ComponentProps<\"button\">\n  return <PopoverCascader props={props} state={state} variant={variant} label={label} trigger={trigger} />\n}\n\nexport { Cascader, type CascaderProps, type CascaderOption, type CascaderPath, type CascaderVariant, type CascaderEffect, type CascaderLayout, type LoadChildren }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-classes.ts",
      "content": "type CascaderVariant = \"plain\" | \"inline\" | \"compact\" | \"multiple\"\ntype CascaderEffect = \"slide\"\ntype CascaderLayout = \"auto\" | \"columns\" | \"steps\"\n\nconst TRIGGER_CLASS: Record<Exclude<CascaderVariant, \"inline\">, string> = {\n  plain: \"h-9 px-3 text-sm pointer-coarse:h-10\",\n  compact: \"h-8 px-2 text-[13px] pointer-coarse:h-10\",\n  multiple: \"h-9 px-3 text-sm pointer-coarse:h-10\",\n}\n\nconst TRIGGER_BASE_CLASS = [\n  \"flex w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-transparent text-left shadow-xs\",\n  \"transition-[color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-mise)\",\n  \"outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n  \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50\",\n]\n\n/** Fixed widths, so a column keeps its place while the one beside it opens. They share nothing: the panel scrolls sideways if it must. */\nconst COLUMN_WIDTH_CLASS: Record<CascaderVariant, string> = {\n  plain: \"w-40\",\n  inline: \"w-40\",\n  compact: \"w-32\",\n  multiple: \"w-44\",\n}\n\nconst ROW_BASE_CLASS = [\n  \"relative flex cursor-default items-center gap-2 rounded-sm outline-none select-none\",\n  \"transition-[color,background-color] duration-(--duration-fast) ease-(--ease-mise)\",\n  \"hover:bg-accent/60 focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-ring/60 data-[active=true]:bg-accent\",\n  \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n]\n\nconst ROW_CLASS: Record<CascaderVariant, string> = {\n  plain: \"px-2 py-1.5 text-sm pointer-coarse:py-2.5\",\n  inline: \"px-2 py-1.5 text-sm pointer-coarse:py-2.5\",\n  compact: \"px-2 py-1 text-[13px] pointer-coarse:py-2.5\",\n  multiple: \"px-2 py-1.5 text-sm pointer-coarse:py-2.5\",\n}\n\n/** Tall enough to show that a list scrolls, short enough to stay on a small laptop screen. */\nconst LIST_HEIGHT_CLASS = \"max-h-72 overflow-y-auto\"\n\nconst INLINE_FRAME_CLASS = \"grid min-w-0 grid-cols-[minmax(0,1fr)] rounded-md border bg-background\"\n\nexport {\n  COLUMN_WIDTH_CLASS,\n  INLINE_FRAME_CLASS,\n  LIST_HEIGHT_CLASS,\n  ROW_BASE_CLASS,\n  ROW_CLASS,\n  TRIGGER_BASE_CLASS,\n  TRIGGER_CLASS,\n  type CascaderEffect,\n  type CascaderLayout,\n  type CascaderVariant,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-columns.tsx",
      "content": "import { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ChevronLeft } from \"lucide-react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { COLUMN_WIDTH_CLASS, LIST_HEIGHT_CLASS, type CascaderVariant } from \"@/registry/miseui/ui/cascader-classes\"\nimport { columnsFor, firstEnabled, pathKey, type CascaderColumn } from \"@/registry/miseui/ui/cascader-math\"\nimport { CascaderRow } from \"@/registry/miseui/ui/cascader-row\"\nimport type { CascaderState } from \"@/registry/miseui/ui/cascader-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\n/** Far enough to read as a move between levels, close enough to stay one surface. Material's shared axis uses 30 px. */\nconst AXIS_OFFSET_PX = 24\nconst INSTANT = { duration: 0 } as const\n\n/**\n * Functions of `custom`, because an exiting element keeps the props of its last render: only AnimatePresence's\n * custom can tell it the direction that has just changed, or going back would leave the wrong way.\n */\nconst AXIS_VARIANTS = {\n  enter: (direction: number) => ({ x: direction * AXIS_OFFSET_PX, opacity: 0 }),\n  center: { x: 0, opacity: 1 },\n  exit: (direction: number) => ({ x: -direction * AXIS_OFFSET_PX, opacity: 0 }),\n}\nconst STILL_VARIANTS = { enter: { x: 0, opacity: 1 }, center: { x: 0, opacity: 1 }, exit: { x: 0, opacity: 1 } }\n\ninterface ViewProps {\n  state: CascaderState\n  variant: CascaderVariant\n  label: string\n  searchable: boolean\n  loadingText: string\n  errorText: string\n  focusSearch: () => void\n}\n\nfunction ColumnBody({ state, column, stepped, variant, searchable, loadingText, errorText, focusSearch }: ViewProps & { column: CascaderColumn; stepped: boolean }) {\n  const key = pathKey(column.parentPath)\n  if (column.parent && column.options.length === 0) {\n    const failed = state.failed.includes(key)\n    return (\n      <div className=\"px-2 py-1.5 text-[13px] text-muted-foreground\">\n        {failed ? (\n          <button\n            type=\"button\"\n            data-slot=\"cascader-retry\"\n            className=\"rounded-sm text-left underline underline-offset-4 outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 pointer-coarse:py-2.5\"\n            onClick={() => state.expand(column.parentPath, column.parent!, true)}\n          >\n            {errorText}\n          </button>\n        ) : (\n          <span role=\"status\">{state.loading.includes(key) ? loadingText : null}</span>\n        )}\n      </div>\n    )\n  }\n  const firstIndex = firstEnabled(column.options)\n  // Until something has focus, the tab stop is the first row of the first level on screen.\n  const holdsTabStop = !state.focusPath && (stepped || column.level === 0)\n  return column.options.map((option, index) => (\n    <CascaderRow\n      key={option.value}\n      state={state}\n      column={column}\n      option={option}\n      index={index}\n      variant={variant}\n      searchable={searchable}\n      stepped={stepped}\n      isFirstFocusable={holdsTabStop && index === firstIndex}\n      focusSearch={focusSearch}\n    />\n  ))\n}\n\nfunction ColumnsView(props: ViewProps) {\n  const { state, variant, label } = props\n  const { setTree } = state\n  const columns = columnsFor(state.resolved, state.activePath)\n  return (\n    <div ref={setTree} role=\"tree\" aria-label={label} aria-multiselectable={state.multiple || undefined} data-slot=\"cascader-tree\" data-layout=\"columns\" className=\"flex min-w-0 overflow-x-auto\">\n      {columns.map((column) => (\n        <div\n          key={column.level}\n          role=\"group\"\n          aria-label={column.parent?.label}\n          data-slot=\"cascader-column\"\n          className={cn(\"grid shrink-0 content-start gap-px border-r p-1 last:border-r-0\", COLUMN_WIDTH_CLASS[variant], LIST_HEIGHT_CLASS)}\n        >\n          <ColumnBody {...props} column={column} stepped={false} />\n        </div>\n      ))}\n    </div>\n  )\n}\n\nfunction StepsView(props: ViewProps & { slide: boolean; backLabel: string }) {\n  const { state, label, slide, backLabel } = props\n  const { setTree } = state\n  const reduceMotion = useReducedMotion() ?? false\n  const columns = columnsFor(state.resolved, state.activePath)\n  const column = columns[columns.length - 1]\n  const moving = slide && !reduceMotion\n  return (\n    <div data-slot=\"cascader-steps\" className=\"grid min-w-0 grid-cols-[minmax(0,1fr)] overflow-hidden\">\n      {column.parent ? (\n        <button\n          type=\"button\"\n          aria-label={`${backLabel}, ${column.parent.label}`}\n          data-slot=\"cascader-back\"\n          className=\"flex h-10 min-w-0 items-center gap-2 border-b px-2 text-left text-sm font-medium outline-none hover:bg-accent/60 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\"\n          onClick={() => state.collapseTo(column.level)}\n        >\n          <ChevronLeft aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n          <span className=\"min-w-0 truncate\">{column.parent.label}</span>\n        </button>\n      ) : null}\n      <div ref={setTree} role=\"tree\" aria-label={label} aria-multiselectable={state.multiple || undefined} data-slot=\"cascader-tree\" data-layout=\"steps\" className=\"relative min-w-0 overflow-hidden\">\n        <AnimatePresence initial={false} mode=\"popLayout\" custom={state.direction}>\n          <motion.div\n            key={pathKey(column.parentPath)}\n            role=\"group\"\n            aria-label={column.parent?.label}\n            data-slot=\"cascader-column\"\n            className={cn(\"grid content-start gap-px p-1\", LIST_HEIGHT_CLASS)}\n            custom={state.direction}\n            variants={moving ? AXIS_VARIANTS : STILL_VARIANTS}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n            transition={moving ? SPRING_DEFAULT : INSTANT}\n          >\n            <ColumnBody {...props} column={column} stepped />\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  )\n}\n\nexport { ColumnsView, StepsView }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-math.ts",
      "content": "interface CascaderOption {\n  value: string\n  label: string\n  disabled?: boolean\n  children?: readonly CascaderOption[]\n  /** Has children that are not here yet; `loadChildren` fetches them when this row is opened. */\n  lazy?: boolean\n}\n\ntype CascaderPath = readonly string[]\n\ntype CheckState = \"checked\" | \"mixed\" | \"unchecked\"\n\ninterface CascaderColumn {\n  level: number\n  parent: CascaderOption | null\n  parentPath: CascaderPath\n  options: readonly CascaderOption[]\n}\n\ninterface CascaderMatch {\n  path: CascaderPath\n  labels: readonly string[]\n}\n\nconst pathKey = (path: CascaderPath) => JSON.stringify(path)\nconst samePath = (a: CascaderPath, b: CascaderPath) => a.length === b.length && a.every((value, index) => value === b[index])\nconst isPrefix = (prefix: CascaderPath, path: CascaderPath) => prefix.length <= path.length && prefix.every((value, index) => value === path[index])\nconst isBranch = (option: CascaderOption) => Boolean(option.lazy) || (option.children?.length ?? 0) > 0\n\n/** Merges lazily loaded children into the tree, so everything else can treat the options as one whole. */\nfunction resolveOptions(options: readonly CascaderOption[], loaded: Readonly<Record<string, readonly CascaderOption[]>>, parent: CascaderPath = []): readonly CascaderOption[] {\n  return options.map((option) => {\n    const path = [...parent, option.value]\n    const children = option.children ?? loaded[pathKey(path)]\n    return children ? { ...option, children: resolveOptions(children, loaded, path) } : option\n  })\n}\n\n/** Walks as far as the path holds. A value that no longer exists stops the walk rather than breaking the panel. */\nfunction optionsOnPath(options: readonly CascaderOption[], path: CascaderPath) {\n  const found: CascaderOption[] = []\n  let level = options\n  for (const value of path) {\n    const option = level.find((candidate) => candidate.value === value)\n    if (!option) break\n    found.push(option)\n    level = option.children ?? []\n  }\n  return found\n}\n\n/** Before a lazy parent has loaded, its option is unknown, so the raw value stands in for its label. */\nfunction labelsFor(options: readonly CascaderOption[], path: CascaderPath) {\n  const found = optionsOnPath(options, path)\n  return path.map((value, index) => found[index]?.label ?? value)\n}\n\nfunction columnsFor(options: readonly CascaderOption[], activePath: CascaderPath): readonly CascaderColumn[] {\n  const root: CascaderColumn = { level: 0, parent: null, parentPath: [], options }\n  const branches = optionsOnPath(options, activePath).filter(isBranch)\n  return [\n    root,\n    ...branches.map((parent, index) => ({\n      level: index + 1,\n      parent,\n      parentPath: activePath.slice(0, index + 1),\n      options: parent.children ?? [],\n    })),\n  ]\n}\n\n/** Leaves under an option. An unloaded lazy option has none yet, so ticking it cannot claim children nobody has seen. */\nfunction leavesOf(option: CascaderOption, path: CascaderPath): readonly CascaderPath[] {\n  if (option.disabled) return []\n  if (option.children?.length) return option.children.flatMap((child) => leavesOf(child, [...path, child.value]))\n  return option.lazy ? [] : [path]\n}\n\nfunction checkStateOf(option: CascaderOption, path: CascaderPath, selected: readonly CascaderPath[]): CheckState {\n  const leaves = leavesOf(option, path)\n  if (leaves.length === 0) return \"unchecked\"\n  const chosen = leaves.filter((leaf) => selected.some((entry) => samePath(entry, leaf))).length\n  if (chosen === 0) return \"unchecked\"\n  return chosen === leaves.length ? \"checked\" : \"mixed\"\n}\n\n/** Ticks everything under the option, or unticks it if it was already all ticked, which is what a tri-state box promises. */\nfunction toggleLeaves(selected: readonly CascaderPath[], leaves: readonly CascaderPath[]) {\n  const without = selected.filter((entry) => !leaves.some((leaf) => samePath(entry, leaf)))\n  const allOn = leaves.length > 0 && without.length === selected.length - leaves.length\n  return allOn ? without : [...without, ...leaves]\n}\n\nfunction searchLeaves(options: readonly CascaderOption[], query: string): readonly CascaderMatch[] {\n  const terms = query.trim().toLowerCase().split(/\\s+/).filter(Boolean)\n  if (terms.length === 0) return []\n  const walk = (level: readonly CascaderOption[], path: CascaderPath, labels: readonly string[]): readonly CascaderMatch[] =>\n    level.flatMap((option) => {\n      if (option.disabled) return []\n      const nextPath = [...path, option.value]\n      const nextLabels = [...labels, option.label]\n      if (option.children?.length) return walk(option.children, nextPath, nextLabels)\n      if (option.lazy) return []\n      // Searches the whole path, so \"korea seoul\" finds Seoul under Korea and \"asia\" finds everything beneath it.\n      const text = nextLabels.join(\" \").toLowerCase()\n      return terms.every((term) => text.includes(term)) ? [{ path: nextPath, labels: nextLabels }] : []\n    })\n  return walk(options, [], [])\n}\n\n/** Next enabled row, or null at the end of the column: the list does not wrap, so the edge can be felt. */\nfunction enabledIndex(options: readonly CascaderOption[], from: number, step: 1 | -1) {\n  for (let index = from + step; index >= 0 && index < options.length; index += step) {\n    if (!options[index].disabled) return index\n  }\n  return null\n}\n\nconst firstEnabled = (options: readonly CascaderOption[]) => enabledIndex(options, -1, 1)\nconst lastEnabled = (options: readonly CascaderOption[]) => enabledIndex(options, options.length, -1)\n\ntype TreeKeyAction = \"next\" | \"previous\" | \"first\" | \"last\" | \"expand\" | \"collapse\" | \"activate\" | \"toggle\" | \"search\"\n\nfunction treeKeyAction(event: Pick<KeyboardEvent, \"key\" | \"ctrlKey\" | \"metaKey\" | \"altKey\">, multiple: boolean, searchable: boolean): TreeKeyAction | null {\n  switch (event.key) {\n    case \"ArrowDown\":\n      return \"next\"\n    case \"ArrowUp\":\n      return \"previous\"\n    case \"Home\":\n      return \"first\"\n    case \"End\":\n      return \"last\"\n    case \"ArrowRight\":\n      return \"expand\"\n    case \"ArrowLeft\":\n      return \"collapse\"\n    case \"Enter\":\n      return \"activate\"\n    case \" \":\n      return multiple ? \"toggle\" : \"activate\"\n    default:\n      // A plain character goes on to the search box, so nobody has to find it before they can type.\n      return searchable && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey ? \"search\" : null\n  }\n}\n\nexport {\n  checkStateOf,\n  columnsFor,\n  enabledIndex,\n  firstEnabled,\n  isBranch,\n  isPrefix,\n  labelsFor,\n  lastEnabled,\n  leavesOf,\n  optionsOnPath,\n  pathKey,\n  resolveOptions,\n  samePath,\n  searchLeaves,\n  toggleLeaves,\n  treeKeyAction,\n  type CascaderColumn,\n  type CascaderMatch,\n  type CascaderOption,\n  type CascaderPath,\n  type CheckState,\n  type TreeKeyAction,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-row.tsx",
      "content": "import type * as React from \"react\"\nimport { Check, ChevronRight, Minus } from \"lucide-react\"\nimport { ROW_BASE_CLASS, ROW_CLASS, type CascaderVariant } from \"@/registry/miseui/ui/cascader-classes\"\nimport {\n  checkStateOf,\n  enabledIndex,\n  firstEnabled,\n  isBranch,\n  isPrefix,\n  lastEnabled,\n  pathKey,\n  samePath,\n  treeKeyAction,\n  type CascaderColumn,\n  type CascaderOption,\n  type CascaderPath,\n  type CheckState,\n} from \"@/registry/miseui/ui/cascader-math\"\nimport type { CascaderState } from \"@/registry/miseui/ui/cascader-state\"\nimport { Spinner } from \"@/registry/miseui/ui/spinner\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nfunction CheckMark({ state, onToggle }: { state: CheckState; onToggle: (event: React.MouseEvent) => void }) {\n  return (\n    // A wider press area than the box: the row itself opens a level, so a near miss must not do that instead.\n    <span data-slot=\"cascader-check\" className=\"-m-2 flex size-8 shrink-0 items-center justify-center pointer-coarse:size-10\" onClick={onToggle}>\n      <span\n        className={cn(\n          \"flex size-4 items-center justify-center rounded-sm border border-input\",\n          state !== \"unchecked\" && \"border-mise bg-mise text-mise-foreground\",\n        )}\n      >\n        {state === \"checked\" ? <Check aria-hidden=\"true\" className=\"size-3\" /> : null}\n        {state === \"mixed\" ? <Minus aria-hidden=\"true\" className=\"size-3\" /> : null}\n      </span>\n    </span>\n  )\n}\n\ninterface RowProps {\n  state: CascaderState\n  column: CascaderColumn\n  option: CascaderOption\n  index: number\n  variant: CascaderVariant\n  searchable: boolean\n  stepped: boolean\n  isFirstFocusable: boolean\n  focusSearch: () => void\n}\n\nfunction useRowKeys({ state, column, option, index, searchable, focusSearch }: RowProps, path: CascaderPath) {\n  const go = (next: number | null) => {\n    if (next !== null) state.focusOn([...column.parentPath, column.options[next].value])\n  }\n  return (event: React.KeyboardEvent) => {\n    const action = treeKeyAction(event, state.multiple, searchable)\n    if (!action) return\n    // A search key must fall through, so the character lands in the box that is about to take focus.\n    if (action === \"search\") return focusSearch()\n    event.preventDefault()\n    if (action === \"next\") go(enabledIndex(column.options, index, 1))\n    if (action === \"previous\") go(enabledIndex(column.options, index, -1))\n    if (action === \"first\") go(firstEnabled(column.options))\n    if (action === \"last\") go(lastEnabled(column.options))\n    if (action === \"expand\") state.expand(path, option, true)\n    if (action === \"collapse\") state.collapseTo(column.level)\n    if (action === \"toggle\") state.pick(path, option)\n    if (action !== \"activate\") return\n    if (isBranch(option)) state.expand(path, option, true)\n    else state.pick(path, option)\n  }\n}\n\nfunction rowFacts({ state, column, option, isFirstFocusable }: RowProps) {\n  const path = [...column.parentPath, option.value]\n  const branch = isBranch(option)\n  return {\n    path,\n    branch,\n    expanded: branch && isPrefix(path, state.activePath),\n    check: state.multiple ? checkStateOf(option, path, state.selected) : null,\n    selected: !state.multiple && state.selected.some((entry) => samePath(entry, path)),\n    focused: state.focusPath ? samePath(state.focusPath, path) : isFirstFocusable,\n    isLoading: state.loading.includes(pathKey(path)),\n  }\n}\n\nfunction CascaderRow(props: RowProps) {\n  const { state, column, option, index, variant, stepped } = props\n  const { path, branch, expanded, check, selected, focused, isLoading } = rowFacts(props)\n  const onKeyDown = useRowKeys(props, path)\n  const ariaChecked = check === null ? undefined : check === \"mixed\" ? \"mixed\" : check === \"checked\"\n  return (\n    <div\n      role=\"treeitem\"\n      data-path={pathKey(path)}\n      data-active={expanded ? \"true\" : \"false\"}\n      aria-level={column.level + 1}\n      aria-setsize={column.options.length}\n      aria-posinset={index + 1}\n      aria-expanded={branch ? expanded : undefined}\n      aria-selected={state.multiple ? undefined : selected}\n      aria-checked={ariaChecked}\n      aria-busy={isLoading || undefined}\n      aria-disabled={option.disabled || undefined}\n      tabIndex={focused ? 0 : -1}\n      className={cn(ROW_BASE_CLASS, ROW_CLASS[variant])}\n      onClick={() => {\n        state.setFocusPath(path)\n        if (branch) state.expand(path, option, stepped)\n        else state.pick(path, option)\n      }}\n      onKeyDown={onKeyDown}\n    >\n      {check !== null ? (\n        <CheckMark\n          state={check}\n          onToggle={(event) => {\n            event.stopPropagation()\n            state.pick(path, option)\n          }}\n        />\n      ) : null}\n      <span className={cn(\"min-w-0 flex-1 wrap-anywhere\", selected && \"font-medium\")}>{option.label}</span>\n      {isLoading ? (\n        <span aria-hidden=\"true\" className=\"flex shrink-0\">\n          <Spinner size=\"sm\" label=\"\" />\n        </span>\n      ) : null}\n      {branch && !isLoading ? <ChevronRight aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" /> : null}\n      {selected ? <Check aria-hidden=\"true\" className=\"size-4 shrink-0\" /> : null}\n    </div>\n  )\n}\n\nexport { CascaderRow }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-search.tsx",
      "content": "import type * as React from \"react\"\nimport { useId, useState } from \"react\"\nimport { Check, Search } from \"lucide-react\"\nimport { ROW_BASE_CLASS, ROW_CLASS, LIST_HEIGHT_CLASS, type CascaderVariant } from \"@/registry/miseui/ui/cascader-classes\"\nimport { optionsOnPath, samePath, searchLeaves, type CascaderMatch } from \"@/registry/miseui/ui/cascader-math\"\nimport type { CascaderState } from \"@/registry/miseui/ui/cascader-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface SearchProps {\n  state: CascaderState\n  variant: CascaderVariant\n  placeholder: string\n  emptyText: string\n  inputRef: React.RefObject<HTMLInputElement | null>\n}\n\nfunction ResultRow({ match, id, active, selected, variant, onPick, onHover }: { match: CascaderMatch; id: string; active: boolean; selected: boolean; variant: CascaderVariant; onPick: () => void; onHover: () => void }) {\n  const parents = match.labels.slice(0, -1).join(\" / \")\n  return (\n    <div id={id} role=\"option\" aria-selected={selected} data-active={active ? \"true\" : \"false\"} className={cn(ROW_BASE_CLASS, ROW_CLASS[variant])} onClick={onPick} onPointerMove={onHover}>\n      <span className=\"min-w-0 flex-1 wrap-anywhere\">\n        {parents ? <span className=\"text-muted-foreground\">{parents} / </span> : null}\n        <span className={selected ? \"font-medium\" : undefined}>{match.labels[match.labels.length - 1]}</span>\n      </span>\n      {selected ? <Check aria-hidden=\"true\" className=\"size-4 shrink-0\" /> : null}\n    </div>\n  )\n}\n\nfunction useSearchKeys(state: CascaderState, results: readonly CascaderMatch[], activeIndex: number, setActiveIndex: (index: number) => void, pickMatch: (match: CascaderMatch) => void) {\n  return (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.nativeEvent.isComposing) return\n    if (event.key === \"Escape\" && state.query !== \"\") {\n      // The first Escape only empties the box, or a slip would throw away the whole panel.\n      event.preventDefault()\n      event.stopPropagation()\n      state.setQuery(\"\")\n      return\n    }\n    if (state.query === \"\") {\n      if (event.key === \"ArrowDown\") {\n        event.preventDefault()\n        const root = state.tree?.querySelector<HTMLElement>(\"[role='treeitem'][tabindex='0']\")\n        root?.focus()\n      }\n      return\n    }\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault()\n      const step = event.key === \"ArrowDown\" ? 1 : -1\n      setActiveIndex(Math.min(results.length - 1, Math.max(0, activeIndex + step)))\n    }\n    if (event.key === \"Enter\" && results[activeIndex]) {\n      event.preventDefault()\n      pickMatch(results[activeIndex])\n    }\n  }\n}\n\ninterface ResultsProps {\n  state: CascaderState\n  variant: CascaderVariant\n  listId: string\n  label: string\n  emptyText: string\n  results: readonly CascaderMatch[]\n  activeIndex: number\n  onPick: (match: CascaderMatch) => void\n  onHover: (index: number) => void\n}\n\nfunction ResultsList({ state, variant, listId, label, emptyText, results, activeIndex, onPick, onHover }: ResultsProps) {\n  return (\n    // Wider than one column, because a whole path has to fit where a single name did.\n    <div id={listId} role=\"listbox\" aria-label={label} aria-multiselectable={state.multiple || undefined} data-slot=\"cascader-results\" className={cn(\"grid min-w-64 gap-px p-1\", LIST_HEIGHT_CLASS)}>\n      {results.length === 0 ? <p className=\"px-2 py-2 text-[13px] text-muted-foreground\">{emptyText}</p> : null}\n      {results.map((match, index) => (\n        <ResultRow\n          key={match.path.join(\"/\")}\n          id={`${listId}-${index}`}\n          match={match}\n          variant={variant}\n          active={index === activeIndex}\n          selected={state.selected.some((entry) => samePath(entry, match.path))}\n          onPick={() => onPick(match)}\n          onHover={() => onHover(index)}\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction SearchBox({ state, variant, placeholder, emptyText, inputRef }: SearchProps) {\n  const listId = useId()\n  const [activeRaw, setActiveIndex] = useState(0)\n  const results = state.query === \"\" ? [] : searchLeaves(state.resolved, state.query)\n  const activeIndex = Math.min(activeRaw, Math.max(0, results.length - 1))\n  const pickMatch = (match: CascaderMatch) => {\n    const option = optionsOnPath(state.resolved, match.path).at(-1)\n    if (option) state.pick(match.path, option)\n  }\n  const onKeyDown = useSearchKeys(state, results, activeIndex, setActiveIndex, pickMatch)\n  return (\n    <>\n      <div className=\"flex items-center gap-2 border-b px-3\">\n        <Search aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        <input\n          ref={inputRef}\n          role=\"combobox\"\n          aria-label={placeholder}\n          aria-expanded={state.query !== \"\"}\n          aria-controls={state.query !== \"\" ? listId : undefined}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={results.length > 0 ? `${listId}-${activeIndex}` : undefined}\n          autoComplete=\"off\"\n          placeholder={placeholder}\n          value={state.query}\n          className=\"h-10 w-full min-w-24 bg-transparent text-base outline-none placeholder:text-muted-foreground md:text-sm\"\n          onChange={(event) => {\n            state.setQuery(event.target.value)\n            setActiveIndex(0)\n          }}\n          onKeyDown={onKeyDown}\n        />\n      </div>\n      {state.query === \"\" ? null : (\n        <ResultsList state={state} variant={variant} listId={listId} label={placeholder} emptyText={emptyText} results={results} activeIndex={activeIndex} onPick={pickMatch} onHover={setActiveIndex} />\n      )}\n    </>\n  )\n}\n\nexport { SearchBox }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/cascader-state.ts",
      "content": "import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\"\nimport {\n  isBranch,\n  firstEnabled,\n  leavesOf,\n  optionsOnPath,\n  pathKey,\n  resolveOptions,\n  samePath,\n  toggleLeaves,\n  type CascaderOption,\n  type CascaderPath,\n} from \"@/registry/miseui/ui/cascader-math\"\n\nconst NARROW_QUERY = \"(max-width: 639px)\"\n/** Below this an inline panel cannot hold two columns and a part of a third, so it steps instead. */\nconst INLINE_COLUMNS_MIN_PX = 440\n\nconst subscribeToViewport = (notify: () => void) => {\n  const query = window.matchMedia(NARROW_QUERY)\n  query.addEventListener(\"change\", notify)\n  return () => query.removeEventListener(\"change\", notify)\n}\n\nfunction useViewportNarrow() {\n  return useSyncExternalStore(subscribeToViewport, () => window.matchMedia(NARROW_QUERY).matches, () => false)\n}\n\n/** An inline panel lives in a column of the page, so its own width decides, while a popover can use the whole screen. */\nfunction useContainerNarrow(node: HTMLElement | null) {\n  const [narrow, setNarrow] = useState(false)\n  useEffect(() => {\n    if (!node || typeof ResizeObserver === \"undefined\") return\n    const observer = new ResizeObserver(([entry]) => setNarrow((entry?.contentRect.width ?? node.clientWidth) < INLINE_COLUMNS_MIN_PX))\n    observer.observe(node)\n    return () => observer.disconnect()\n  }, [node])\n  return narrow\n}\n\ntype LoadChildren = (path: CascaderPath, option: CascaderOption) => Promise<readonly CascaderOption[]>\n\ninterface LazyHandlers {\n  /** Called with the first enabled child's path when children arrive, for keyboard people who pressed to go in. */\n  onLoaded?: (path: CascaderPath, children: readonly CascaderOption[]) => void\n}\n\nfunction useLazyChildren(options: readonly CascaderOption[], loadChildren: LoadChildren | undefined, handlers: LazyHandlers) {\n  const [loaded, setLoaded] = useState<Readonly<Record<string, readonly CascaderOption[]>>>({})\n  const [loading, setLoading] = useState<readonly string[]>([])\n  const [failed, setFailed] = useState<readonly string[]>([])\n  const resolved = useMemo(() => resolveOptions(options, loaded), [options, loaded])\n  const load = (path: CascaderPath, option: CascaderOption) => {\n    const key = pathKey(path)\n    if (!loadChildren || loading.includes(key)) return\n    setLoading((previous) => [...previous, key])\n    setFailed((previous) => previous.filter((entry) => entry !== key))\n    loadChildren(path, option)\n      .then((children) => {\n        setLoaded((previous) => ({ ...previous, [key]: children }))\n        handlers.onLoaded?.(path, children)\n      })\n      .catch(() => setFailed((previous) => [...previous, key]))\n      .finally(() => setLoading((previous) => previous.filter((entry) => entry !== key)))\n  }\n  return { resolved, loading, failed, load }\n}\n\ninterface SelectionConfig {\n  multiple: boolean\n  controlled: readonly CascaderPath[] | undefined\n  initial: readonly CascaderPath[]\n  emit: (next: readonly CascaderPath[]) => void\n}\n\nfunction useSelection({ controlled, initial, emit }: SelectionConfig) {\n  const [uncontrolled, setUncontrolled] = useState(initial)\n  const selected = controlled ?? uncontrolled\n  const update = (next: readonly CascaderPath[]) => {\n    setUncontrolled(next)\n    emit(next)\n  }\n  return { selected, update }\n}\n\ninterface StateConfig extends SelectionConfig {\n  options: readonly CascaderOption[]\n  loadChildren?: LoadChildren\n  inline: boolean\n}\n\n/**\n * One tab stop, moved by hand. The tree may sit in a portal that mounts after the first layout effect, so the element\n * is kept in state through a callback ref, and the row is found again after each commit because levels come and go.\n */\nfunction useTreeFocus(activePath: CascaderPath) {\n  const [focusPath, setFocusPath] = useState<CascaderPath | null>(null)\n  const [tree, setTree] = useState<HTMLElement | null>(null)\n  const pendingFocus = useRef(false)\n  const focusOn = (path: CascaderPath) => {\n    pendingFocus.current = true\n    setFocusPath(path)\n  }\n  useEffect(() => {\n    if (!pendingFocus.current || !tree || !focusPath) return\n    const key = pathKey(focusPath)\n    const row = Array.from(tree.querySelectorAll<HTMLElement>(\"[role='treeitem']\")).find((entry) => entry.dataset.path === key)\n    if (!row) return\n    pendingFocus.current = false\n    row.focus()\n    row.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" })\n  }, [tree, focusPath, activePath])\n  return { focusPath, setFocusPath, focusOn, tree, setTree }\n}\n\nfunction useCascaderState(config: StateConfig) {\n  const { multiple, inline, loadChildren } = config\n  const [open, setOpenState] = useState(false)\n  const [query, setQuery] = useState(\"\")\n  const selection = useSelection(config)\n  const levels = useLevels(config.options, loadChildren, config.initial[0])\n  const { focus, lazy, setActivePath } = levels\n  const setOpen = (next: boolean) => {\n    setOpenState(next)\n    setQuery(\"\")\n    const chosen = next ? selection.selected[0] : undefined\n    if (next) setActivePath(parentOf(chosen))\n    // Tab lands on what was chosen, unless that option has since gone and no row could take the stop.\n    const exists = chosen !== undefined && optionsOnPath(lazy.resolved, chosen).length === chosen.length\n    focus.setFocusPath(exists ? chosen : null)\n  }\n  const pick = (path: CascaderPath, option: CascaderOption) => {\n    if (option.disabled) return\n    if (multiple) {\n      selection.update(toggleLeaves(selection.selected, leavesOf(option, path)))\n      return\n    }\n    setActivePath(parentOf(path))\n    selection.update(selection.selected[0] && samePath(selection.selected[0], path) && inline ? [] : [path])\n    if (!inline) setOpen(false)\n  }\n  const { activePath, direction, expand, collapseTo } = levels\n  return { ...selection, ...lazy, ...focus, open, setOpen, query, setQuery, activePath, direction, expand, collapseTo, pick, multiple }\n}\n\nfunction useLevels(options: readonly CascaderOption[], loadChildren: LoadChildren | undefined, initial: CascaderPath | undefined) {\n  const [activePath, setActivePath] = useState<CascaderPath>(() => parentOf(initial))\n  const [direction, setDirection] = useState<1 | -1>(1)\n  const descendInto = useRef<string | null>(null)\n  const focus = useTreeFocus(activePath)\n  const lazy = useLazyChildren(options, loadChildren, {\n    onLoaded: (path, children) => {\n      if (descendInto.current !== pathKey(path)) return\n      descendInto.current = null\n      const index = firstEnabled(children)\n      if (index !== null) focus.focusOn([...path, children[index].value])\n    },\n  })\n  /** `descend` moves focus into the opened level: always from the keyboard, and on phones, where the row pressed is about to leave the screen. */\n  const expand = (path: CascaderPath, option: CascaderOption, descend: boolean) => {\n    if (option.disabled || !isBranch(option)) return\n    setDirection(1)\n    setActivePath(path)\n    const children = optionsOnPath(lazy.resolved, path).at(-1)?.children\n    if (!children) {\n      if (descend) descendInto.current = pathKey(path)\n      lazy.load(path, option)\n      return\n    }\n    const index = firstEnabled(children)\n    if (descend && index !== null) focus.focusOn([...path, children[index].value])\n  }\n  const collapseTo = (level: number) => {\n    if (level <= 0) return\n    const parent = activePath.slice(0, level)\n    setDirection(-1)\n    setActivePath(parent.slice(0, level - 1))\n    focus.focusOn(parent)\n  }\n  return { activePath, setActivePath, direction, focus, lazy, expand, collapseTo }\n}\n\nfunction parentOf(path: CascaderPath | undefined): CascaderPath {\n  return path ? path.slice(0, -1) : []\n}\n\ntype CascaderState = ReturnType<typeof useCascaderState>\n\nexport { useCascaderState, useContainerNarrow, useViewportNarrow, type CascaderState, type LoadChildren }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}