{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "colour-cue",
  "type": "registry:ui",
  "title": "Colour cue",
  "description": "A fixed-footprint six-digit colour word opens the shared hue well and rewrites source through one gesture transaction.",
  "dependencies": [
    "@base-ui/react@^1.8.0"
  ],
  "registryDependencies": [
    "https://metalui.dev/r/mark.json",
    "https://metalui.dev/r/motion.json",
    "https://metalui.dev/r/popover.json",
    "https://metalui.dev/r/slider.json",
    "https://metalui.dev/r/tokens.json",
    "https://metalui.dev/r/well.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/colour-cue/colour-cue.tsx",
      "type": "registry:ui",
      "target": "components/metalui/components/colour-cue/colour-cue.tsx",
      "content": "'use client';\nimport * as React from 'react';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { Button as BaseButton } from '@base-ui/react/button';\nimport { Mark } from '../mark/mark';\nimport { Popover } from '../popover/popover';\nimport { Slider } from '../slider/slider';\nimport { Well } from '../well/well';\nimport { SwapText } from '../../motion/swap';\nimport { haptic } from '../../motion/haptic';\nimport { useReducedMotion } from '../../motion/reduced';\n\nconst TURN = 360, CHANNEL = 255, SECTOR = 60;\nconst FOOTPRINT = '#' + 'F'.repeat(6);\n/** Colour math carries no material or timing decisions. Achromatic hue stays undefined. */\nfunction colourCueHSL(hex: string) {\n  if (!/^#[0-9a-f]{6}$/i.test(hex)) throw new TypeError('ColourCue needs a full six-digit hex value');\n  const rgb = [1, 3, 5].map(at => parseInt(hex.slice(at, at + 2), 16) / CHANNEL);\n  const [r, g, b] = rgb, hi = Math.max(...rgb), lo = Math.min(...rgb), delta = hi - lo, lightness = (hi + lo) / 2;\n  const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));\n  const hue = delta === 0 ? undefined : ((hi === r ? (g - b) / delta : hi === g ? (b - r) / delta + 2 : (r - g) / delta + 4) * SECTOR + TURN) % TURN;\n  return { hue, saturation, lightness };\n}\nfunction hueHex(hue: number, saturation: number, lightness: number) {\n  const c = (1 - Math.abs(2 * lightness - 1)) * saturation, h = ((hue % TURN) + TURN) % TURN / SECTOR;\n  const x = c * (1 - Math.abs(h % 2 - 1)), m = lightness - c / 2;\n  const rgb = h < 1 ? [c, x, 0] : h < 2 ? [x, c, 0] : h < 3 ? [0, c, x] : h < 4 ? [0, x, c] : h < 5 ? [x, 0, c] : [c, 0, x];\n  return '#' + rgb.map(v => Math.round((v + m) * CHANNEL).toString(16).padStart(2, '0')).join('').toUpperCase();\n}\nexport interface ColourCueProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseButton>, 'children' | 'value' | 'onChange' | 'render' | 'nativeButton'> {\n  value: string;\n  label: string;\n  onChange?: (hex: string) => void;\n  /** Return false when the host source transaction has gone stale. */\n  onSourceChange?: (words: string) => boolean | void;\n  onBegin?: () => boolean | void;\n  onCommit?: () => void;\n  onCancel?: (reason: 'escape' | 'external' | 'unmount') => void;\n  editing?: boolean;\n  readOnly?: boolean;\n  disabled?: boolean;\n  raw?: boolean;\n  className?: string;\n}\nconst ROOT = 'mu-colour-cue inline-grid relative align-baseline type-readout text-ink whitespace-nowrap p-0 bg-transparent border-none cursor-pointer focus-visible:focus-ring disabled:opacity-button-disabled';\nconst CELL = 'col-start-1 row-start-1';\n/** A colour Component: the text opens the shared well and hue Slider; the source owns history. */\nexport const ColourCue = React.forwardRef<HTMLElement, ColourCueProps>(function ColourCue({ value, label, onChange, onSourceChange, onBegin, onCommit, onCancel, editing, readOnly = false, disabled = false, raw = false, className, ...triggerProps }, forwardedRef) {\n  const hsl = colourCueHSL(value);\n  const [open, setOpen] = React.useState(false);\n  const [anchor, setAnchor] = React.useState<HTMLElement | null>(null);\n  React.useImperativeHandle(forwardedRef, () => anchor!, [anchor]);\n  const reduced = useReducedMotion(anchor);\n  const active = React.useRef<{ before: string; last: string; saturation: number; lightness: number } | null>(null);\n  const pointerHeld = React.useRef(false), stalePointer = React.useRef(false);\n  const latest = React.useRef({ onCancel }); latest.current = { onCancel };\n  const begin = () => { if (active.current) return true; if (readOnly || disabled || onBegin?.() === false) return false; active.current = { before: value, last: value, saturation: hsl.saturation, lightness: hsl.lightness }; return true; };\n  const commit = () => { if (!active.current) return; active.current = null; onCommit?.(); };\n  const cancel = () => {\n    const held = active.current; if (!held) return;\n    active.current = null; stalePointer.current = pointerHeld.current;\n    onSourceChange?.(held.before); onChange?.(held.before); onCancel?.('escape');\n  };\n  React.useEffect(() => {\n    if (active.current && (value !== active.current.last || editing === false || disabled || readOnly)) {\n      active.current = null; stalePointer.current = pointerHeld.current; onCancel?.('external');\n    }\n    if (disabled || readOnly) setOpen(false);\n  }, [value, editing, disabled, readOnly, onCancel]);\n  React.useEffect(() => () => { if (active.current) latest.current.onCancel?.('unmount'); }, []);\n  const change = (hue: number, keyboard: boolean) => {\n    if (readOnly || disabled || stalePointer.current) return;\n    const basis = active.current ?? hsl;\n    const next = hueHex(hue, basis.saturation, basis.lightness);\n    if (next === value) return;\n    if (!begin()) return;\n    if (onSourceChange?.(next) === false) { active.current = null; stalePointer.current = pointerHeld.current; onCancel?.('external'); return; }\n    onChange?.(next);\n    if (active.current) active.current.last = next;\n    haptic('detent');\n    if (keyboard) commit();\n  };\n  const instructions = readOnly ? 'Read only' : 'Open colour well; drag hue or use arrow keys';\n  const faceProps: React.ComponentPropsWithoutRef<typeof BaseButton> = {\n    disabled, 'aria-disabled': readOnly || undefined,\n    'aria-label': `${label}, ${value}${readOnly ? ', read only' : ''}`,\n    'aria-description': [instructions, triggerProps['aria-description']].filter(Boolean).join('. '),\n    className: className ? `${ROOT} ${className}` : ROOT,\n  };\n  const face = <BaseButton {...mergeProps(triggerProps, faceProps)} ref={setAnchor}>\n    <span aria-hidden className={`invisible ${CELL}`}>{FOOTPRINT}</span>\n    <Mark kind=\"hex\" meaning=\"colour\" meaningLabel={label} color={value} raw={raw} className={CELL}><SwapText value={value} /></Mark>\n  </BaseButton>;\n  if (readOnly || disabled) return face;\n  return <Popover open={open} onOpenChange={(next, details) => {\n    if (!next && details.reason === 'escape-key') cancel();\n    else if (!next) commit();\n    setOpen(next);\n  }}>\n    <Popover.Trigger>{face}</Popover.Trigger>\n    <Popover.Content data-mu-motion={reduced ? 'reduce' : undefined} aria-label={`${label} colour well`}>\n      <Popover.Title>{label}</Popover.Title>\n      <Popover.Description>Hue changes keep saturation and lightness. Escape cancels a held scrub.</Popover.Description>\n      <Popover.Body>\n        <Well variant=\"field\" radius=\"field\" className=\"mu-stack gap-mu-group p-mu-space-16\">\n          <Mark kind=\"hex\" meaning=\"colour\" meaningLabel={label} color={value} className=\"type-readout mt-mu-space-16\">{value}</Mark>\n          <Slider value={Math.min(hsl.hue ?? 0, TURN - 1)} min={0} max={TURN - 1} step={1} largeStep={15} tone=\"neutral\" aria-label={`${label} hue`} showValue\n            format={v => `${Math.round(v)}°`} onPointerDownCapture={event => { if (event.button === 0) { pointerHeld.current = true; stalePointer.current = !begin(); } }}\n            onPointerUpCapture={() => { queueMicrotask(() => { commit(); pointerHeld.current = false; stalePointer.current = false; }); }}\n            onPointerCancelCapture={() => { cancel(); pointerHeld.current = false; stalePointer.current = false; }}\n            onValueChange={(hue, details) => change(hue, details.reason === 'keyboard')}\n            onValueCommitted={commit} />\n          {hsl.hue === undefined && <span className=\"type-meta text-ink2\">This colour is achromatic; hue alone keeps it unchanged.</span>}\n        </Well>\n      </Popover.Body>\n    </Popover.Content>\n  </Popover>;\n});\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/colour-cue.md. SwiftUI: MetalColourCue in the MetalUI Swift package."
}
