{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"checkbox","type":"registry:ui","title":"Checkbox","description":"A tri-look checkbox — unchecked, checked, indeterminate and disabled — built on a native checkbox input","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["forms"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/checkbox/Checkbox.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useField } from \"../../internals/field\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport \"./checkbox.css\";\n\ninterface CheckboxOwnProps {\n\t/**\n\t * Whether the box is checked. Controlled when supplied: pair it with\n\t * `onCheckedChange`, the React counterpart of the Svelte source's\n\t * `bind:checked`. This is the real state underneath even while\n\t * `indeterminate` is true — a click always resolves to this value,\n\t * never to a third state.\n\t */\n\tchecked?: boolean;\n\t/** Initial checked state when uncontrolled (no `checked` prop given). */\n\tdefaultChecked?: boolean;\n\t/**\n\t * Mixed/dash visual state. A DOM property with no HTML attribute\n\t * equivalent, so it is assigned straight to the element and reapplied\n\t * whenever this prop changes, not only on mount. Any interaction that\n\t * changes `checked` clears it back to `false` — a controlled caller\n\t * that wants it to stay cleared simply stops re-passing `true`, which\n\t * it learns to do from `onCheckedChange`.\n\t */\n\tindeterminate?: boolean;\n\t/** Called with the new checked value whenever the box is activated. */\n\tonCheckedChange?: (checked: boolean) => void;\n\t/** Blocks interaction; excluded from form submission. Overridden by a surrounding FormField. */\n\tdisabled?: boolean;\n\t/** Native `required`. Overridden by a surrounding FormField. */\n\trequired?: boolean;\n\t/** Drives the error border and `aria-invalid`. Overridden by a surrounding FormField. */\n\tinvalid?: boolean;\n\t/** Element id. Overridden by a surrounding FormField's own `controlId`. */\n\tid?: string;\n\t/** Native `name`, read on form submission. */\n\tname?: string;\n\t/** Form value submitted while checked. */\n\tvalue?: string;\n\t/**\n\t * Accessible name, rendered as `aria-label`. Typically for a control\n\t * with no visible `children` text; also applies alongside `children`\n\t * that render no text of their own (e.g. an icon), since the two props\n\t * aren't mutually exclusive and there is no way to detect from here\n\t * whether an arbitrary node renders text. Skip this when `children`\n\t * already supplies the visible label text — passing both means\n\t * `aria-label` wins the accessible name and the visible text is\n\t * announced by nothing.\n\t */\n\tlabel?: string;\n\t/** Visible label text, rendered beside the box. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes, merged onto the wrapping `<label>`. */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off\n\t * by default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\nexport interface CheckboxProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"input\">, keyof CheckboxOwnProps | \"type\">,\n\t\tCheckboxOwnProps {}\n\n/**\n * A native `<input type=\"checkbox\">` with a drawn tick, a real indeterminate\n * state and full FormField integration.\n *\n * The element reference arrives through the ref channel (the Svelte source\n * declares `ref = $bindable(null)`), pointing at the native `<input>`.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(\n\t(\n\t\t{\n\t\t\tchecked: checkedProp,\n\t\t\tdefaultChecked = false,\n\t\t\tindeterminate: indeterminateProp = false,\n\t\t\tonCheckedChange,\n\t\t\tdisabled = false,\n\t\t\trequired = false,\n\t\t\tinvalid = false,\n\t\t\tid,\n\t\t\tname,\n\t\t\tvalue,\n\t\t\tlabel,\n\t\t\tchildren,\n\t\t\tclassName,\n\t\t\tsound = false,\n\t\t\tonChange,\n\t\t\t\"aria-label\": ariaLabel,\n\t\t\t\"aria-describedby\": ariaDescribedby,\n\t\t\t\"aria-invalid\": ariaInvalid,\n\t\t\t...rest\n\t\t},\n\t\tforwardedRef\n\t) => {\n\t\tconst isControlled = checkedProp !== undefined;\n\t\tconst [uncontrolledChecked, setUncontrolledChecked] = useState(defaultChecked);\n\t\tconst checked = isControlled ? checkedProp : uncontrolledChecked;\n\n\t\t// `indeterminate` mirrors the Svelte side's `$bindable`: the prop\n\t\t// seeds local state, a later prop CHANGE re-syncs it (the render-phase\n\t\t// adjustment below), and an interaction clears it locally regardless\n\t\t// of what the caller last passed.\n\t\tconst [indeterminate, setIndeterminate] = useState(indeterminateProp);\n\t\tconst prevIndeterminateProp = useRef(indeterminateProp);\n\t\tif (indeterminateProp !== prevIndeterminateProp.current) {\n\t\t\tprevIndeterminateProp.current = indeterminateProp;\n\t\t\tsetIndeterminate(indeterminateProp);\n\t\t}\n\n\t\t// Undefined outside a FormField — every fallback below then uses this\n\t\t// component's own props instead of the context, so the control works\n\t\t// standalone exactly as it does wrapped.\n\t\tconst field = useField();\n\n\t\tconst effectiveId = field?.controlId ?? id;\n\t\tconst effectiveDisabled = field?.disabled ?? disabled;\n\t\tconst effectiveRequired = field?.required ?? required;\n\t\tconst effectiveInvalid = field?.invalid ?? invalid;\n\n\t\tconst playCue = useSoundCue(sound);\n\n\t\t// The node, not a bare ref: the effect below must re-run when the\n\t\t// element is (re)bound, which a state-published node gives for free\n\t\t// (convention C-1).\n\t\tconst [input, inputRef] = useElementRef<HTMLInputElement>();\n\t\tconst composedRef = useComposedRefs(forwardedRef, inputRef);\n\n\t\t// No HTML attribute reflects `indeterminate` — it exists only as a DOM\n\t\t// property — so it has to be assigned imperatively. It also drives the\n\t\t// CSS `:indeterminate` pseudo-class that paints the box's fill, border\n\t\t// and drawn mark (checkbox.css), so it has to land before the browser\n\t\t// paints — a passive effect fires after paint and flashes the resting\n\t\t// look for one frame first. Re-running on every change (not just on\n\t\t// mount) is what keeps a later prop update in sync; keying on the node\n\t\t// also covers the element being (re)bound.\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tif (input) input.indeterminate = indeterminate;\n\t\t}, [input, indeterminate]);\n\n\t\t// The native `disabled` attribute already blocks real interaction, but\n\t\t// a synthetic event dispatched straight at the element — as a test\n\t\t// does — walks past that guard, so the handler repeats it. Reading the\n\t\t// DOM's own post-toggle `checked` (rather than computing `!checked`\n\t\t// ourselves) trusts the browser's native activation behaviour for a\n\t\t// checkbox, which already resolves an indeterminate box to a real\n\t\t// boolean on interaction; `indeterminate` is still cleared explicitly\n\t\t// below so the prop mirrors that outcome regardless of how faithfully\n\t\t// a given environment applies it.\n\t\tfunction handleChange(event: ChangeEvent<HTMLInputElement>) {\n\t\t\tif (effectiveDisabled) {\n\t\t\t\t// A disabled control must never let its visible state drift\n\t\t\t\t// from the app's own model. A real browser already refuses to\n\t\t\t\t// run the default toggle action on a disabled checkbox, but a\n\t\t\t\t// synthetic event dispatched straight at the element — as a\n\t\t\t\t// test does — can still mutate the DOM property directly, so\n\t\t\t\t// the handler puts it back rather than trusting the guard\n\t\t\t\t// above alone.\n\t\t\t\tevent.currentTarget.checked = checked;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst next = event.currentTarget.checked;\n\t\t\tif (!isControlled) setUncontrolledChecked(next);\n\t\t\tsetIndeterminate(false);\n\t\t\tplayCue(next ? \"toggle-on\" : \"toggle-off\");\n\t\t\tonCheckedChange?.(next);\n\t\t\tonChange?.(event);\n\t\t}\n\n\t\tconst wrapperClasses = cn(\n\t\t\t\"ft-checkbox inline-flex items-center gap-[10px] text-[13px]\",\n\t\t\teffectiveDisabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\",\n\t\t\tclassName\n\t\t);\n\n\t\treturn (\n\t\t\t<label className={wrapperClasses}>\n\t\t\t\t{/*\n\t\t\t\t\tThe mark is a SIBLING overlaid on the input, not a child of\n\t\t\t\t\tit: the control is a real `<input type=\"checkbox\">`, and an\n\t\t\t\t\t`<input>` is a void element that cannot contain an SVG. This\n\t\t\t\t\t`<span>` is the only box the markup gains — the positioning\n\t\t\t\t\tcontext the overlay needs. Clicking the mark still toggles,\n\t\t\t\t\tboth because it sits inside the `<label>` and because\n\t\t\t\t\t`pointer-events: none` hands the event straight to the input\n\t\t\t\t\tunderneath.\n\t\t\t\t*/}\n\t\t\t\t<span className=\"ft-checkbox-box\">\n\t\t\t\t\t<input\n\t\t\t\t\t\t{...rest}\n\t\t\t\t\t\tref={composedRef}\n\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\tclassName=\"ft-checkbox-control border-input\"\n\t\t\t\t\t\tid={effectiveId}\n\t\t\t\t\t\tname={name}\n\t\t\t\t\t\tvalue={value}\n\t\t\t\t\t\tchecked={checked}\n\t\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t\t\trequired={effectiveRequired}\n\t\t\t\t\t\taria-checked={indeterminate ? \"mixed\" : checked}\n\t\t\t\t\t\taria-invalid={effectiveInvalid ? \"true\" : ariaInvalid}\n\t\t\t\t\t\taria-describedby={\n\t\t\t\t\t\t\t[field?.describedBy, ariaDescribedby].filter(Boolean).join(\" \") || undefined\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={label ?? ariaLabel}\n\t\t\t\t\t\t{...(effectiveInvalid ? { \"data-invalid\": \"true\" } : {})}\n\t\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\t/>\n\t\t\t\t\t{/*\n\t\t\t\t\t\t`focusable=\"false\"` alongside `aria-hidden`: older\n\t\t\t\t\t\tengines put SVG elements in the tab order on their own,\n\t\t\t\t\t\tand a decorative mark that can be tabbed to is a\n\t\t\t\t\t\tkeyboard trap between the box and whatever follows it.\n\t\t\t\t\t*/}\n\t\t\t\t\t<svg\n\t\t\t\t\t\tclassName=\"ft-checkbox-mark\"\n\t\t\t\t\t\tviewBox=\"0 0 18 18\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tfocusable=\"false\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tclassName=\"ft-checkbox-mark-check\"\n\t\t\t\t\t\t\td=\"M4 9.2 7.2 12.4 14 5.6\"\n\t\t\t\t\t\t\tpathLength={1}\n\t\t\t\t\t\t\tvectorEffect=\"non-scaling-stroke\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tclassName=\"ft-checkbox-mark-dash\"\n\t\t\t\t\t\t\td=\"M4.5 9h9\"\n\t\t\t\t\t\t\tpathLength={1}\n\t\t\t\t\t\t\tvectorEffect=\"non-scaling-stroke\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</svg>\n\t\t\t\t</span>\n\t\t\t\t{children}\n\t\t\t</label>\n\t\t);\n\t}\n);\n\nCheckbox.displayName = \"Checkbox\";\n","type":"registry:ui","target":"@ui/fancy/components/checkbox/Checkbox.tsx"},{"path":"react/src/components/checkbox/checkbox.css","content":"/* Ported verbatim from the Svelte source's <style> block. Every rule was\n   already anchored on the component's own `ft-checkbox-*` class names, so no\n   port-added anchor was needed. */\n\n/* The positioning context for the mark, and nothing else — the box's own\n   geometry still lives entirely on the input below. */\n.ft-checkbox-box {\n\tposition: relative;\n\tdisplay: inline-flex;\n\tflex: none;\n}\n\n.ft-checkbox-control {\n\t--ft-checkbox-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n\tappearance: none;\n\tflex: none;\n\twidth: 18px;\n\theight: 18px;\n\tmargin: 0;\n\tborder-radius: 5px;\n\tborder-width: 1.5px;\n\tcursor: pointer;\n\t/* Colour only, so it stays OUTSIDE the reduced-motion query on purpose:\n\t   a colour change is not motion, and gating it would make the fill snap\n\t   for exactly the users who asked for less movement. 150ms =\n\t   tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =\n\t   tokens.EASINGS.inout. */\n\ttransition:\n\t\tbackground-color var(--ft-duration-fast, 150ms)\n\t\t\tvar(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1)),\n\t\tborder-color var(--ft-duration-fast, 150ms) var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));\n}\n\n.ft-checkbox-control:disabled {\n\tcursor: not-allowed;\n}\n\n.ft-checkbox-control:checked,\n.ft-checkbox-control:indeterminate {\n\tborder-color: transparent;\n\tbackground-color: var(--ft-checkbox-accent);\n}\n\n/*\n * The mark, overlaid on the filled box. Checked and indeterminate each get\n * their own distinct shape (a drawn tick vs. a flat dash) rather than only\n * a colour difference between the two — and both are real stroked paths\n * now, which is what makes the tick draw itself instead of appearing whole.\n */\n.ft-checkbox-mark {\n\tposition: absolute;\n\tinset: 0;\n\twidth: 18px;\n\theight: 18px;\n\tpointer-events: none;\n\t/* `stroke: currentColor` below plus a colour set HERE is also the whole\n\t   forced-colors story: in forced-colors mode the UA overrides `color`\n\t   and `background-color` from the same system palette, so the mark and\n\t   the box behind it are re-coloured together and can never disagree.\n\t   No `@media (forced-colors: active)` block, and nothing to keep in\n\t   sync with one. */\n\tcolor: white;\n}\n\n.ft-checkbox-mark path {\n\tfill: none;\n\tstroke: currentColor;\n\tstroke-width: 2;\n\tstroke-linecap: round;\n\tstroke-linejoin: round;\n\t/* The pathLength-normalised dash trick, the same idiom StatusMorph's\n\t   check and cross already use: `pathLength=\"1\"` on the path makes the\n\t   dash units fractions of its own length, so a single dash exactly as\n\t   long as the path (dasharray 1) is hidden at dashoffset 1 and fully\n\t   drawn at 0. Never `getTotalLength()` — that needs layout, and this\n\t   needs none. */\n\tstroke-dasharray: 1;\n\tstroke-dashoffset: 1;\n}\n\n/* Both paths are always present, each waiting at dashoffset 1 until its own\n   state matches. That is what makes indeterminate → checked one gesture:\n   the dash un-draws while the tick draws, over the same window, instead of\n   one shape being swapped for another. */\n.ft-checkbox-control:checked + .ft-checkbox-mark .ft-checkbox-mark-check,\n.ft-checkbox-control:indeterminate + .ft-checkbox-mark .ft-checkbox-mark-dash {\n\tstroke-dashoffset: 0;\n}\n\n/* `checked` and `indeterminate` can legitimately both be true — the prop\n   docs say so (\"`checked` is the real state underneath even while\n   `indeterminate` is true\"), and `aria-checked` reports \"mixed\" for it. The\n   two selectors above are independent, so on their own that combination\n   draws the tick and the dash superimposed: a struck-through check that\n   says neither thing. The mark has to agree with what the control already\n   announces, so the dash wins the shape. Equal specificity, later in\n   source, which is exactly enough to win — and the indeterminate → checked\n   gesture is untouched, because the change handler clears `indeterminate`\n   in the same tick it sets `checked`. */\n.ft-checkbox-control:indeterminate + .ft-checkbox-mark .ft-checkbox-mark-check {\n\tstroke-dashoffset: 1;\n}\n\n/*\n * The draw is the only thing gated: the resting and drawn dashoffsets above\n * are outside the query, so without motion the mark is simply there, whole,\n * the instant the box is checked. It is never invisible for want of an\n * animation.\n *\n * The fill runs `fast` (150ms) and the draw runs `base` (300ms), which is\n * not one state change announcing itself twice at two speeds: the two are\n * ordered, the box fills and then the mark is drawn onto the filled box —\n * the same shape, and the same 300ms/--ft-ease-out clock, StatusMorph's own\n * check already ships. One check-draw in the library, at one speed.\n * `--ft-checkbox-draw-duration` is the one knob if a given surface wants\n * the tick faster than the family default.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-checkbox-mark path {\n\t\t/* 300ms = tokens.DURATIONS.base, cubic-bezier(0.16, 1, 0.3, 1) = tokens.EASINGS.out */\n\t\ttransition: stroke-dashoffset var(--ft-checkbox-draw-duration, var(--ft-duration-base, 300ms))\n\t\t\tvar(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));\n\t}\n}\n\n.ft-checkbox-control:focus-visible {\n\toutline: none;\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-checkbox-accent) 25%, transparent);\n}\n\n/*\n * Echoes aria-invalid on the resting border, same as Input and\n * RadioGroupItem. On its own this would vanish exactly when the box\n * fills in for checked/indeterminate, since the border goes transparent\n * under the fill — the outline ring below is what survives that.\n */\n.ft-checkbox-control[data-invalid=\"true\"]:not(:checked):not(:indeterminate) {\n\tborder-color: var(--color-destructive, oklch(0.63 0.24 25));\n}\n\n/*\n * A ring outside the box, not a colour change to the fill or border — this\n * is what keeps the invalid cue visible once checked or indeterminate\n * fills the box and makes its own border transparent. It is unconditional\n * (not scoped to :checked), so resting, checked and indeterminate all\n * carry the same shape cue rather than only the resting look getting one.\n * Offset past the focus-visible halo (0–3px) so the two rings never\n * visually merge into one blurred band when both are active at once.\n */\n.ft-checkbox-control[data-invalid=\"true\"] {\n\toutline: 1.5px solid var(--color-destructive, oklch(0.63 0.24 25));\n\toutline-offset: 3px;\n}\n","type":"registry:ui","target":"@ui/fancy/components/checkbox/checkbox.css"},{"path":"react/src/components/checkbox/index.ts","content":"\"use client\";\n\nexport { Checkbox } from \"./Checkbox\";\nexport type { CheckboxProps } from \"./Checkbox\";\n","type":"registry:ui","target":"@ui/fancy/components/checkbox/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/checkbox?framework=react"}}