{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-select","type":"registry:component","title":"Select (Motion)","description":"Select with motion: the list springs out of its trigger with its options rising in, one highlight glides between them, and a new value rises into the trigger.","dependencies":["lucide-react","motion","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-portal.json","https://dashral.com/r/motion-field.json","https://dashral.com/r/motion-hover.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/motion-tokens.json","https://dashral.com/r/field.json","https://dashral.com/r/select.json"],"files":[{"path":"registry/dashral/motion/select.tsx","type":"registry:file","target":"components/dashral/motion/select.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Select as SelectPrimitive } from \"radix-ui\"\nimport { ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\nimport { AnimatePresence, LayoutGroup, motion } from \"motion/react\"\nimport { FieldWrapper, splitFieldProps, useFieldControl } from \"../field\"\nimport type { SelectOption, SelectProps } from \"../select\"\nimport { cn } from \"../lib/utils\"\nimport { usePortalContainer } from \"../lib/portal\"\n\nimport { useHoverPill } from \"../lib/motion-hover\"\nimport { motionMessages } from \"../lib/motion-field\"\nimport { useReducedMotion } from \"../lib/motion-reduced\"\nimport { duration, ease, spring } from \"../lib/motion-tokens\"\n\nexport type { SelectProps, SelectOption, SelectOptionGroup } from \"../select\"\n\ntype Option = { value: string; label: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode; disabled?: boolean }\n\nconst toOption = (option: SelectOption): Option =>\n  typeof option === \"string\" ? { value: option, label: option } : { ...option, label: option.label ?? option.value }\n\n/**\n * The kit's Select with motion: the same options, groups, placeholder, prefix, sizes and props. The list springs out\n * of its trigger, its options rising in one after another; the highlight is one pill gliding from option to option\n * after the pointer or the arrow keys; the picked option's tick draws itself; the chevron turns as it opens; and a new\n * value rises into the trigger over the old one. It leaves quickly. With reduced motion it only fades.\n */\nexport function Select(props: SelectProps) {\n  const portalContainer = usePortalContainer()\n  const [\n    field,\n    { options, placeholder, prefix, size = \"default\", side = \"bottom\", id, className, classNames, \"aria-label\": ariaLabel, \"aria-invalid\": invalid, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, ...root },\n  ] = splitFieldProps(props)\n  const reduced = useReducedMotion()\n  const [own, setOwn] = React.useState(defaultValue ?? \"\")\n  const current = value ?? own\n  const [ownOpen, setOwnOpen] = React.useState(defaultOpen ?? false)\n  const isOpen = open ?? ownOpen\n  const group = React.useId()\n  const pill = useHoverPill<string>(reduced)\n\n  const flat = options.flatMap((entry) => (typeof entry === \"object\" && \"options\" in entry ? entry.options : [entry])).map(toOption)\n  const picked = flat.find((option) => option.value === current)\n  const from = reduced ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: side === \"top\" ? 6 : -6 }\n  let index = 0\n\n  const renderOption = (option: Option) => {\n    const at = index++\n    return (\n      <SelectPrimitive.Item\n        key={option.value}\n        value={option.value}\n        disabled={option.disabled}\n        data-slot=\"select-item\"\n        onFocus={() => pill.enter(option.value)}\n        onBlur={pill.leave}\n        className={cn(\n          \"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pe-8 ps-2 text-sm outline-hidden select-none focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n          classNames?.item\n        )}\n      >\n        {/* The highlight: one pill for the list, gliding from option to option. */}\n        {pill.hot === option.value && (\n          <motion.span\n            layoutId=\"hot\"\n            aria-hidden\n            className=\"absolute inset-0 -z-10 rounded-sm bg-accent\"\n            // Its opacity is the list's, so it doesn't start from nothing at each new option.\n            style={{ opacity: pill.opacity }}\n            transition={reduced ? { duration: 0 } : spring.move}\n          />\n        )}\n        <span className=\"absolute end-2 flex size-3.5 items-center justify-center\">\n          <svg viewBox=\"0 0 24 24\" aria-hidden className=\"size-4 fill-none stroke-current stroke-2 [stroke-linecap:round] [stroke-linejoin:round]\">\n            <motion.path\n              d=\"M4 12l5 5L20 6\"\n              initial={reduced ? false : { pathLength: 0 }}\n              animate={{ pathLength: current === option.value ? 1 : 0, opacity: current === option.value ? 1 : 0 }}\n              transition={{ pathLength: { duration: duration.standard, ease: ease.enter, delay: 0.08 }, opacity: { duration: 0.01 } }}\n            />\n          </svg>\n        </span>\n        <motion.span\n          className=\"flex min-w-0 items-center gap-2\"\n          initial={reduced ? false : { opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: duration.standard, ease: ease.enter, delay: Math.min(at, 8) * 0.025 }}\n        >\n          {option.description ? (\n            <span className=\"grid gap-0.5\">\n              <SelectPrimitive.ItemText>\n                <span className=\"flex items-center gap-2\">\n                  {option.icon}\n                  {option.label}\n                </span>\n              </SelectPrimitive.ItemText>\n              <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n            </span>\n          ) : (\n            <SelectPrimitive.ItemText>\n              <span className=\"flex items-center gap-2\">\n                {option.icon}\n                {option.label}\n              </span>\n            </SelectPrimitive.ItemText>\n          )}\n        </motion.span>\n      </SelectPrimitive.Item>\n    )\n  }\n\n  return (\n    <FieldWrapper {...field} renderMessages={motionMessages}>\n      <SelectPrimitive.Root\n        data-slot=\"select\"\n        value={current}\n        onValueChange={(next) => {\n          if (value === undefined) setOwn(next)\n          onValueChange?.(next)\n        }}\n        open={isOpen}\n        onOpenChange={(next) => {\n          if (open === undefined) setOwnOpen(next)\n          if (!next) pill.clear()\n          onOpenChange?.(next)\n        }}\n        {...root}\n      >\n        <SelectTrigger id={id} size={size} aria-label={ariaLabel} aria-invalid={invalid} open={isOpen} reduced={reduced} placeholder={!picked} className={cn(\"w-full\", className, classNames?.trigger)}>\n          {prefix && (\n            <span data-slot=\"select-prefix\" className=\"flex shrink-0 items-center text-muted-foreground\">\n              {prefix}\n            </span>\n          )}\n          {/* The value as shown: a new one rises in over the old. Radix's own Value stays for its bookkeeping, unseen. */}\n          <span className=\"relative grid min-w-0 flex-1 justify-items-start overflow-hidden\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.span\n                key={current || \"placeholder\"}\n                className=\"col-start-1 row-start-1 flex min-w-0 items-center gap-2 truncate\"\n                initial={reduced ? { opacity: 0 } : { opacity: 0, y: 8 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, transition: { duration: duration.fast, ease: ease.exit } }}\n                transition={{ duration: duration.standard, ease: ease.enter }}\n              >\n                {picked ? (\n                  <>\n                    {picked.icon}\n                    {picked.label}\n                  </>\n                ) : (\n                  placeholder\n                )}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n          <span className=\"sr-only\">\n            <SelectPrimitive.Value />\n          </span>\n        </SelectTrigger>\n        {/*\n          Closed, Radix still keeps the options (off screen) for the native select a form submits and for typing on the\n          trigger to pick; the animated list below is only there while it's open.\n        */}\n        {!isOpen && (\n          <SelectPrimitive.Content>\n            <SelectPrimitive.Viewport>\n              {flat.map((option) => (\n                <SelectPrimitive.Item key={option.value} value={option.value} disabled={option.disabled}>\n                  <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n                </SelectPrimitive.Item>\n              ))}\n            </SelectPrimitive.Viewport>\n          </SelectPrimitive.Content>\n        )}\n        <AnimatePresence>\n          {isOpen && (\n            <SelectPrimitive.Portal forceMount container={portalContainer}>\n              <SelectPrimitive.Content forceMount asChild position=\"popper\" side={side} sideOffset={4}>\n                <motion.div\n                  data-slot=\"select-content\"\n                  className={cn(\n                    \"relative isolate z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md\",\n                    classNames?.content\n                  )}\n                  style={{ transformOrigin: \"var(--radix-select-content-transform-origin)\" }}\n                  initial={from}\n                  animate={{ opacity: 1, scale: 1, y: 0 }}\n                  exit={{ ...from, transition: { duration: duration.fast, ease: ease.exit } }}\n                  transition={reduced ? { duration: duration.fast } : { ...spring.appear, opacity: { duration: duration.fast, ease: ease.enter } }}\n                >\n                  <SelectPrimitive.ScrollUpButton className=\"flex cursor-default items-center justify-center py-1\">\n                    <ChevronUpIcon className=\"size-4\" />\n                  </SelectPrimitive.ScrollUpButton>\n                  <LayoutGroup id={group}>\n                    <SelectPrimitive.Viewport className=\"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1 p-1\">\n                      {options.map((entry, i) =>\n                        typeof entry === \"object\" && \"options\" in entry ? (\n                          <React.Fragment key={i}>\n                            {i > 0 && <SelectPrimitive.Separator className=\"pointer-events-none -mx-1 my-1 h-px bg-border\" />}\n                            <SelectPrimitive.Group>\n                              {entry.label && <SelectPrimitive.Label className=\"px-2 py-1.5 text-xs text-muted-foreground\">{entry.label}</SelectPrimitive.Label>}\n                              {entry.options.map((option) => renderOption(toOption(option)))}\n                            </SelectPrimitive.Group>\n                          </React.Fragment>\n                        ) : (\n                          renderOption(toOption(entry))\n                        )\n                      )}\n                    </SelectPrimitive.Viewport>\n                  </LayoutGroup>\n                  <SelectPrimitive.ScrollDownButton className=\"flex cursor-default items-center justify-center py-1\">\n                    <ChevronDownIcon className=\"size-4\" />\n                  </SelectPrimitive.ScrollDownButton>\n                </motion.div>\n              </SelectPrimitive.Content>\n            </SelectPrimitive.Portal>\n          )}\n        </AnimatePresence>\n      </SelectPrimitive.Root>\n    </FieldWrapper>\n  )\n}\n\nfunction SelectTrigger({\n  className,\n  size,\n  open,\n  reduced,\n  placeholder,\n  children,\n  ...rest\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size: \"sm\" | \"default\"; open: boolean; reduced: boolean; placeholder: boolean }) {\n  // A button can't be required: the Select root takes that.\n  const { required: _required, ...props } = useFieldControl(rest as typeof rest & { required?: boolean })\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      data-placeholder={placeholder || undefined}\n      className={cn(\n        \"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/15 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/15 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      {/* The chevron turns over as the list opens, and back as it closes. */}\n      <motion.span className=\"flex opacity-50\" animate={{ rotate: open && !reduced ? 180 : 0 }} transition={reduced ? { duration: 0 } : spring.move}>\n        <ChevronDownIcon className=\"size-4\" />\n      </motion.span>\n    </SelectPrimitive.Trigger>\n  )\n}\n"}]}