{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "type": "registry:ui",
  "title": "Select",
  "description": "A compact choice field with a keyboard friendly menu.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "@radix-ui/react-select",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/select/select.tsx",
      "type": "registry:component",
      "target": "@components/xzero/select/select.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { Variants } from \"motion/react\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./select.module.css\";\n\nexport interface SelectProps extends Omit<ComponentPropsWithoutRef<typeof SelectPrimitive.Root>, \"children\"> {\n  label: string;\n  description?: string;\n  placeholder?: string;\n  id?: string;\n  className?: string;\n  options: { value: string; label: string; disabled?: boolean }[];\n}\n\n/** The shown value rolls in the direction of the list: a later option rises from below, an earlier one drops from above. */\nconst valueRoll: Variants = {\n  enter: (direction: number) => ({ opacity: 0, y: `${direction * 0.35}em`, filter: `blur(${motionTokens.blur.soft}px)` }),\n  center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } },\n  exit: (direction: number) => ({ opacity: 0, y: `${direction * -0.3}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }),\n};\n/** Reduced motion keeps a short crossfade; the resting state matches valueRoll so server and client markup agree. */\nconst valueFade: Variants = { enter: { opacity: 0 }, center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };\n\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n  { label, description, placeholder = \"Select an option\", options, id, className, disabled, onValueChange, ...rootProps },\n  ref,\n) {\n  const generatedId = useId();\n  const controlId = id ?? generatedId;\n  const hintId = description ? `${controlId}-description` : undefined;\n  const reduceMotion = useReducedMotion();\n  const [uncontrolledValue, setUncontrolledValue] = useState(rootProps.defaultValue ?? \"\");\n  const currentValue = rootProps.value ?? uncontrolledValue;\n  const index = options.findIndex((option) => option.value === currentValue);\n  const shown = currentValue ? options[index]?.label ?? \"\" : placeholder;\n  const [previousIndex, setPreviousIndex] = useState(index);\n  const [direction, setDirection] = useState(1);\n  if (previousIndex !== index) { setPreviousIndex(index); setDirection(index > previousIndex ? 1 : -1); }\n\n  return (\n    <div className={styles.field}>\n      <label htmlFor={controlId}>{label}</label>\n      <SelectPrimitive.Root {...rootProps} disabled={disabled} onValueChange={(next) => { setUncontrolledValue(next); onValueChange?.(next); }}>\n        <SelectPrimitive.Trigger\n          ref={ref}\n          id={controlId}\n          aria-describedby={hintId}\n          className={[styles.trigger, className].filter(Boolean).join(\" \")}\n        >\n          {/* Radix keeps the real value for assistive tech; the visible copy below animates between values. */}\n          <span className={styles.srOnly}><SelectPrimitive.Value placeholder={placeholder} /></span>\n          <span className={styles.valueText} aria-hidden=\"true\">\n            <AnimatePresence initial={false} custom={direction}>\n              <motion.span key={currentValue ? `value-${currentValue}` : \"placeholder\"} data-placeholder={currentValue ? undefined : \"\"} custom={direction} variants={reduceMotion ? valueFade : valueRoll} initial=\"enter\" animate=\"center\" exit=\"exit\">{shown}</motion.span>\n            </AnimatePresence>\n          </span>\n          <SelectPrimitive.Icon className={styles.chevron}>\n            <ChevronDown size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n          </SelectPrimitive.Icon>\n        </SelectPrimitive.Trigger>\n        <SelectPrimitive.Portal>\n          <SelectPrimitive.Content className={styles.content} position=\"popper\" sideOffset={4} collisionPadding={12}>\n            <SelectPrimitive.ScrollUpButton className={styles.scrollButton}>\n              <ChevronUp size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n            </SelectPrimitive.ScrollUpButton>\n            <SelectPrimitive.Viewport className={styles.viewport}>\n              {options.map((option) => (\n                <SelectPrimitive.Item key={option.value} value={option.value} disabled={option.disabled} className={styles.item}>\n                  <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n                  <SelectPrimitive.ItemIndicator className={styles.indicator}>\n                    <Check size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </SelectPrimitive.ItemIndicator>\n                </SelectPrimitive.Item>\n              ))}\n            </SelectPrimitive.Viewport>\n            <SelectPrimitive.ScrollDownButton className={styles.scrollButton}>\n              <ChevronDown size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n            </SelectPrimitive.ScrollDownButton>\n          </SelectPrimitive.Content>\n        </SelectPrimitive.Portal>\n      </SelectPrimitive.Root>\n      {description && <span id={hintId} className={styles.hint}>{description}</span>}\n    </div>\n  );\n});\n"
    },
    {
      "path": "registry/components/select/select.module.css",
      "type": "registry:component",
      "target": "@components/xzero/select/select.module.css",
      "content": ".field { display: grid; gap: var(--space-2); min-width: 0; }\n.field label { font-size: var(--text-sm); font-weight: 500; }\n/* The trigger anchors the floating menu. Radix opens on pointerdown and measures this box, so it never scales: press feedback is color only. */\n.trigger { position: relative; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); width: 100%; min-height: var(--control-height-md); border: 1px solid var(--border); border-radius: var(--radius-control); padding: 0 var(--space-3); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }\n@media (hover: hover) and (pointer: fine) { .trigger:hover:not([data-disabled]) { border-color: var(--border-strong); background: var(--surface-muted); } }\n.trigger:active:not([data-disabled]), .trigger[data-state=\"open\"] { border-color: var(--border-strong); background: var(--surface-muted); }\n.trigger:focus-visible { border-color: var(--border-strong); }\n.trigger[data-disabled] { opacity: .5; cursor: not-allowed; }\n.valueText { display: grid; flex: 1; min-width: 0; grid-template-columns: minmax(0, 1fr); }\n.valueText > span { grid-area: 1 / 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.valueText > [data-placeholder] { color: var(--text-muted); }\n.srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }\n.chevron { display: inline-flex; flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }\n.trigger[data-state=\"open\"] .chevron { transform: rotate(180deg); }\n.hint { color: var(--text-muted); font-size: var(--text-xs); }\n/* Radix holds the animation until the menu is positioned, so it always grows from the trigger edge. */\n.content { --menu-x: 0px; --menu-y: -6px; box-sizing: border-box; z-index: 1000; width: var(--radix-select-trigger-width); min-width: var(--radix-select-trigger-width); max-width: min(24rem, calc(100vw - 20px)); max-height: min(320px, var(--radix-select-content-available-height)); overflow: hidden; border: 1px solid var(--border); border-radius: calc(var(--radius-control) + 2px); padding: 6px; background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-select-content-transform-origin); animation: select-in var(--duration-standard) var(--ease-enter) both; }\n.content[data-side=\"top\"] { --menu-y: 6px; }\n.content[data-side=\"left\"] { --menu-x: 6px; --menu-y: 0px; }\n.content[data-side=\"right\"] { --menu-x: -6px; --menu-y: 0px; }\n.content[data-state=\"closed\"] { animation: select-out var(--duration-instant) var(--ease-standard) both; }\n.viewport { padding: 2px 0; }\n.item { position: relative; display: flex; align-items: center; min-height: 36px; border-radius: calc(var(--radius-control) - 4px); padding: 0 34px 0 11px; color: var(--foreground); font-size: var(--text-sm); cursor: default; outline: none; user-select: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard); }\n.item[data-highlighted] { background: var(--surface-muted); }\n.item[data-disabled] { opacity: .45; }\n.indicator { position: absolute; right: 10px; display: inline-flex; align-items: center; color: var(--foreground); animation: indicator-in var(--duration-standard) var(--ease-enter) 40ms both; }\n.scrollButton { display: grid; height: 28px; place-items: center; color: var(--text-muted); }\n@keyframes select-in { from { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); } }\n@keyframes select-out { to { opacity: 0; transform: translate(calc(var(--menu-x) / 2), calc(var(--menu-y) / 2)) scale(.98); } }\n@keyframes indicator-in { from { opacity: 0; transform: scale(.6); filter: blur(2px); } }\n@media (prefers-reduced-transparency: reduce) { .content { background: var(--surface-raised); backdrop-filter: none; -webkit-backdrop-filter: none; } }\n@media (prefers-reduced-motion: reduce) { .trigger, .chevron, .item { transition: none; } .content { animation: select-fade var(--duration-instant) var(--ease-standard) both; } .content[data-state=\"closed\"] { animation: select-fade-out var(--duration-instant) var(--ease-standard) both; } .indicator { animation: none; } }\n@keyframes select-fade { from { opacity: 0; } }\n@keyframes select-fade-out { to { opacity: 0; } }\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/select",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "field",
      "form"
    ],
    "docs": "https://ui.xzero.co.uk/components/select",
    "markdown": "https://ui.xzero.co.uk/components/select/markdown"
  }
}