{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "person-cue",
  "type": "registry:ui",
  "title": "Person cue",
  "description": "An exact source name opens a host-provided avatar/name picker with a fixed footprint and one confirmed document transaction.",
  "dependencies": [
    "@base-ui/react@^1.8.0"
  ],
  "registryDependencies": [
    "https://metalui.dev/r/colorway.json",
    "https://metalui.dev/r/icons.json",
    "https://metalui.dev/r/mark.json",
    "https://metalui.dev/r/menu.json",
    "https://metalui.dev/r/motion.json",
    "https://metalui.dev/r/tick-glyph.json",
    "https://metalui.dev/r/tokens.json",
    "https://metalui.dev/r/tooltip.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/person-cue/person-cue.tsx",
      "type": "registry:ui",
      "target": "components/metalui/components/person-cue/person-cue.tsx",
      "content": "'use client';\nimport * as React from 'react';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { Select as BaseSelect } from '@base-ui/react/select';\nimport { Icon } from '../../icons/Icon';\nimport { TickGlyph } from '../../icons/TickGlyph';\nimport { SwapText } from '../../motion/swap';\nimport { usePortalColorway } from '../../theme/portal-colorway';\nimport { Tooltip } from '../tooltip/tooltip';\nimport { Mark, useReadingLine } from '../mark/mark';\nimport { MARK_GLYPH_SIZE } from '../mark/identity.generated';\nimport { ListGlide, menuParts } from '../menu/menu';\n\nexport interface PersonCueChoice {\n  /** Exact name written into the document. Identity and recognition belong to the host. */\n  value: string;\n  label?: string;\n  /** A host-provided Avatar Object, or another meaning glyph. The control never creates a person. */\n  avatar?: React.ReactNode;\n  disabled?: boolean;\n}\nexport interface PersonCueProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange' | 'children'> {\n  value: string;\n  choices: readonly PersonCueChoice[];\n  label: string;\n  disabled?: boolean;\n  readOnly?: boolean;\n  raw?: boolean;\n  /** Suppress the visual first-use hint when an enclosing provenance tooltip owns help. */\n  hint?: boolean;\n  editing?: boolean;\n  className?: string;\n  /** Props reach the actual operable trigger, including provenance descriptions and focus events. */\n  triggerProps?: Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'value' | 'disabled' | 'className'>;\n  onBegin?: () => boolean | void;\n  onChange: (words: string) => boolean | void;\n  onCommit?: () => void;\n  onCancel?: () => void;\n}\n\n/* PERSON CUE STORYBOARD\n * rest    exact source name; all host names reserve its footprint; host avatar is a Mark meaning slot\n * open    one source snapshot; Base Select opens the shared names plate and owns focus/typeahead\n * choose  one confirmed name replaces the range and commits once; the existing drum changes words\n * cancel  Escape/outside/unmount/vocabulary change ends the snapshot, with no invented identity\n * Reduced motion keeps the donor plate fade and the drum crossfade. No clock runs at rest.\n */\nfunction offset() {\n  if (typeof window === 'undefined') return 6;\n  return parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--mu-r-select-pop-offset')) || 6;\n}\nexport const PersonCue = React.forwardRef<HTMLButtonElement, PersonCueProps>(function PersonCue(props, ref) {\n  const reading = useReadingLine();\n  const { value, choices, label, disabled = false, readOnly = false, raw = false, hint = true, className, triggerProps, editing: _editing, onBegin: _onBegin, onChange: _onChange, onCommit: _onCommit, onCancel: _onCancel, ...triggerEvents } = props;\n  const [open, setOpen] = React.useState(false);\n  const [hintOpen, setHintOpen] = React.useState(false);\n  const [anchor, setAnchor] = React.useState<HTMLButtonElement | null>(null);\n  const colorway = usePortalColorway(anchor);\n  const callbacks = React.useRef(props); callbacks.current = props;\n  const transaction = React.useRef<{ original: string; vocabulary: string } | null>(null);\n  const vocabulary = JSON.stringify(choices.map(choice => [choice.value, !!choice.disabled]));\n  const valid = choices.length > 0 && choices.every(choice => !!choice.value) && new Set(choices.map(choice => choice.value)).size === choices.length;\n  const mutable = valid && !disabled && !readOnly && choices.some(choice => !choice.disabled);\n  const current = choices.find(choice => choice.value === value);\n  function cancel(render = true) {\n    const held = transaction.current; if (!held) return;\n    transaction.current = null;\n    if (render) setOpen(false);\n    callbacks.current.onCancel?.();\n  }\n  const latestCancel = React.useRef(cancel); latestCancel.current = cancel;\n  React.useEffect(() => {\n    const held = transaction.current;\n    if (held && (!mutable || held.original !== value || held.vocabulary !== vocabulary || props.editing === false)) latestCancel.current();\n  }, [mutable, value, vocabulary, props.editing]);\n  React.useEffect(() => () => latestCancel.current(false), []);\n  const triggerRef = React.useCallback((element: HTMLButtonElement | null) => {\n    setAnchor(element);\n    if (typeof ref === 'function') ref(element); else if (ref) ref.current = element;\n  }, [ref]);\n  const glyph = (choice?: PersonCueChoice) => choice?.avatar ?? <Icon name=\"person\" size={MARK_GLYPH_SIZE} />;\n  const own = 'mu-person-cue person-cue relative inline-grid align-baseline border-0 bg-transparent p-0 type-content text-ink select-none focus-visible:focus-ring data-disabled:opacity-field-state-disabled';\n  const names = [...new Set([...choices.map(choice => choice.value), value])];\n  const description = [...new Set([\"Choose a known person. Arrows or type a name, Return chooses, Escape cancels.\", triggerEvents[\"aria-description\"], triggerProps?.[\"aria-description\"]].filter(Boolean))].join(\" \");\n  return <BaseSelect.Root<string> value={value} open={open} disabled={disabled || !valid}\n    onOpenChange={next => {\n      if (!next) { cancel(); setOpen(false); return; }\n      if (!mutable || callbacks.current.onBegin?.() === false) return;\n      transaction.current = { original: value, vocabulary }; setOpen(true);\n    }}\n    onValueChange={next => {\n      if (!transaction.current || next == null || !choices.some(choice => choice.value === next && !choice.disabled)) return;\n      if (next !== value && callbacks.current.onChange(next) === false) { cancel(); return; }\n      transaction.current = null; setOpen(false); callbacks.current.onCommit?.();\n    }}>\n    <BaseSelect.Trigger {...mergeProps({ onFocus: () => setHintOpen(true), onBlur: () => setHintOpen(false) }, triggerEvents, triggerProps ?? {})} ref={triggerRef} aria-label={`${label}: ${value}`} aria-readonly={readOnly || undefined}\n      data-value={value} data-readonly={readOnly || undefined} className={className ? `${own} ${className}` : own} aria-description={description}>\n      {!reading && names.map(name => <span key={name} aria-hidden className=\"invisible col-start-1 row-start-1 whitespace-nowrap\">{name}</span>)}\n      <Tooltip label=\"Choose a known person · Arrows or type a name\" open={hint && hintOpen && !open && mutable} onOpenChange={next => setHintOpen(next)}><span className=\"col-start-1 row-start-1 justify-self-start\"><Mark kind=\"duration\" meaning=\"person\" meaningLabel={value} meaningGlyph={glyph(current)} raw={raw}><SwapText value={value} /></Mark></span></Tooltip>\n    </BaseSelect.Trigger>\n    <BaseSelect.Portal>\n      <BaseSelect.Positioner data-mu-colorway={colorway} className=\"mu-menu-positioner z-menu-z\" align=\"start\" sideOffset={offset()}>\n        <BaseSelect.Popup className={`${menuParts.PLATE} relative`} aria-label={label}>\n          <ListGlide />\n          {choices.map(choice => <BaseSelect.Item key={choice.value} value={choice.value} disabled={choice.disabled} aria-label={choice.label && choice.label !== choice.value ? `${choice.label} (${choice.value})` : choice.value} className={menuParts.LIVE_ROW}>\n            <span aria-hidden className={menuParts.GLYPH}>{glyph(choice)}</span>\n            <BaseSelect.ItemText className={menuParts.LABEL}>{choice.label ?? choice.value}</BaseSelect.ItemText>\n            <BaseSelect.ItemIndicator keepMounted render={(indicatorProps, state) => <TickGlyph {...indicatorProps as React.SVGProps<SVGSVGElement>} mark={state.selected ? 'tick' : null} className=\"mu-select-mark select-mark\" />} />\n          </BaseSelect.Item>)}\n        </BaseSelect.Popup>\n      </BaseSelect.Positioner>\n    </BaseSelect.Portal>\n  </BaseSelect.Root>;\n});\n"
    },
    {
      "path": "packages/metalui/src/components/person-cue/person-cue.css",
      "type": "registry:file",
      "target": "components/metalui/components/person-cue/person-cue.css",
      "content": "/* PersonCue composes Mark's quiet underline/meaning slot, the Enum fixed-footprint layout and\n   Select/Menu's plate, rows, glide and pen. Source names and host avatars have no new material. */\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/person-cue.md. SwiftUI: MetalPersonCue, MetalPersonCueChoice in the MetalUI Swift package."
}
