{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "shape-context", "title": "Shape Context", "description": "React context provider for runtime shape variant switching (pill or rounded). Controls border-radius across all Zeron Design components.", "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json" ], "files": [ { "path": "src/system/shape-context.tsx", "content": "\"use client\";\n\nimport {\n createContext,\n useContext,\n useState,\n useEffect,\n useRef,\n useCallback,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport {\n shapeTokenClasses,\n shapeTokenValues,\n type GeneratedShapeVariant,\n} from \"./design-tokens\";\n\ntype ShapeVariant = GeneratedShapeVariant;\n\ninterface ShapeClasses {\n item: string;\n bg: string;\n focusRing: string;\n mergedBg: string;\n container: string;\n button: string;\n input: string;\n // Numeric counterparts of `bg` / `mergedBg`, in px. Needed where individual\n // corners are animated (e.g. the selected-background merge/split animation),\n // which requires per-corner numeric border-radii rather than a class.\n bgRadius: number;\n mergedRadius: number;\n}\n\nconst shapeMap: Record = {\n pill: {\n item: shapeTokenClasses.pill.control,\n bg: shapeTokenClasses.pill.control,\n // +2px over `item` because the focus ring sits 2px outside the element\n // (top/left -2, width/height +4); this keeps the corners concentric so a\n // pill element gets a pill ring (matches the rounded-mode 8px→10px bump).\n focusRing: shapeTokenClasses.pill.focus,\n mergedBg: shapeTokenClasses.pill.selection,\n container: shapeTokenClasses.pill.container,\n button: shapeTokenClasses.pill.control,\n input: shapeTokenClasses.pill.control,\n bgRadius: shapeTokenValues.pill.control,\n mergedRadius: shapeTokenValues.pill.selection,\n },\n rounded: {\n item: shapeTokenClasses.rounded.control,\n bg: shapeTokenClasses.rounded.control,\n focusRing: shapeTokenClasses.rounded.focus,\n mergedBg: shapeTokenClasses.rounded.selection,\n container: shapeTokenClasses.rounded.container,\n button: shapeTokenClasses.rounded.control,\n input: shapeTokenClasses.rounded.control,\n bgRadius: shapeTokenValues.rounded.control,\n mergedRadius: shapeTokenValues.rounded.selection,\n },\n};\n\ninterface ShapeContextValue {\n shape: ShapeVariant;\n setShape: (shape: ShapeVariant) => void;\n classes: ShapeClasses;\n}\n\nconst ShapeContext = createContext(null);\n\nfunction useShape(): ShapeClasses {\n const ctx = useContext(ShapeContext);\n if (!ctx) return shapeMap.pill;\n return ctx.classes;\n}\n\nfunction useShapeContext() {\n const ctx = useContext(ShapeContext);\n if (!ctx) throw new Error(\"useShapeContext must be used within a ShapeProvider\");\n return ctx;\n}\n\nfunction ShapeProvider({\n children,\n defaultShape = \"pill\",\n}: {\n children: ReactNode;\n defaultShape?: ShapeVariant;\n}) {\n const [shape, setShapeState] = useState(defaultShape);\n const transitionTimeoutRef = useRef | null>(null);\n\n // Run a state change under the `.transitioning` guard (added + reflow-flushed\n // first so the 180ms border-radius cross-fade applies). Clearing the previous\n // timeout first keeps a double-press from removing the class mid-fade.\n const transitionShape = useCallback((callback: () => void) => {\n const root = document.documentElement;\n root.classList.add(\"transitioning\");\n void root.offsetHeight;\n callback();\n if (transitionTimeoutRef.current) clearTimeout(transitionTimeoutRef.current);\n transitionTimeoutRef.current = setTimeout(\n () => root.classList.remove(\"transitioning\"),\n 200\n );\n }, []);\n\n const setShape = useCallback(\n (next: ShapeVariant) => {\n transitionShape(() => setShapeState(next));\n },\n [transitionShape]\n );\n\n // Publish every semantic radius on so plain CSS and portalled content\n // stay in sync with the React shape context.\n useEffect(() => {\n const root = document.documentElement;\n for (const [role, value] of Object.entries(shapeTokenValues[shape])) {\n root.style.setProperty(`--${role}-radius`, `${value}px`);\n }\n // Backward-compatible alias used by the base focus fallback.\n root.style.setProperty(\"--shape-input-radius\", `${shapeMap[shape].bgRadius}px`);\n }, [shape]);\n\n const value = useMemo(\n () => ({ shape, setShape, classes: shapeMap[shape] }),\n [shape, setShape]\n );\n\n return (\n \n {children}\n \n );\n}\n\nexport { ShapeProvider, useShape, useShapeContext, shapeMap };\nexport type { ShapeVariant, ShapeClasses };\n", "type": "registry:lib", "target": "lib/shape-context.tsx" }, { "path": "src/system/design-tokens.ts", "content": "// Generated from src/system/tokens/semantic-tokens.mjs. Do not edit directly.\n\nexport const shapeTokenValues = {\n \"rounded\": {\n \"control\": 8,\n \"focus\": 10,\n \"selection\": 8,\n \"container\": 12,\n \"full\": 9999\n },\n \"pill\": {\n \"control\": 20,\n \"focus\": 22,\n \"selection\": 16,\n \"container\": 24,\n \"full\": 9999\n }\n} as const;\n\nexport const shapeTokenClasses = {\n \"rounded\": {\n \"control\": \"rounded-[8px]\",\n \"focus\": \"rounded-[10px]\",\n \"selection\": \"rounded-[8px]\",\n \"container\": \"rounded-[12px]\",\n \"full\": \"rounded-[9999px]\"\n },\n \"pill\": {\n \"control\": \"rounded-[20px]\",\n \"focus\": \"rounded-[22px]\",\n \"selection\": \"rounded-[16px]\",\n \"container\": \"rounded-[24px]\",\n \"full\": \"rounded-[9999px]\"\n }\n} as const;\n\nexport type GeneratedShapeVariant = keyof typeof shapeTokenValues;\n", "type": "registry:lib", "target": "lib/design-tokens.ts" } ], "type": "registry:lib" }