import {
type ComponentPropsWithoutRef,
type ComponentRef,
type ElementType,
type ForwardRefExoticComponent,
type PropsWithoutRef,
type ReactElement,
type ReactNode,
type RefAttributes,
cloneElement,
forwardRef,
isValidElement,
} from "react";
import { Primitive as RadixPrimitive } from "@radix-ui/react-primitive";
/**
* Thin wrapper around `@radix-ui/react-primitive` that adds `render` prop support.
*
* When `render` is provided, it is converted to the equivalent `asChild` pattern:
* render={} + children → asChild + {children}
*
* All prop merging, ref composition, and event handler chaining remain handled
* by Radix's battle-tested Slot implementation — we add zero custom logic for that.
*/
// Match @radix-ui/react-primitive's full element set
const NODES = [
"a",
"button",
"div",
"form",
"h2",
"h3",
"img",
"input",
"label",
"li",
"nav",
"ol",
"p",
"select",
"span",
"svg",
"ul",
] as const;
type PrimitiveNode = (typeof NODES)[number];
type WithRenderPropProps =
ComponentPropsWithoutRef & {
render?: ReactElement | undefined;
};
type PrimitiveProps = WithRenderPropProps<
(typeof RadixPrimitive)[E]
>;
type WithRenderPropRuntimeProps =
WithRenderPropProps & {
asChild?: boolean | undefined;
children?: ReactNode | undefined;
};
type PrimitiveRef = ComponentRef<
(typeof RadixPrimitive)[E]
>;
function withRenderProp(Component: T) {
const Wrapped = forwardRef, WithRenderPropRuntimeProps>(
(
{
render,
asChild,
children,
...rest
}: PropsWithoutRef>,
ref,
) => {
const Comp = Component as any;
if (render && isValidElement(render)) {
const renderChildren =
children !== undefined
? children
: ((render.props as Record).children as ReactNode);
return (
{cloneElement(render, undefined, renderChildren)}
);
}
return (
{children}
);
},
);
const componentName =
typeof Component === "string"
? Component
: (Component.displayName ?? Component.name ?? "Component");
Wrapped.displayName = componentName;
return Wrapped as ForwardRefExoticComponent<
WithRenderPropProps & RefAttributes>
>;
}
function createPrimitive(node: E) {
const RadixComp = RadixPrimitive[node];
const Component = withRenderProp(RadixComp);
Component.displayName = `Primitive.${node}`;
return Component as ForwardRefExoticComponent<
PrimitiveProps & RefAttributes>
>;
}
const Primitive = NODES.reduce(
(acc, node) => {
acc[node] = createPrimitive(node);
return acc;
},
{} as {
[K in PrimitiveNode]: ReturnType>;
},
);
export { Primitive, withRenderProp };
export type { PrimitiveProps, WithRenderPropProps };